
org.mozilla.javascript.tools.debugger.Dim Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rhino-tools Show documentation
Show all versions of rhino-tools Show documentation
Rhino tools, including shell and debugger
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 java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Kit;
import org.mozilla.javascript.NativeCall;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.SecurityUtilities;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.debug.DebugFrame;
import org.mozilla.javascript.debug.DebuggableObject;
import org.mozilla.javascript.debug.DebuggableScript;
import org.mozilla.javascript.debug.Debugger;
/** 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 final Object monitor = new Object();
/** Synchronization object used to wait for valid {@link #interruptedContextData}. */
private final 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, StandardCharsets.UTF_8));
} 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) {
ArrayList functions = new ArrayList<>();
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, List 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();
boolean wasInterpreted = cx.isInterpretedMode();
cx.setDebugger(null, null);
cx.setInterpretedMode(false);
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.setInterpretedMode(wasInterpreted);
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}. */
@Override
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}. */
@SuppressWarnings("unchecked")
private void withContext() {
dim.contextFactory.call(this);
}
// ContextFactory.Listener
/** Called when a Context is created. */
@Override
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.setInterpretedMode(true);
}
/** Called when a Context is destroyed. */
@Override
public void contextReleased(Context cx) {
if (type != IPROXY_LISTEN) Kit.codeBug();
}
// Debugger
/** Returns a StackFrame for the given function or script. */
@Override
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. */
@Override
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 ArrayList frameStack = new ArrayList<>();
/** 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.add(frame);
}
/** Pops a stack frame from the stack. */
private void popFrame() {
frameStack.remove(frameStack.size() - 1);
}
}
/** 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. */
@Override
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. */
@Override
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. */
@Override
public void onExceptionThrown(Context cx, Throwable exception) {
dim.handleExceptionThrown(cx, exception, this);
}
/** Called when the stack frame has been left. */
@Override
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. */
@Override
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;
/** Lock for same */
private final Object breakpointsLock = new Object();
/** 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 (breakpointsLock) {
if (breakpoints[line] != value) {
breakpoints[line] = value;
changed = true;
} else {
changed = false;
}
}
return changed;
}
/** Removes all breakpoints from the script. */
public void removeAllBreakpoints() {
synchronized (breakpointsLock) {
for (int line = 0; line != breakpoints.length; ++line) {
breakpoints[line] = false;
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy