org.mozilla.javascript.tools.debugger.Dim Maven / Gradle / Ivy
The newest version!
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript.tools.debugger;
import org.mozilla.javascript.*;
import org.mozilla.javascript.debug.*;
import java.util.*;
import java.io.*;
import java.net.URL;
/**
* Dim or Debugger Implementation for Rhino.
*/
public class Dim {
// Constants for instructing the debugger what action to perform
// to end interruption. Used by 'returnValue'.
public static final int STEP_OVER = 0;
public static final int STEP_INTO = 1;
public static final int STEP_OUT = 2;
public static final int GO = 3;
public static final int BREAK = 4;
public static final int EXIT = 5;
// Constants for the DimIProxy interface implementation class.
private static final int IPROXY_DEBUG = 0;
private static final int IPROXY_LISTEN = 1;
private static final int IPROXY_COMPILE_SCRIPT = 2;
private static final int IPROXY_EVAL_SCRIPT = 3;
private static final int IPROXY_STRING_IS_COMPILABLE = 4;
private static final int IPROXY_OBJECT_TO_STRING = 5;
private static final int IPROXY_OBJECT_PROPERTY = 6;
private static final int IPROXY_OBJECT_IDS = 7;
/**
* Interface to the debugger GUI.
*/
private GuiCallback callback;
/**
* Whether the debugger should break.
*/
private boolean breakFlag;
/**
* The ScopeProvider object that provides the scope in which to
* evaluate script.
*/
private ScopeProvider scopeProvider;
/**
* The SourceProvider object that provides the source of evaluated scripts.
*/
private SourceProvider sourceProvider;
/**
* The index of the current stack frame.
*/
private int frameIndex = -1;
/**
* Information about the current stack at the point of interruption.
*/
private volatile ContextData interruptedContextData;
/**
* The ContextFactory to listen to for debugging information.
*/
private ContextFactory contextFactory;
/**
* Synchronization object used to allow script evaluations to
* happen when a thread is resumed.
*/
private Object monitor = new Object();
/**
* Synchronization object used to wait for valid
* {@link #interruptedContextData}.
*/
private Object eventThreadMonitor = new Object();
/**
* The action to perform to end the interruption loop.
*/
private volatile int returnValue = -1;
/**
* Whether the debugger is inside the interruption loop.
*/
private boolean insideInterruptLoop;
/**
* The requested script string to be evaluated when the thread
* has been resumed.
*/
private String evalRequest;
/**
* The stack frame in which to evaluate {@link #evalRequest}.
*/
private StackFrame evalFrame;
/**
* The result of evaluating {@link #evalRequest}.
*/
private String evalResult;
/**
* Whether the debugger should break when a script exception is thrown.
*/
private boolean breakOnExceptions;
/**
* Whether the debugger should break when a script function is entered.
*/
private boolean breakOnEnter;
/**
* Whether the debugger should break when a script function is returned
* from.
*/
private boolean breakOnReturn;
/**
* Table mapping URLs to information about the script source.
*/
private final Map urlToSourceInfo =
Collections.synchronizedMap(new HashMap());
/**
* Table mapping function names to information about the function.
*/
private final Map functionNames =
Collections.synchronizedMap(new HashMap());
/**
* Table mapping functions to information about the function.
*/
private final Map functionToSource =
Collections.synchronizedMap(new HashMap());
/**
* ContextFactory.Listener instance attached to {@link #contextFactory}.
*/
private DimIProxy listener;
/**
* Sets the GuiCallback object to use.
*/
public void setGuiCallback(GuiCallback callback) {
this.callback = callback;
}
/**
* Tells the debugger to break at the next opportunity.
*/
public void setBreak() {
this.breakFlag = true;
}
/**
* Sets the ScopeProvider to be used.
*/
public void setScopeProvider(ScopeProvider scopeProvider) {
this.scopeProvider = scopeProvider;
}
/**
* Sets the ScopeProvider to be used.
*/
public void setSourceProvider(final SourceProvider sourceProvider) {
this.sourceProvider = sourceProvider;
}
/**
* Switches context to the stack frame with the given index.
*/
public void contextSwitch(int frameIndex) {
this.frameIndex = frameIndex;
}
/**
* Sets whether the debugger should break on exceptions.
*/
public void setBreakOnExceptions(boolean breakOnExceptions) {
this.breakOnExceptions = breakOnExceptions;
}
/**
* Sets whether the debugger should break on function entering.
*/
public void setBreakOnEnter(boolean breakOnEnter) {
this.breakOnEnter = breakOnEnter;
}
/**
* Sets whether the debugger should break on function return.
*/
public void setBreakOnReturn(boolean breakOnReturn) {
this.breakOnReturn = breakOnReturn;
}
/**
* Attaches the debugger to the given ContextFactory.
*/
public void attachTo(ContextFactory factory) {
detach();
this.contextFactory = factory;
this.listener = new DimIProxy(this, IPROXY_LISTEN);
factory.addListener(this.listener);
}
/**
* Detaches the debugger from the current ContextFactory.
*/
public void detach() {
if (listener != null) {
contextFactory.removeListener(listener);
contextFactory = null;
listener = null;
}
}
/**
* Releases resources associated with this debugger.
*/
public void dispose() {
detach();
}
/**
* Returns the FunctionSource object for the given script or function.
*/
private FunctionSource getFunctionSource(DebuggableScript fnOrScript) {
FunctionSource fsource = functionSource(fnOrScript);
if (fsource == null) {
String url = getNormalizedUrl(fnOrScript);
SourceInfo si = sourceInfo(url);
if (si == null) {
if (!fnOrScript.isGeneratedScript()) {
// Not eval or Function, try to load it from URL
String source = loadSource(url);
if (source != null) {
DebuggableScript top = fnOrScript;
for (;;) {
DebuggableScript parent = top.getParent();
if (parent == null) {
break;
}
top = parent;
}
registerTopScript(top, source);
fsource = functionSource(fnOrScript);
}
}
}
}
return fsource;
}
/**
* Loads the script at the given URL.
*/
private String loadSource(String sourceUrl) {
String source = null;
int hash = sourceUrl.indexOf('#');
if (hash >= 0) {
sourceUrl = sourceUrl.substring(0, hash);
}
try {
InputStream is;
openStream:
{
if (sourceUrl.indexOf(':') < 0) {
// Can be a file name
try {
if (sourceUrl.startsWith("~/")) {
String home = SecurityUtilities.getSystemProperty("user.home");
if (home != null) {
String pathFromHome = sourceUrl.substring(2);
File f = new File(new File(home), pathFromHome);
if (f.exists()) {
is = new FileInputStream(f);
break openStream;
}
}
}
File f = new File(sourceUrl);
if (f.exists()) {
is = new FileInputStream(f);
break openStream;
}
} catch (SecurityException ex) { }
// No existing file, assume missed http://
if (sourceUrl.startsWith("//")) {
sourceUrl = "http:" + sourceUrl;
} else if (sourceUrl.startsWith("/")) {
sourceUrl = "http://127.0.0.1" + sourceUrl;
} else {
sourceUrl = "http://" + sourceUrl;
}
}
is = (new URL(sourceUrl)).openStream();
}
try {
source = Kit.readReader(new InputStreamReader(is));
} finally {
is.close();
}
} catch (IOException ex) {
System.err.println
("Failed to load source from "+sourceUrl+": "+ ex);
}
return source;
}
/**
* Registers the given script as a top-level script in the debugger.
*/
private void registerTopScript(DebuggableScript topScript, String source) {
if (!topScript.isTopLevel()) {
throw new IllegalArgumentException();
}
String url = getNormalizedUrl(topScript);
DebuggableScript[] functions = getAllFunctions(topScript);
if (sourceProvider != null) {
final String providedSource = sourceProvider.getSource(topScript);
if(providedSource != null) {
source = providedSource;
}
}
final SourceInfo sourceInfo = new SourceInfo(source, functions, url);
synchronized (urlToSourceInfo) {
SourceInfo old = urlToSourceInfo.get(url);
if (old != null) {
sourceInfo.copyBreakpointsFrom(old);
}
urlToSourceInfo.put(url, sourceInfo);
for (int i = 0; i != sourceInfo.functionSourcesTop(); ++i) {
FunctionSource fsource = sourceInfo.functionSource(i);
String name = fsource.name();
if (name.length() != 0) {
functionNames.put(name, fsource);
}
}
}
synchronized (functionToSource) {
for (int i = 0; i != functions.length; ++i) {
FunctionSource fsource = sourceInfo.functionSource(i);
functionToSource.put(functions[i], fsource);
}
}
callback.updateSourceText(sourceInfo);
}
/**
* Returns the FunctionSource object for the given function or script.
*/
private FunctionSource functionSource(DebuggableScript fnOrScript) {
return functionToSource.get(fnOrScript);
}
/**
* Returns an array of all function names.
*/
public String[] functionNames() {
synchronized (urlToSourceInfo) {
return functionNames.keySet().toArray(new String[functionNames.size()]);
}
}
/**
* Returns the FunctionSource object for the function with the given name.
*/
public FunctionSource functionSourceByName(String functionName) {
return functionNames.get(functionName);
}
/**
* Returns the SourceInfo object for the given URL.
*/
public SourceInfo sourceInfo(String url) {
return urlToSourceInfo.get(url);
}
/**
* Returns the source URL for the given script or function.
*/
private String getNormalizedUrl(DebuggableScript fnOrScript) {
String url = fnOrScript.getSourceName();
if (url == null) { url = ""; }
else {
// Not to produce window for eval from different lines,
// strip line numbers, i.e. replace all #[0-9]+\(eval\) by
// (eval)
// Option: similar teatment for Function?
char evalSeparator = '#';
StringBuilder sb = null;
int urlLength = url.length();
int cursor = 0;
for (;;) {
int searchStart = url.indexOf(evalSeparator, cursor);
if (searchStart < 0) {
break;
}
String replace = null;
int i = searchStart + 1;
while (i != urlLength) {
int c = url.charAt(i);
if (!('0' <= c && c <= '9')) {
break;
}
++i;
}
if (i != searchStart + 1) {
// i points after #[0-9]+
if ("(eval)".regionMatches(0, url, i, 6)) {
cursor = i + 6;
replace = "(eval)";
}
}
if (replace == null) {
break;
}
if (sb == null) {
sb = new StringBuilder();
sb.append(url.substring(0, searchStart));
}
sb.append(replace);
}
if (sb != null) {
if (cursor != urlLength) {
sb.append(url.substring(cursor));
}
url = sb.toString();
}
}
return url;
}
/**
* Returns an array of all functions in the given script.
*/
private static DebuggableScript[] getAllFunctions
(DebuggableScript function) {
ObjArray functions = new ObjArray();
collectFunctions_r(function, functions);
DebuggableScript[] result = new DebuggableScript[functions.size()];
functions.toArray(result);
return result;
}
/**
* Helper function for {@link #getAllFunctions(DebuggableScript)}.
*/
private static void collectFunctions_r(DebuggableScript function,
ObjArray array) {
array.add(function);
for (int i = 0; i != function.getFunctionCount(); ++i) {
collectFunctions_r(function.getFunction(i), array);
}
}
/**
* Clears all breakpoints.
*/
public void clearAllBreakpoints() {
for (SourceInfo si: urlToSourceInfo.values()) {
si.removeAllBreakpoints();
}
}
/**
* Called when a breakpoint has been hit.
*/
private void handleBreakpointHit(StackFrame frame, Context cx) {
breakFlag = false;
interrupted(cx, frame, null);
}
/**
* Called when a script exception has been thrown.
*/
private void handleExceptionThrown(Context cx, Throwable ex,
StackFrame frame) {
if (breakOnExceptions) {
ContextData cd = frame.contextData();
if (cd.lastProcessedException != ex) {
interrupted(cx, frame, ex);
cd.lastProcessedException = ex;
}
}
}
/**
* Returns the current ContextData object.
*/
public ContextData currentContextData() {
return interruptedContextData;
}
/**
* Sets the action to perform to end interruption.
*/
public void setReturnValue(int returnValue) {
synchronized (monitor) {
this.returnValue = returnValue;
monitor.notify();
}
}
/**
* Resumes execution of script.
*/
public void go() {
synchronized (monitor) {
this.returnValue = GO;
monitor.notifyAll();
}
}
/**
* Evaluates the given script.
*/
public String eval(String expr) {
String result = "undefined";
if (expr == null) {
return result;
}
ContextData contextData = currentContextData();
if (contextData == null || frameIndex >= contextData.frameCount()) {
return result;
}
StackFrame frame = contextData.getFrame(frameIndex);
if (contextData.eventThreadFlag) {
Context cx = Context.getCurrentContext();
result = do_eval(cx, frame, expr);
} else {
synchronized (monitor) {
if (insideInterruptLoop) {
evalRequest = expr;
evalFrame = frame;
monitor.notify();
do {
try {
monitor.wait();
} catch (InterruptedException exc) {
Thread.currentThread().interrupt();
break;
}
} while (evalRequest != null);
result = evalResult;
}
}
}
return result;
}
/**
* Compiles the given script.
*/
public void compileScript(String url, String text) {
DimIProxy action = new DimIProxy(this, IPROXY_COMPILE_SCRIPT);
action.url = url;
action.text = text;
action.withContext();
}
/**
* Evaluates the given script.
*/
public void evalScript(final String url, final String text) {
DimIProxy action = new DimIProxy(this, IPROXY_EVAL_SCRIPT);
action.url = url;
action.text = text;
action.withContext();
}
/**
* Converts the given script object to a string.
*/
public String objectToString(Object object) {
DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_TO_STRING);
action.object = object;
action.withContext();
return action.stringResult;
}
/**
* Returns whether the given string is syntactically valid script.
*/
public boolean stringIsCompilableUnit(String str) {
DimIProxy action = new DimIProxy(this, IPROXY_STRING_IS_COMPILABLE);
action.text = str;
action.withContext();
return action.booleanResult;
}
/**
* Returns the value of a property on the given script object.
*/
public Object getObjectProperty(Object object, Object id) {
DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_PROPERTY);
action.object = object;
action.id = id;
action.withContext();
return action.objectResult;
}
/**
* Returns an array of the property names on the given script object.
*/
public Object[] getObjectIds(Object object) {
DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_IDS);
action.object = object;
action.withContext();
return action.objectArrayResult;
}
/**
* Returns the value of a property on the given script object.
*/
private Object getObjectPropertyImpl(Context cx, Object object,
Object id) {
Scriptable scriptable = (Scriptable)object;
Object result;
if (id instanceof String) {
String name = (String)id;
if (name.equals("this")) {
result = scriptable;
} else if (name.equals("__proto__")) {
result = scriptable.getPrototype();
} else if (name.equals("__parent__")) {
result = scriptable.getParentScope();
} else {
result = ScriptableObject.getProperty(scriptable, name);
if (result == ScriptableObject.NOT_FOUND) {
result = Undefined.instance;
}
}
} else {
int index = ((Integer)id).intValue();
result = ScriptableObject.getProperty(scriptable, index);
if (result == ScriptableObject.NOT_FOUND) {
result = Undefined.instance;
}
}
return result;
}
/**
* Returns an array of the property names on the given script object.
*/
private Object[] getObjectIdsImpl(Context cx, Object object) {
if (!(object instanceof Scriptable) || object == Undefined.instance) {
return Context.emptyArgs;
}
Object[] ids;
Scriptable scriptable = (Scriptable)object;
if (scriptable instanceof DebuggableObject) {
ids = ((DebuggableObject)scriptable).getAllIds();
} else {
ids = scriptable.getIds();
}
Scriptable proto = scriptable.getPrototype();
Scriptable parent = scriptable.getParentScope();
int extra = 0;
if (proto != null) {
++extra;
}
if (parent != null) {
++extra;
}
if (extra != 0) {
Object[] tmp = new Object[extra + ids.length];
System.arraycopy(ids, 0, tmp, extra, ids.length);
ids = tmp;
extra = 0;
if (proto != null) {
ids[extra++] = "__proto__";
}
if (parent != null) {
ids[extra++] = "__parent__";
}
}
return ids;
}
/**
* Interrupts script execution.
*/
private void interrupted(Context cx, final StackFrame frame,
Throwable scriptException) {
ContextData contextData = frame.contextData();
boolean eventThreadFlag = callback.isGuiEventThread();
contextData.eventThreadFlag = eventThreadFlag;
boolean recursiveEventThreadCall = false;
interruptedCheck:
synchronized (eventThreadMonitor) {
if (eventThreadFlag) {
if (interruptedContextData != null) {
recursiveEventThreadCall = true;
break interruptedCheck;
}
} else {
while (interruptedContextData != null) {
try {
eventThreadMonitor.wait();
} catch (InterruptedException exc) {
return;
}
}
}
interruptedContextData = contextData;
}
if (recursiveEventThreadCall) {
// XXX: For now the following is commented out as on Linux
// too deep recursion of dispatchNextGuiEvent causes GUI lockout.
// Note: it can make GUI unresponsive if long-running script
// will be called on GUI thread while processing another interrupt
if (false) {
// Run event dispatch until gui sets a flag to exit the initial
// call to interrupted.
while (this.returnValue == -1) {
try {
callback.dispatchNextGuiEvent();
} catch (InterruptedException exc) {
}
}
}
return;
}
if (interruptedContextData == null) Kit.codeBug();
try {
do {
int frameCount = contextData.frameCount();
this.frameIndex = frameCount -1;
final String threadTitle = Thread.currentThread().toString();
final String alertMessage;
if (scriptException == null) {
alertMessage = null;
} else {
alertMessage = scriptException.toString();
}
int returnValue = -1;
if (!eventThreadFlag) {
synchronized (monitor) {
if (insideInterruptLoop) Kit.codeBug();
this.insideInterruptLoop = true;
this.evalRequest = null;
this.returnValue = -1;
callback.enterInterrupt(frame, threadTitle,
alertMessage);
try {
for (;;) {
try {
monitor.wait();
} catch (InterruptedException exc) {
Thread.currentThread().interrupt();
break;
}
if (evalRequest != null) {
this.evalResult = null;
try {
evalResult = do_eval(cx, evalFrame,
evalRequest);
} finally {
evalRequest = null;
evalFrame = null;
monitor.notify();
}
continue;
}
if (this.returnValue != -1) {
returnValue = this.returnValue;
break;
}
}
} finally {
insideInterruptLoop = false;
}
}
} else {
this.returnValue = -1;
callback.enterInterrupt(frame, threadTitle, alertMessage);
while (this.returnValue == -1) {
try {
callback.dispatchNextGuiEvent();
} catch (InterruptedException exc) {
}
}
returnValue = this.returnValue;
}
switch (returnValue) {
case STEP_OVER:
contextData.breakNextLine = true;
contextData.stopAtFrameDepth = contextData.frameCount();
break;
case STEP_INTO:
contextData.breakNextLine = true;
contextData.stopAtFrameDepth = -1;
break;
case STEP_OUT:
if (contextData.frameCount() > 1) {
contextData.breakNextLine = true;
contextData.stopAtFrameDepth
= contextData.frameCount() -1;
}
break;
}
} while (false);
} finally {
synchronized (eventThreadMonitor) {
interruptedContextData = null;
eventThreadMonitor.notifyAll();
}
}
}
/**
* Evaluates script in the given stack frame.
*/
private static String do_eval(Context cx, StackFrame frame, String expr) {
String resultString;
Debugger saved_debugger = cx.getDebugger();
Object saved_data = cx.getDebuggerContextData();
int saved_level = cx.getOptimizationLevel();
cx.setDebugger(null, null);
cx.setOptimizationLevel(-1);
cx.setGeneratingDebug(false);
try {
Callable script = (Callable)cx.compileString(expr, "", 0, null);
Object result = script.call(cx, frame.scope, frame.thisObj,
ScriptRuntime.emptyArgs);
if (result == Undefined.instance) {
resultString = "";
} else {
resultString = ScriptRuntime.toString(result);
}
} catch (Exception exc) {
resultString = exc.getMessage();
} finally {
cx.setGeneratingDebug(true);
cx.setOptimizationLevel(saved_level);
cx.setDebugger(saved_debugger, saved_data);
}
if (resultString == null) {
resultString = "null";
}
return resultString;
}
/**
* Proxy class to implement debug interfaces without bloat of class
* files.
*/
private static class DimIProxy
implements ContextAction, ContextFactory.Listener, Debugger {
/**
* The debugger.
*/
private Dim dim;
/**
* The interface implementation type. One of the IPROXY_* constants
* defined in {@link Dim}.
*/
private int type;
/**
* The URL origin of the script to compile or evaluate.
*/
private String url;
/**
* The text of the script to compile, evaluate or test for compilation.
*/
private String text;
/**
* The object to convert, get a property from or enumerate.
*/
private Object object;
/**
* The property to look up in {@link #object}.
*/
private Object id;
/**
* The boolean result of the action.
*/
private boolean booleanResult;
/**
* The String result of the action.
*/
private String stringResult;
/**
* The Object result of the action.
*/
private Object objectResult;
/**
* The Object[] result of the action.
*/
private Object[] objectArrayResult;
/**
* Creates a new DimIProxy.
*/
private DimIProxy(Dim dim, int type) {
this.dim = dim;
this.type = type;
}
// ContextAction
/**
* Performs the action given by {@link #type}.
*/
public Object run(Context cx) {
switch (type) {
case IPROXY_COMPILE_SCRIPT:
cx.compileString(text, url, 1, null);
break;
case IPROXY_EVAL_SCRIPT:
{
Scriptable scope = null;
if (dim.scopeProvider != null) {
scope = dim.scopeProvider.getScope();
}
if (scope == null) {
scope = new ImporterTopLevel(cx);
}
cx.evaluateString(scope, text, url, 1, null);
}
break;
case IPROXY_STRING_IS_COMPILABLE:
booleanResult = cx.stringIsCompilableUnit(text);
break;
case IPROXY_OBJECT_TO_STRING:
if (object == Undefined.instance) {
stringResult = "undefined";
} else if (object == null) {
stringResult = "null";
} else if (object instanceof NativeCall) {
stringResult = "[object Call]";
} else {
stringResult = Context.toString(object);
}
break;
case IPROXY_OBJECT_PROPERTY:
objectResult = dim.getObjectPropertyImpl(cx, object, id);
break;
case IPROXY_OBJECT_IDS:
objectArrayResult = dim.getObjectIdsImpl(cx, object);
break;
default:
throw Kit.codeBug();
}
return null;
}
/**
* Performs the action given by {@link #type} with the attached
* {@link ContextFactory}.
*/
private void withContext() {
dim.contextFactory.call(this);
}
// ContextFactory.Listener
/**
* Called when a Context is created.
*/
public void contextCreated(Context cx) {
if (type != IPROXY_LISTEN) Kit.codeBug();
ContextData contextData = new ContextData();
Debugger debugger = new DimIProxy(dim, IPROXY_DEBUG);
cx.setDebugger(debugger, contextData);
cx.setGeneratingDebug(true);
cx.setOptimizationLevel(-1);
}
/**
* Called when a Context is destroyed.
*/
public void contextReleased(Context cx) {
if (type != IPROXY_LISTEN) Kit.codeBug();
}
// Debugger
/**
* Returns a StackFrame for the given function or script.
*/
public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) {
if (type != IPROXY_DEBUG) Kit.codeBug();
FunctionSource item = dim.getFunctionSource(fnOrScript);
if (item == null) {
// Can not debug if source is not available
return null;
}
return new StackFrame(cx, dim, item);
}
/**
* Called when compilation is finished.
*/
public void handleCompilationDone(Context cx,
DebuggableScript fnOrScript,
String source) {
if (type != IPROXY_DEBUG) Kit.codeBug();
if (!fnOrScript.isTopLevel()) {
return;
}
dim.registerTopScript(fnOrScript, source);
}
}
/**
* Class to store information about a stack.
*/
public static class ContextData {
/**
* The stack frames.
*/
private ObjArray frameStack = new ObjArray();
/**
* Whether the debugger should break at the next line in this context.
*/
private boolean breakNextLine;
/**
* The frame depth the debugger should stop at. Used to implement
* "step over" and "step out".
*/
private int stopAtFrameDepth = -1;
/**
* Whether this context is in the event thread.
*/
private boolean eventThreadFlag;
/**
* The last exception that was processed.
*/
private Throwable lastProcessedException;
/**
* Returns the ContextData for the given Context.
*/
public static ContextData get(Context cx) {
return (ContextData) cx.getDebuggerContextData();
}
/**
* Returns the number of stack frames.
*/
public int frameCount() {
return frameStack.size();
}
/**
* Returns the stack frame with the given index.
*/
public StackFrame getFrame(int frameNumber) {
int num = frameStack.size() - frameNumber - 1;
return (StackFrame) frameStack.get(num);
}
/**
* Pushes a stack frame on to the stack.
*/
private void pushFrame(StackFrame frame) {
frameStack.push(frame);
}
/**
* Pops a stack frame from the stack.
*/
private void popFrame() {
frameStack.pop();
}
}
/**
* Object to represent one stack frame.
*/
public static class StackFrame implements DebugFrame {
/**
* The debugger.
*/
private Dim dim;
/**
* The ContextData for the Context being debugged.
*/
private ContextData contextData;
/**
* The scope.
*/
private Scriptable scope;
/**
* The 'this' object.
*/
private Scriptable thisObj;
/**
* Information about the function.
*/
private FunctionSource fsource;
/**
* Array of breakpoint state for each source line.
*/
private boolean[] breakpoints;
/**
* Current line number.
*/
private int lineNumber;
/**
* Creates a new StackFrame.
*/
private StackFrame(Context cx, Dim dim, FunctionSource fsource) {
this.dim = dim;
this.contextData = ContextData.get(cx);
this.fsource = fsource;
this.breakpoints = fsource.sourceInfo().breakpoints;
this.lineNumber = fsource.firstLine();
}
/**
* Called when the stack frame is entered.
*/
public void onEnter(Context cx, Scriptable scope,
Scriptable thisObj, Object[] args) {
contextData.pushFrame(this);
this.scope = scope;
this.thisObj = thisObj;
if (dim.breakOnEnter) {
dim.handleBreakpointHit(this, cx);
}
}
/**
* Called when the current position has changed.
*/
public void onLineChange(Context cx, int lineno) {
this.lineNumber = lineno;
if (!breakpoints[lineno] && !dim.breakFlag) {
boolean lineBreak = contextData.breakNextLine;
if (lineBreak && contextData.stopAtFrameDepth >= 0) {
lineBreak = (contextData.frameCount()
<= contextData.stopAtFrameDepth);
}
if (!lineBreak) {
return;
}
contextData.stopAtFrameDepth = -1;
contextData.breakNextLine = false;
}
dim.handleBreakpointHit(this, cx);
}
/**
* Called when an exception has been thrown.
*/
public void onExceptionThrown(Context cx, Throwable exception) {
dim.handleExceptionThrown(cx, exception, this);
}
/**
* Called when the stack frame has been left.
*/
public void onExit(Context cx, boolean byThrow,
Object resultOrException) {
if (dim.breakOnReturn && !byThrow) {
dim.handleBreakpointHit(this, cx);
}
contextData.popFrame();
}
/**
* Called when a 'debugger' statement is executed.
*/
public void onDebuggerStatement(Context cx) {
dim.handleBreakpointHit(this, cx);
}
/**
* Returns the SourceInfo object for the function.
*/
public SourceInfo sourceInfo() {
return fsource.sourceInfo();
}
/**
* Returns the ContextData object for the Context.
*/
public ContextData contextData() {
return contextData;
}
/**
* Returns the scope object for this frame.
*/
public Object scope() {
return scope;
}
/**
* Returns the 'this' object for this frame.
*/
public Object thisObj() {
return thisObj;
}
/**
* Returns the source URL.
*/
public String getUrl() {
return fsource.sourceInfo().url();
}
/**
* Returns the current line number.
*/
public int getLineNumber() {
return lineNumber;
}
/**
* Returns the current function name.
*/
public String getFunctionName() {
return fsource.name();
}
}
/**
* Class to store information about a function.
*/
public static class FunctionSource {
/**
* Information about the source of the function.
*/
private SourceInfo sourceInfo;
/**
* Line number of the first line of the function.
*/
private int firstLine;
/**
* The function name.
*/
private String name;
/**
* Creates a new FunctionSource.
*/
private FunctionSource(SourceInfo sourceInfo, int firstLine,
String name) {
if (name == null) throw new IllegalArgumentException();
this.sourceInfo = sourceInfo;
this.firstLine = firstLine;
this.name = name;
}
/**
* Returns the SourceInfo object that describes the source of the
* function.
*/
public SourceInfo sourceInfo() {
return sourceInfo;
}
/**
* Returns the line number of the first line of the function.
*/
public int firstLine() {
return firstLine;
}
/**
* Returns the name of the function.
*/
public String name() {
return name;
}
}
/**
* Class to store information about a script source.
*/
public static class SourceInfo {
/**
* An empty array of booleans.
*/
private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
/**
* The script.
*/
private String source;
/**
* The URL of the script.
*/
private String url;
/**
* Array indicating which lines can have breakpoints set.
*/
private boolean[] breakableLines;
/**
* Array indicating whether a breakpoint is set on the line.
*/
private boolean[] breakpoints;
/**
* Array of FunctionSource objects for the functions in the script.
*/
private FunctionSource[] functionSources;
/**
* Creates a new SourceInfo object.
*/
private SourceInfo(String source, DebuggableScript[] functions,
String normilizedUrl) {
this.source = source;
this.url = normilizedUrl;
int N = functions.length;
int[][] lineArrays = new int[N][];
for (int i = 0; i != N; ++i) {
lineArrays[i] = functions[i].getLineNumbers();
}
int minAll = 0, maxAll = -1;
int[] firstLines = new int[N];
for (int i = 0; i != N; ++i) {
int[] lines = lineArrays[i];
if (lines == null || lines.length == 0) {
firstLines[i] = -1;
} else {
int min, max;
min = max = lines[0];
for (int j = 1; j != lines.length; ++j) {
int line = lines[j];
if (line < min) {
min = line;
} else if (line > max) {
max = line;
}
}
firstLines[i] = min;
if (minAll > maxAll) {
minAll = min;
maxAll = max;
} else {
if (min < minAll) {
minAll = min;
}
if (max > maxAll) {
maxAll = max;
}
}
}
}
if (minAll > maxAll) {
// No line information
this.breakableLines = EMPTY_BOOLEAN_ARRAY;
this.breakpoints = EMPTY_BOOLEAN_ARRAY;
} else {
if (minAll < 0) {
// Line numbers can not be negative
throw new IllegalStateException(String.valueOf(minAll));
}
int linesTop = maxAll + 1;
this.breakableLines = new boolean[linesTop];
this.breakpoints = new boolean[linesTop];
for (int i = 0; i != N; ++i) {
int[] lines = lineArrays[i];
if (lines != null && lines.length != 0) {
for (int j = 0; j != lines.length; ++j) {
int line = lines[j];
this.breakableLines[line] = true;
}
}
}
}
this.functionSources = new FunctionSource[N];
for (int i = 0; i != N; ++i) {
String name = functions[i].getFunctionName();
if (name == null) {
name = "";
}
this.functionSources[i]
= new FunctionSource(this, firstLines[i], name);
}
}
/**
* Returns the source text.
*/
public String source() {
return this.source;
}
/**
* Returns the script's origin URL.
*/
public String url() {
return this.url;
}
/**
* Returns the number of FunctionSource objects stored in this object.
*/
public int functionSourcesTop() {
return functionSources.length;
}
/**
* Returns the FunctionSource object with the given index.
*/
public FunctionSource functionSource(int i) {
return functionSources[i];
}
/**
* Copies the breakpoints from the given SourceInfo object into this
* one.
*/
private void copyBreakpointsFrom(SourceInfo old) {
int end = old.breakpoints.length;
if (end > this.breakpoints.length) {
end = this.breakpoints.length;
}
for (int line = 0; line != end; ++line) {
if (old.breakpoints[line]) {
this.breakpoints[line] = true;
}
}
}
/**
* Returns whether the given line number can have a breakpoint set on
* it.
*/
public boolean breakableLine(int line) {
return (line < this.breakableLines.length)
&& this.breakableLines[line];
}
/**
* Returns whether there is a breakpoint set on the given line.
*/
public boolean breakpoint(int line) {
if (!breakableLine(line)) {
throw new IllegalArgumentException(String.valueOf(line));
}
return line < this.breakpoints.length && this.breakpoints[line];
}
/**
* Sets or clears the breakpoint flag for the given line.
*/
public boolean breakpoint(int line, boolean value) {
if (!breakableLine(line)) {
throw new IllegalArgumentException(String.valueOf(line));
}
boolean changed;
synchronized (breakpoints) {
if (breakpoints[line] != value) {
breakpoints[line] = value;
changed = true;
} else {
changed = false;
}
}
return changed;
}
/**
* Removes all breakpoints from the script.
*/
public void removeAllBreakpoints() {
synchronized (breakpoints) {
for (int line = 0; line != breakpoints.length; ++line) {
breakpoints[line] = false;
}
}
}
}
}