package com.oracle.truffle.tools.chromeinspector;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Phaser;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import com.oracle.truffle.tools.utils.json.JSONArray;
import com.oracle.truffle.tools.utils.json.JSONObject;
import com.oracle.truffle.api.debug.Breakpoint;
import com.oracle.truffle.api.debug.DebugException;
import com.oracle.truffle.api.debug.DebugScope;
import com.oracle.truffle.api.debug.DebugStackFrame;
import com.oracle.truffle.api.debug.DebugStackTraceElement;
import com.oracle.truffle.api.debug.DebugValue;
import com.oracle.truffle.api.debug.Debugger;
import com.oracle.truffle.api.debug.DebuggerSession;
import com.oracle.truffle.api.debug.SourceElement;
import com.oracle.truffle.api.debug.StepConfig;
import com.oracle.truffle.api.debug.SuspendAnchor;
import com.oracle.truffle.api.debug.SuspendedCallback;
import com.oracle.truffle.api.debug.SuspendedEvent;
import com.oracle.truffle.api.debug.SuspensionFilter;
import com.oracle.truffle.api.nodes.LanguageInfo;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.tools.chromeinspector.InspectorExecutionContext.CancellableRunnable;
import com.oracle.truffle.tools.chromeinspector.InspectorExecutionContext.NoSuspendedThreadException;
import com.oracle.truffle.tools.chromeinspector.InspectorExecutionContext.SuspendedThreadExecutor;
import com.oracle.truffle.tools.chromeinspector.ScriptsHandler.LoadScriptListener;
import com.oracle.truffle.tools.chromeinspector.commands.Params;
import com.oracle.truffle.tools.chromeinspector.commands.Result;
import com.oracle.truffle.tools.chromeinspector.domains.DebuggerDomain;
import com.oracle.truffle.tools.chromeinspector.events.Event;
import com.oracle.truffle.tools.chromeinspector.server.CommandProcessException;
import com.oracle.truffle.tools.chromeinspector.server.InspectServerSession.CommandPostProcessor;
import com.oracle.truffle.tools.chromeinspector.types.CallArgument;
import com.oracle.truffle.tools.chromeinspector.types.CallFrame;
import com.oracle.truffle.tools.chromeinspector.types.ExceptionDetails;
import com.oracle.truffle.tools.chromeinspector.types.Location;
import com.oracle.truffle.tools.chromeinspector.types.RemoteObject;
import com.oracle.truffle.tools.chromeinspector.types.Scope;
import com.oracle.truffle.tools.chromeinspector.types.Script;
import com.oracle.truffle.tools.chromeinspector.types.StackTrace;
import com.oracle.truffle.tools.chromeinspector.util.LineSearch;
import org.graalvm.collections.Pair;
public final class InspectorDebugger extends DebuggerDomain {
private static final StepConfig STEP_CONFIG = StepConfig.newBuilder().suspendAnchors(SourceElement.ROOT, SuspendAnchor.AFTER).build();
private static final Pattern FUNCTION_COMPLETION_PATTERN = Pattern.compile(
"\\(function\\s*\\((?<x>\\w+)\\)\\s*\\{\\s*var\\s+(?<a>\\w+)\\s*=\\s*\\[\\];\\s*" +
"for\\s*\\(var\\s+(?<o>\\w+)\\s*=\\s*\\k<x>;\\s*\\k<o>\\s*\\!==\\s*null\\s*&&\\s*typeof\\s+\\k<o>\\s*\\!==\\s*.undefined.;\\k<o>\\s*=\\s*\\k<o>\\.__proto__\\)\\s*\\{" +
"\\s*\\k<a>\\.push\\(Object\\.getOwnPropertyNames\\(\\k<o>\\)\\)" +
"\\};\\s*return\\s+\\k<a>\\}\\)\\((?<object>.*)\\)$");
private final InspectorExecutionContext context;
private final Object suspendLock = new Object();
private volatile SuspendedCallbackImpl suspendedCallback;
private volatile DebuggerSession debuggerSession;
private volatile ScriptsHandler scriptsHandler;
private volatile BreakpointsHandler breakpointsHandler;
private volatile DebuggerSuspendedInfo suspendedInfo;
private boolean running = true;
private boolean runningUnwind = false;
private boolean silentResume = false;
private volatile CommandLazyResponse commandLazyResponse;
private final AtomicBoolean delayUnlock = new AtomicBoolean();
private final Phaser onSuspendPhaser = new Phaser();
private final BlockingQueue<CancellableRunnable> suspendThreadExecutables = new LinkedBlockingQueue<>();
private final ReadWriteLock domainLock;
public InspectorDebugger(InspectorExecutionContext context, boolean suspend, ReadWriteLock domainLock) {
this.context = context;
this.domainLock = domainLock;
context.setSuspendThreadExecutor(new SuspendedThreadExecutor() {
@Override
public void execute(CancellableRunnable executable) throws NoSuspendedThreadException {
try {
synchronized (suspendLock) {
if (running) {
NoSuspendedThreadException.raise();
}
suspendThreadExecutables.put(executable);
suspendLock.notifyAll();
}
} catch (InterruptedException iex) {
throw new RuntimeException(iex);
}
}
});
if (suspend) {
Lock lock = domainLock.writeLock();
lock.lock();
try {
startSession();
} finally {
lock.unlock();
}
debuggerSession.suspendNextExecution();
}
}
private void startSession() {
Debugger tdbg = context.getEnv().lookup(context.getEnv().getInstruments().get("debugger"), Debugger.class);
suspendedCallback = new SuspendedCallbackImpl();
debuggerSession = tdbg.startSession(suspendedCallback, SourceElement.ROOT, SourceElement.STATEMENT);
debuggerSession.setSourcePath(context.getSourcePath());
debuggerSession.setSteppingFilter(SuspensionFilter.newBuilder().ignoreLanguageContextInitialization(!context.isInspectInitialization()).includeInternal(context.isInspectInternal()).build());
scriptsHandler = context.acquireScriptsHandler();
scriptsHandler.setDebuggerSession(debuggerSession);
breakpointsHandler = new BreakpointsHandler(debuggerSession, scriptsHandler, () -> eventHandler);
}
@Override
public void doEnable() {
if (debuggerSession == null) {
startSession();
}
scriptsHandler.addLoadScriptListener(new LoadScriptListenerImpl());
}
@Override
public void doDisable() {
assert debuggerSession != null;
scriptsHandler.setDebuggerSession(null);
debuggerSession.close();
debuggerSession = null;
suspendedCallback.dispose();
suspendedCallback = null;
context.releaseScriptsHandler();
scriptsHandler = null;
breakpointsHandler = null;
synchronized (suspendLock) {
if (!running) {
running = true;
suspendLock.notifyAll();
}
}
}
@Override
protected void notifyDisabled() {
if (debuggerSession != null) {
doDisable();
}
}
@Override
public void setAsyncCallStackDepth(int maxDepth) throws CommandProcessException {
if (maxDepth >= 0) {
debuggerSession.setAsynchronousStackDepth(maxDepth);
} else {
throw new CommandProcessException("Invalid async call stack depth: " + maxDepth);
}
}
@Override
public void setBlackboxPatterns(String[] patterns) {
final Pattern[] compiledPatterns = new Pattern[patterns.length];
for (int i = 0; i < patterns.length; i++) {
compiledPatterns[i] = Pattern.compile(patterns[i]);
}
debuggerSession.setSteppingFilter(SuspensionFilter.newBuilder().ignoreLanguageContextInitialization(!context.isInspectInitialization()).includeInternal(context.isInspectInternal()).sourceIs(
source -> !sourceMatchesBlackboxPatterns(source, compiledPatterns)).build());
}
@Override
public void setPauseOnExceptions(String state) throws CommandProcessException {
switch (state) {
case "none":
breakpointsHandler.setExceptionBreakpoint(false, false);
break;
case "uncaught":
breakpointsHandler.setExceptionBreakpoint(false, true);
break;
case "all":
breakpointsHandler.setExceptionBreakpoint(true, true);
break;
default:
throw new CommandProcessException("Unknown Pause on exceptions mode: " + state);
}
}
@Override
public Params getPossibleBreakpoints(Location start, Location end, boolean restrictToFunction) throws CommandProcessException {
if (start == null) {
throw new CommandProcessException("Start location required.");
}
int scriptId = start.getScriptId();
if (end != null && scriptId != end.getScriptId()) {
throw new CommandProcessException("Different location scripts: " + scriptId + ", " + end.getScriptId());
}
Script script = scriptsHandler.getScript(scriptId);
if (script == null) {
throw new CommandProcessException("Unknown scriptId: " + scriptId);
}
JSONObject json = new JSONObject();
JSONArray arr = new JSONArray();
Source source = script.getSource();
if (source.hasCharacters() && source.getLength() > 0) {
int lc = source.getLineCount();
int l1 = start.getLine();
int c1 = start.getColumn();
if (c1 <= 0) {
c1 = 1;
}
if (l1 > lc) {
l1 = lc;
c1 = source.getLineLength(l1);
}
int l2;
int c2;
if (end != null) {
l2 = end.getLine();
c2 = end.getColumn();
if (l1 != l2 || c1 != c2) {
if (l2 > lc) {
l2 = lc;
c2 = source.getLineLength(l2);
} else {
if (c2 <= 1) {
l2 = l2 - 1;
if (l2 <= 0) {
l2 = 1;
}
c2 = source.getLineLength(l2);
} else {
c2 = c2 - 1;
}
}
if (l1 > l2) {
l1 = l2;
}
}
} else {
l2 = l1;
c2 = source.getLineLength(l2);
}
if (c2 == 0) {
c2 = 1;
}
if (l1 == l2 && c2 < c1) {
c1 = c2;
}
SourceSection range = source.createSection(l1, c1, l2, c2);
Iterable<SourceSection> locations = SuspendableLocationFinder.findSuspendableLocations(range, restrictToFunction, debuggerSession, context.getEnv());
for (SourceSection ss : locations) {
arr.put(new Location(scriptId, ss.getStartLine(), ss.getStartColumn()).toJSON());
}
}
json.put("locations", arr);
return new Params(json);
}
@Override
public Params getScriptSource(String scriptId) throws CommandProcessException {
if (scriptId == null) {
throw new CommandProcessException("A scriptId required.");
}
CharSequence characters = getScript(scriptId).getCharacters();
JSONObject json = new JSONObject();
json.put("scriptSource", characters.toString());
return new Params(json);
}
private Script getScript(String scriptId) throws CommandProcessException {
Script script;
try {
script = scriptsHandler.getScript(Integer.parseInt(scriptId));
if (script == null) {
throw new CommandProcessException("Unknown scriptId: " + scriptId);
}
} catch (NumberFormatException nfe) {
throw new CommandProcessException(nfe.getMessage());
}
return script;
}
@Override
public void pause() {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp == null) {
debuggerSession.suspendNextExecution();
}
}
@Override
public void resume(CommandPostProcessor postProcessor) {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
postProcessor.setPostProcessJob(() -> doResume());
}
}
@Override
public void stepInto(CommandPostProcessor postProcessor) {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
susp.getSuspendedEvent().prepareStepInto(STEP_CONFIG);
delayUnlock.set(true);
postProcessor.setPostProcessJob(() -> doResume());
}
}
@Override
public void stepOver(CommandPostProcessor postProcessor) {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
susp.getSuspendedEvent().prepareStepOver(STEP_CONFIG);
delayUnlock.set(true);
postProcessor.setPostProcessJob(() -> doResume());
}
}
@Override
public void stepOut(CommandPostProcessor postProcessor) {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
susp.getSuspendedEvent().prepareStepOut(STEP_CONFIG);
delayUnlock.set(true);
postProcessor.setPostProcessJob(() -> doResume());
}
}
private void doResume() {
synchronized (suspendLock) {
if (!running) {
running = true;
suspendLock.notifyAll();
}
}
try {
onSuspendPhaser.awaitAdvanceInterruptibly(0);
} catch (InterruptedException ex) {
}
}
private CallFrame[] createCallFrames(Iterable<DebugStackFrame> frames, SuspendAnchor topAnchor, DebugValue returnValue) {
return createCallFrames(frames, topAnchor, returnValue, null);
}
CallFrame[] refreshCallFrames(Iterable<DebugStackFrame> frames, SuspendAnchor topAnchor, CallFrame[] oldFrames) {
DebugValue returnValue = null;
if (oldFrames.length > 0 && oldFrames[0].getReturnValue() != null) {
returnValue = oldFrames[0].getReturnValue().getDebugValue();
}
return createCallFrames(frames, topAnchor, returnValue, oldFrames);
}
private CallFrame[] createCallFrames(Iterable<DebugStackFrame> frames, SuspendAnchor topAnchor, DebugValue returnValue, CallFrame[] oldFrames) {
List<CallFrame> cfs = new ArrayList<>();
int depth = 0;
int depthAll = -1;
if (scriptsHandler == null || debuggerSession == null) {
return new CallFrame[0];
}
for (DebugStackFrame frame : frames) {
depthAll++;
SourceSection sourceSection = frame.getSourceSection();
if (sourceSection == null || !sourceSection.isAvailable()) {
continue;
}
if (!context.isInspectInternal() && frame.isInternal()) {
continue;
}
Source source = sourceSection.getSource();
if (!context.isInspectInternal() && source.isInternal()) {
continue;
}
int scriptId = scriptsHandler.assureLoaded(source);
if (scriptId < 0) {
continue;
}
Script script = scriptsHandler.getScript(scriptId);
List<Scope> scopes = new ArrayList<>();
DebugScope dscope;
try {
dscope = frame.getScope();
} catch (DebugException ex) {
PrintWriter err = context.getErr();
if (err != null) {
err.println("getScope() has caused " + ex);
ex.printStackTrace(err);
}
dscope = null;
}
String scopeType = "block";
boolean wasFunction = false;
SourceSection functionSourceSection = null;
DebugValue thisValue = null;
if (dscope == null) {
functionSourceSection = sourceSection;
}
Scope[] oldScopes;
if (oldFrames != null && oldFrames.length > depth) {
oldScopes = oldFrames[depth].getScopeChain();
} else {
oldScopes = null;
}
int scopeIndex = 0;
while (dscope != null) {
if (wasFunction) {
scopeType = "closure";
} else if (dscope.isFunctionScope()) {
scopeType = "local";
functionSourceSection = dscope.getSourceSection();
thisValue = dscope.getReceiver();
wasFunction = true;
}
addScope(scopes, dscope, scopeType, scopeIndex, oldScopes);
dscope = getParent(dscope);
scopeIndex++;
}
try {
dscope = debuggerSession.getTopScope(source.getLanguage());
} catch (DebugException ex) {
PrintWriter err = context.getErr();
if (err != null) {
err.println("getTopScope() has caused " + ex);
ex.printStackTrace(err);
}
}
while (dscope != null) {
addScope(scopes, dscope, "global", scopeIndex, oldScopes);
dscope = getParent(dscope);
scopeIndex++;
}
RemoteObject returnObj = null;
if (depthAll == 0 && returnValue != null) {
returnObj = context.getRemoteObjectsHandler().getRemote(returnValue);
}
SuspendAnchor anchor = (depthAll == 0) ? topAnchor : SuspendAnchor.BEFORE;
RemoteObject thisObj;
if (thisValue != null) {
thisObj = context.getRemoteObjectsHandler().getRemote(thisValue);
} else {
thisObj = RemoteObject.createNullObject(context.getEnv(), frame.getLanguage());
}
CallFrame cf = new CallFrame(frame, depth++, script, sourceSection, anchor, functionSourceSection,
thisObj, returnObj, scopes.toArray(new Scope[scopes.size()]));
cfs.add(cf);
}
return cfs.toArray(new CallFrame[cfs.size()]);
}
private void addScope(List<Scope> scopes, DebugScope dscope, String scopeType, int scopeIndex, Scope[] oldScopes) {
if (dscope.isFunctionScope() || dscope.getDeclaredValues().iterator().hasNext()) {
String lastId = getLastScopeId(oldScopes, scopeIndex);
scopes.add(createScope(scopeType, dscope, scopeIndex, lastId));
}
}
private static String getLastScopeId(Scope[] oldScopes, int scopeIndex) {
if (oldScopes != null) {
for (Scope scope : oldScopes) {
if (scope.getInternalIndex() == scopeIndex) {
return scope.getObject().getId();
}
}
}
return null;
}
private Scope createScope(String scopeType, DebugScope dscope, int index, String lastId) {
RemoteObject scopeVars = new RemoteObject(dscope, lastId);
context.getRemoteObjectsHandler().register(scopeVars);
return new Scope(scopeType, scopeVars, dscope.getName(), null, null, index);
}
private DebugScope getParent(DebugScope dscope) {
DebugScope parentScope;
try {
parentScope = dscope.getParent();
} catch (DebugException ex) {
PrintWriter err = context.getErr();
if (err != null) {
err.println("Scope.getParent() has caused " + ex);
ex.printStackTrace(err);
}
parentScope = null;
}
return parentScope;
}
@Override
public Params searchInContent(String scriptId, String query, boolean caseSensitive, boolean isRegex) throws CommandProcessException {
if (scriptId.isEmpty() || query.isEmpty()) {
throw new CommandProcessException("Must specify both scriptId and query.");
}
Source source = getScript(scriptId).getSource();
JSONArray matchLines;
try {
matchLines = LineSearch.matchLines(source, query, caseSensitive, isRegex);
} catch (PatternSyntaxException ex) {
throw new CommandProcessException(ex.getDescription());
}
JSONObject match = new JSONObject();
match.put("properties", matchLines);
return new Params(match);
}
@Override
public void setBreakpointsActive(Optional<Boolean> active) throws CommandProcessException {
if (!active.isPresent()) {
throw new CommandProcessException("Must specify active argument.");
}
debuggerSession.setBreakpointsActive(Breakpoint.Kind.SOURCE_LOCATION, active.get());
}
@Override
public void setSkipAllPauses(Optional<Boolean> skip) throws CommandProcessException {
if (!skip.isPresent()) {
throw new CommandProcessException("Must specify 'skip' argument.");
}
boolean active = !skip.get();
for (Breakpoint.Kind kind : Breakpoint.Kind.values()) {
debuggerSession.setBreakpointsActive(kind, active);
}
}
@Override
public Params setBreakpointByUrl(String url, String urlRegex, int line, int column, String condition) throws CommandProcessException {
if (url.isEmpty() && urlRegex.isEmpty()) {
throw new CommandProcessException("Must specify either url or urlRegex.");
}
if (line <= 0) {
throw new CommandProcessException("Must specify line number.");
}
if (!url.isEmpty()) {
return breakpointsHandler.createURLBreakpoint(url, line, column, condition);
} else {
return breakpointsHandler.createURLBreakpoint(Pattern.compile(urlRegex), line, column, condition);
}
}
@Override
public Params setBreakpoint(Location location, String condition) throws CommandProcessException {
if (location == null) {
throw new CommandProcessException("Must specify location.");
}
return breakpointsHandler.createBreakpoint(location, condition);
}
@Override
public Params setBreakpointOnFunctionCall(String functionObjectId, String condition) throws CommandProcessException {
if (functionObjectId == null) {
throw new CommandProcessException("Must specify function object ID.");
}
RemoteObject functionObject = context.getRemoteObjectsHandler().getRemote(functionObjectId);
if (functionObject != null) {
DebugValue functionValue = functionObject.getDebugValue();
try {
return context.executeInSuspendThread(new SuspendThreadExecutable<Params>() {
@Override
public Params executeCommand() throws CommandProcessException {
return breakpointsHandler.createFunctionBreakpoint(functionValue, condition);
}
@Override
public Params processException(DebugException dex) {
return new Params(new JSONObject());
}
});
} catch (NoSuspendedThreadException e) {
return new Params(new JSONObject());
}
} else {
throw new CommandProcessException("Function with object ID " + functionObjectId + " does not exist.");
}
}
@Override
public void removeBreakpoint(String id) throws CommandProcessException {
if (!breakpointsHandler.removeBreakpoint(id)) {
throw new CommandProcessException("No breakpoint with id '" + id + "'");
}
}
@Override
public void continueToLocation(Location location, CommandPostProcessor postProcessor) throws CommandProcessException {
if (location == null) {
throw new CommandProcessException("Must specify location.");
}
breakpointsHandler.createOneShotBreakpoint(location);
resume(postProcessor);
}
static String getEvalNonInteractiveMessage() {
return "<Can not evaluate in a non-interactive language>";
}
@Override
public Params evaluateOnCallFrame(String callFrameId, String expressionOrig, String objectGroup,
boolean includeCommandLineAPI, boolean silent, boolean returnByValue,
boolean generatePreview, boolean throwOnSideEffect) throws CommandProcessException {
if (callFrameId == null) {
throw new CommandProcessException("A callFrameId required.");
}
if (expressionOrig == null) {
throw new CommandProcessException("An expression required.");
}
int frameId;
try {
frameId = Integer.parseInt(callFrameId);
} catch (NumberFormatException ex) {
throw new CommandProcessException(ex.getLocalizedMessage());
}
ConsoleUtilitiesAPI cuAPI;
if (includeCommandLineAPI) {
cuAPI = ConsoleUtilitiesAPI.parse(expressionOrig);
} else {
cuAPI = null;
}
final String expression;
if (cuAPI != null) {
expression = cuAPI.getExpression();
} else {
expression = expressionOrig;
}
JSONObject jsonResult;
try {
jsonResult = context.executeInSuspendThread(new SuspendThreadExecutable<JSONObject>() {
@Override
public JSONObject executeCommand() throws CommandProcessException {
if (frameId >= suspendedInfo.getCallFrames().length) {
throw new CommandProcessException("Too big callFrameId: " + frameId);
}
CallFrame cf = suspendedInfo.getCallFrames()[frameId];
JSONObject json = new JSONObject();
if (runSpecialFunctions(expression, cf, generatePreview, json)) {
return json;
}
DebugValue value = getVarValue(expression, cf);
if (value == null) {
try {
value = cf.getFrame().eval(expression);
suspendedInfo.refreshFrames();
} catch (IllegalStateException ex) {
}
}
if (value == null) {
LanguageInfo languageInfo = cf.getFrame().getLanguage();
if (languageInfo == null || !languageInfo.isInteractive()) {
String errorMessage = getEvalNonInteractiveMessage();
ExceptionDetails exceptionDetails = new ExceptionDetails(errorMessage);
json.put("exceptionDetails", exceptionDetails.createJSON(context, generatePreview));
JSONObject err = new JSONObject();
err.putOpt("value", errorMessage);
err.putOpt("type", "string");
json.put("result", err);
}
}
if (value != null) {
if (cuAPI != null) {
value = cuAPI.process(value, breakpointsHandler);
if (value == null) {
return json;
}
}
RemoteObject ro = new RemoteObject(value, generatePreview, context);
context.getRemoteObjectsHandler().register(ro, objectGroup);
json.put("result", ro.toJSON());
}
return json;
}
@Override
public JSONObject processException(DebugException dex) {
JSONObject json = new JSONObject();
InspectorRuntime.fillExceptionDetails(json, dex, context, generatePreview);
DebugValue exceptionObject = dex.getExceptionObject();
if (exceptionObject != null) {
RemoteObject ro = context.createAndRegister(exceptionObject, generatePreview);
json.put("result", ro.toJSON());
} else {
JSONObject err = new JSONObject();
err.putOpt("value", dex.getLocalizedMessage());
err.putOpt("type", "string");
json.put("result", err);
}
return json;
}
});
} catch (NoSuspendedThreadException e) {
jsonResult = new JSONObject();
JSONObject err = new JSONObject();
err.putOpt("value", e.getLocalizedMessage());
jsonResult.put("result", err);
}
return new Params(jsonResult);
}
private boolean runSpecialFunctions(String expression, CallFrame cf, boolean generatePreview, JSONObject json) {
Matcher completionMatcher = FUNCTION_COMPLETION_PATTERN.matcher(expression);
if (completionMatcher.matches()) {
String objectOfCompletion = completionMatcher.group("object");
DebugValue value = getVarValue(objectOfCompletion, cf);
if (value == null) {
try {
value = cf.getFrame().eval(objectOfCompletion);
} catch (IllegalStateException ex) {
}
}
if (value != null) {
JSONObject result = InspectorRuntime.createCodecompletion(value, null, generatePreview, context, false);
json.put("result", result);
return true;
}
}
return false;
}
static DebugValue getVarValue(String name, CallFrame cf) {
for (Scope scope : cf.getScopeChain()) {
DebugScope debugScope = scope.getObject().getScope();
DebugValue var = debugScope.getDeclaredValue(name);
if (var != null) {
return var;
}
DebugValue receiver = debugScope.getReceiver();
if (receiver != null && name.equals(receiver.getName())) {
return receiver;
}
}
return null;
}
@Override
public Params restartFrame(long cmdId, String callFrameId, CommandPostProcessor postProcessor) throws CommandProcessException {
if (callFrameId == null) {
throw new CommandProcessException("A callFrameId required.");
}
int frameId;
try {
frameId = Integer.parseInt(callFrameId);
} catch (NumberFormatException ex) {
throw new CommandProcessException(ex.getLocalizedMessage());
}
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
if (frameId >= susp.getCallFrames().length) {
throw new CommandProcessException("Too big callFrameId: " + frameId);
}
CallFrame cf = susp.getCallFrames()[frameId];
susp.getSuspendedEvent().prepareUnwindFrame(cf.getFrame());
postProcessor.setPostProcessJob(() -> {
silentResume = true;
commandLazyResponse = (DebuggerSuspendedInfo suspInfo) -> {
JSONObject res = new JSONObject();
res.put("callFrames", getFramesParam(suspInfo.getCallFrames()));
return new Event(cmdId, new Result(new Params(res)));
};
runningUnwind = true;
doResume();
});
}
return new Params(null);
}
@Override
public void setVariableValue(int scopeNumber, String variableName, CallArgument newValue, String callFrameId) throws CommandProcessException {
if (variableName == null) {
throw new CommandProcessException("A variableName required.");
}
if (newValue == null) {
throw new CommandProcessException("A newValue required.");
}
if (callFrameId == null) {
throw new CommandProcessException("A callFrameId required.");
}
int frameId;
try {
frameId = Integer.parseInt(callFrameId);
} catch (NumberFormatException ex) {
throw new CommandProcessException(ex.getLocalizedMessage());
}
try {
context.executeInSuspendThread(new SuspendThreadExecutable<Void>() {
@Override
public Void executeCommand() throws CommandProcessException {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
if (frameId >= susp.getCallFrames().length) {
throw new CommandProcessException("Too big callFrameId: " + frameId);
}
CallFrame cf = susp.getCallFrames()[frameId];
Scope[] scopeChain = cf.getScopeChain();
if (scopeNumber < 0 || scopeNumber >= scopeChain.length) {
throw new CommandProcessException("Wrong scopeNumber: " + scopeNumber + ", there are " + scopeChain.length + " scopes.");
}
Scope scope = scopeChain[scopeNumber];
DebugScope debugScope = scope.getObject().getScope();
DebugValue debugValue = debugScope.getDeclaredValue(variableName);
Pair<DebugValue, Object> evaluatedValue = susp.lastEvaluatedValue.getAndSet(null);
try {
if (evaluatedValue != null && Objects.equals(evaluatedValue.getRight(), newValue.getPrimitiveValue())) {
debugValue.set(evaluatedValue.getLeft());
} else {
context.setValue(debugValue, newValue);
}
} catch (DebugException ex) {
PrintWriter err = context.getErr();
if (err != null) {
err.println("set of " + debugValue.getName() + " has caused " + ex);
ex.printStackTrace(err);
}
throw ex;
}
}
return null;
}
@Override
public Void processException(DebugException dex) {
return null;
}
});
} catch (NoSuspendedThreadException ex) {
throw new CommandProcessException(ex.getLocalizedMessage());
}
}
@Override
public void setReturnValue(CallArgument newValue) throws CommandProcessException {
if (newValue == null) {
throw new CommandProcessException("A newValue required.");
}
try {
context.executeInSuspendThread(new SuspendThreadExecutable<Void>() {
@Override
public Void executeCommand() throws CommandProcessException {
DebuggerSuspendedInfo susp = suspendedInfo;
if (susp != null) {
SuspendedEvent suspendedEvent = susp.getSuspendedEvent();
DebugValue returnValue = suspendedEvent.getReturnValue();
context.setValue(returnValue, newValue);
susp.getSuspendedEvent().setReturnValue(returnValue);
}
return null;
}
@Override
public Void processException(DebugException dex) {
return null;
}
});
} catch (NoSuspendedThreadException ex) {
throw new CommandProcessException(ex.getLocalizedMessage());
}
}
public boolean sourceMatchesBlackboxPatterns(Source source, Pattern[] patterns) {
String uri = scriptsHandler.getSourceURL(source);
for (Pattern pattern : patterns) {
if (pattern.pattern().equals(source.getName())) {
return true;
}
Matcher matcher = pattern.matcher(uri);
if (matcher.matches() || pattern.pattern().endsWith("$") && matcher.find()) {
return true;
}
String path = source.getPath();
int idx = path != null ? path.lastIndexOf(File.separatorChar) : -1;
if (idx > 0) {
path = path.substring(0, idx);
if (path.endsWith(File.separator + pattern.pattern())) {
return true;
}
}
}
return false;
}
private class LoadScriptListenerImpl implements LoadScriptListener {
@Override
public void loadedScript(Script script) {
JSONObject jsonParams = new JSONObject();
jsonParams.put("scriptId", Integer.toString(script.getId()));
jsonParams.put("url", script.getUrl());
jsonParams.put("startLine", 0);
jsonParams.put("startColumn", 0);
Source source = script.getSource();
int lastLine;
int lastColumn;
int length;
if (source.hasCharacters()) {
lastLine = source.getLineCount() - 1;
if (lastLine < 0) {
lastLine = 0;
lastColumn = 0;
} else {
lastColumn = source.getLineLength(lastLine + 1);
int srcMapLine = lastLine + 1;
CharSequence line;
do {
line = source.getCharacters(srcMapLine);
srcMapLine--;
} while (srcMapLine > 0 && (line.length() == 0 || "});".equals(line)));
CharSequence sourceMapURL = (srcMapLine > 0) ? getSourceMapURL(source, srcMapLine) : null;
if (sourceMapURL != null) {
jsonParams.put("sourceMapURL", sourceMapURL);
lastLine = srcMapLine - 1;
lastColumn = source.getLineLength(lastLine + 1);
}
}
length = source.getLength();
} else {
lastLine = 3;
lastColumn = 0;
length = script.getCharacters().length();
}
jsonParams.put("endLine", lastLine);
jsonParams.put("endColumn", lastColumn);
jsonParams.put("executionContextId", context.getId());
jsonParams.put("hash", script.getHash());
jsonParams.put("length", length);
Params params = new Params(jsonParams);
Event scriptParsed = new Event("Debugger.scriptParsed", params);
eventHandler.event(scriptParsed);
}
private CharSequence getSourceMapURL(Source source, int lastLine) {
String mapKeyword = "sourceMappingURL=";
int mapKeywordLenght = mapKeyword.length();
CharSequence line = source.getCharacters(lastLine + 1);
int lineLength = line.length();
int i = 0;
while (i < lineLength && Character.isWhitespace(line.charAt(i))) {
i++;
}
if (i + 3 < lineLength && line.charAt(i) == '/' && line.charAt(i + 1) == '/' &&
(line.charAt(i + 2) == '#' || line.charAt(i + 2) == '@')) {
i += 3;
} else {
return null;
}
while (i < lineLength && Character.isWhitespace(line.charAt(i))) {
i++;
}
if (i + mapKeywordLenght < lineLength && line.subSequence(i, i + mapKeywordLenght).equals(mapKeyword)) {
i += mapKeywordLenght;
} else {
return null;
}
return line.subSequence(i, line.length());
}
}
private class SuspendedCallbackImpl implements SuspendedCallback {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, new SchedulerThreadFactory());
private final AtomicReference<ScheduledFuture<?>> future = new AtomicReference<>();
private Thread locked = null;
@Override
public void onSuspend(SuspendedEvent se) {
try {
context.waitForRunPermission();
} catch (InterruptedException ex) {
}
SourceSection ss = se.getSourceSection();
lock();
onSuspendPhaser.register();
try {
Event paused;
Lock lock = domainLock.readLock();
lock.lock();
try {
if (debuggerSession == null) {
return;
}
if (se.hasSourceElement(SourceElement.ROOT) && !se.hasSourceElement(SourceElement.STATEMENT) && se.getSuspendAnchor() == SuspendAnchor.BEFORE && se.getBreakpoints().isEmpty()) {
debuggerSession.suspendNextExecution();
return;
}
synchronized (suspendLock) {
running = false;
}
if (!runningUnwind) {
scriptsHandler.assureLoaded(ss.getSource());
context.setLastLanguage(ss.getSource().getLanguage(), ss.getSource().getMimeType());
} else {
runningUnwind = false;
}
JSONObject jsonParams = new JSONObject();
DebugValue returnValue = se.getReturnValue();
if (!se.hasSourceElement(SourceElement.ROOT)) {
returnValue = null;
}
CallFrame[] callFrames = createCallFrames(se.getStackFrames(), se.getSuspendAnchor(), returnValue);
suspendedInfo = new DebuggerSuspendedInfo(InspectorDebugger.this, se, callFrames);
context.setSuspendedInfo(suspendedInfo);
if (commandLazyResponse != null) {
paused = commandLazyResponse.getResponse(suspendedInfo);
commandLazyResponse = null;
} else {
jsonParams.put("callFrames", getFramesParam(callFrames));
jsonParams.putOpt("asyncStackTrace", findAsyncStackTrace(se.getAsynchronousStacks()));
List<Breakpoint> breakpoints = se.getBreakpoints();
JSONArray bpArr = new JSONArray();
Set<Breakpoint.Kind> kinds = new HashSet<>(1);
for (Breakpoint bp : breakpoints) {
String id = breakpointsHandler.getId(bp);
if (id != null) {
bpArr.put(id);
}
kinds.add(bp.getKind());
}
jsonParams.put("reason", getHaltReason(kinds));
JSONObject data = getHaltData(se);
if (data != null) {
jsonParams.put("data", data);
}
jsonParams.put("hitBreakpoints", bpArr);
Params params = new Params(jsonParams);
paused = new Event("Debugger.paused", params);
}
} finally {
lock.unlock();
}
eventHandler.event(paused);
List<CancellableRunnable> executables;
for (;;) {
executables = null;
synchronized (suspendLock) {
if (!running && suspendThreadExecutables.isEmpty()) {
if (context.isSynchronous()) {
running = true;
} else {
try {
suspendLock.wait();
} catch (InterruptedException ex) {
}
}
}
if (!suspendThreadExecutables.isEmpty()) {
executables = new LinkedList<>();
CancellableRunnable r;
while ((r = suspendThreadExecutables.poll()) != null) {
executables.add(r);
}
}
if (running) {
suspendedInfo = null;
context.setSuspendedInfo(null);
break;
}
}
if (executables != null) {
for (CancellableRunnable r : executables) {
r.run();
}
executables = null;
}
}
if (executables != null) {
for (CancellableRunnable r : executables) {
r.cancel();
}
}
if (!silentResume) {
Event resumed = new Event("Debugger.resumed", null);
eventHandler.event(resumed);
} else {
silentResume = false;
}
} finally {
onSuspendPhaser.arrive();
if (delayUnlock.getAndSet(false)) {
future.set(scheduler.schedule(() -> {
unlock();
}, 1, TimeUnit.SECONDS));
} else {
unlock();
}
}
}
private synchronized void lock() {
Thread current = Thread.currentThread();
if (locked != current) {
while (locked != null) {
try {
wait();
} catch (InterruptedException ex) {
}
}
locked = current;
} else {
ScheduledFuture<?> sf = future.getAndSet(null);
if (sf != null) {
sf.cancel(true);
}
}
}
private synchronized void unlock() {
locked = null;
notify();
}
private String getHaltReason(Set<Breakpoint.Kind> kinds) {
if (kinds.size() > 1) {
return "ambiguous";
} else {
if (kinds.contains(Breakpoint.Kind.HALT_INSTRUCTION)) {
return "debugCommand";
} else if (kinds.contains(Breakpoint.Kind.EXCEPTION)) {
return "exception";
} else {
return "other";
}
}
}
private JSONObject getHaltData(SuspendedEvent se) {
DebugException exception = se.getException();
if (exception == null) {
return null;
}
boolean uncaught = exception.getCatchLocation() == null;
DebugValue exceptionObject = exception.getExceptionObject();
JSONObject data;
if (exceptionObject != null) {
RemoteObject remoteObject = context.createAndRegister(exceptionObject, false);
data = remoteObject.toJSON();
} else {
data = new JSONObject();
}
data.put("uncaught", uncaught);
return data;
}
private void dispose() {
unlock();
ScheduledFuture<?> sf = future.getAndSet(null);
if (sf != null) {
sf.cancel(true);
}
scheduler.shutdown();
try {
scheduler.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
}
}
private static class SchedulerThreadFactory implements ThreadFactory {
private final ThreadGroup group;
SchedulerThreadFactory() {
SecurityManager s = System.getSecurityManager();
this.group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, "Suspend Unlocking Scheduler");
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
private static JSONArray getFramesParam(CallFrame[] callFrames) {
JSONArray array = new JSONArray();
for (CallFrame cf : callFrames) {
array.put(cf.toJSON());
}
return array;
}
private JSONObject findAsyncStackTrace(List<List<DebugStackTraceElement>> asyncStacks) {
if (asyncStacks.isEmpty()) {
return null;
}
StackTrace stackTrace = new StackTrace(context, asyncStacks);
return stackTrace.toJSON();
}
private interface CommandLazyResponse {
Event getResponse(DebuggerSuspendedInfo suspendedInfo);
}
}