blob: ae9553e6b2b78ab066d36d23189b80b23aa66ba0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.javascript.v8debug;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.CheckReturnValue;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerInfo;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.io.IOProvider;
import org.netbeans.api.io.InputOutput;
import org.netbeans.api.io.OutputColor;
import org.netbeans.lib.v8debug.PropertyBoolean;
import org.netbeans.lib.v8debug.V8Arguments;
import org.netbeans.lib.v8debug.V8Breakpoint;
import org.netbeans.lib.v8debug.V8Command;
import org.netbeans.lib.v8debug.V8Event;
import org.netbeans.lib.v8debug.V8Request;
import org.netbeans.lib.v8debug.V8Response;
import org.netbeans.lib.v8debug.V8Script;
import org.netbeans.lib.v8debug.connection.ClientConnection;
import org.netbeans.lib.v8debug.connection.IOListener;
import org.netbeans.lib.v8debug.commands.Backtrace;
import org.netbeans.lib.v8debug.commands.ClearBreakpoint;
import org.netbeans.lib.v8debug.commands.Frame;
import org.netbeans.lib.v8debug.commands.Scripts;
import org.netbeans.lib.v8debug.commands.SetBreakpoint;
import org.netbeans.lib.v8debug.events.AfterCompileEventBody;
import org.netbeans.lib.v8debug.events.BreakEventBody;
import org.netbeans.lib.v8debug.events.CompileErrorEventBody;
import org.netbeans.lib.v8debug.events.ExceptionEventBody;
import org.netbeans.lib.v8debug.events.ScriptCollectedEventBody;
import org.netbeans.modules.javascript.v8debug.api.Connector;
import org.netbeans.modules.javascript.v8debug.breakpoints.BreakpointsHandler;
import org.netbeans.modules.javascript.v8debug.frames.CallFrame;
import org.netbeans.modules.javascript.v8debug.frames.CallStack;
import org.netbeans.modules.javascript.v8debug.sources.ChangeLiveSupport;
import org.netbeans.modules.javascript.v8debug.vars.VarValuesLoader;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;
/**
*
* @author Martin Entlicher
*/
public final class V8Debugger {
private static final Logger LOG = Logger.getLogger(V8Debugger.class.getName());
private static final boolean SHOW_V8_PROTOCOL = Boolean.getBoolean("show.v8.protocol");
private final String host;
private final int port;
private final ClientConnection connection;
private final ScriptsHandler scriptsHandler;
private final ChangeLiveSupport changeLiveSupport;
private final BreakpointsHandler breakpointsHandler;
@NullAllowed
private final Runnable finishCallback;
private DebuggerEngine engine;
private DebuggerEngine.Destructor engineDestructor;
private final RequestProcessor rp = new RequestProcessor(V8Debugger.class);
private final Set<Listener> listeners = new CopyOnWriteArraySet<>();
private final AtomicLong requestSequence = new AtomicLong(1l);
private final Map<Long, Pair<V8Request, CommandResponseCallback>> commandCallbacks = new HashMap<>();
private boolean suspended = false;
private final Object suspendedUpdateLock = new Object();
private volatile boolean finished = false;
private final ReentrantReadWriteLock accessLock = new ReentrantReadWriteLock(true);
private CallFrame currentFrame;
private CallStack currentCallStack;
private final Object currentFrameRetrieveLock = new Object();
private final Object currentCallStackRetrieveLock = new Object();
private long runToBreakpointId = -1;
private CommandResponseCallback runToCallBack;
private final ErrorMessageHandler errMsgHandler;
public static DebuggerEngine startSession(Connector.Properties properties,
@NullAllowed Runnable finishCallback) throws IOException {
V8Debugger dbg = new V8Debugger(properties, finishCallback);
VarValuesLoader vvl = new VarValuesLoader(dbg);
DebuggerInfo dInfo = DebuggerInfo.create(V8DebuggerSessionProvider.DEBUG_INFO, new Object[]{ dbg, vvl });
DebuggerEngine[] engines = DebuggerManager.getDebuggerManager().startDebugging(dInfo);
if (engines.length > 0) {
dbg.setEngine(engines[0]);
return engines[0];
} else {
if (finishCallback != null) {
finishCallback.run();
}
return null;
}
}
private V8Debugger(Connector.Properties properties, Runnable finishCallback) throws IOException {
this.host = properties.getHostName();
this.port = properties.getPort();
this.connection = new ClientConnection(properties.getHostName(), properties.getPort());
if (SHOW_V8_PROTOCOL) {
connection.addIOListener(new CommListener());
}
this.scriptsHandler = new ScriptsHandler(properties.getLocalPaths(),
properties.getServerPaths(),
properties.getLocalPathExclusionFilters(),
this);
this.changeLiveSupport = new ChangeLiveSupport(this);
this.breakpointsHandler = new BreakpointsHandler(this);
this.finishCallback = finishCallback;
errMsgHandler = Lookup.getDefault().lookup(ErrorMessageHandler.class);
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public void start() {
spawnResponseLoop();
initScripts();
}
public void finish() {
if (finished) {
return ;
}
finished = true;
try {
connection.close();
} catch (IOException ioex) {}
notifyFinished();
finishCallbacks();
engineDestructor.killEngine();
if (finishCallback != null) {
finishCallback.run();
}
}
public boolean isFinished() {
return finished;
}
public ScriptsHandler getScriptsHandler() {
return scriptsHandler;
}
public ChangeLiveSupport getChangeLiveSupport() {
return changeLiveSupport;
}
public BreakpointsHandler getBreakpointsHandler() {
return breakpointsHandler;
}
@CheckForNull
public V8Request sendCommandRequest(V8Command cmd, V8Arguments args) {
return sendCommandRequest(cmd, args, null);
}
@CheckForNull
@CheckReturnValue
public V8Request sendCommandRequest(V8Command cmd, V8Arguments args, CommandResponseCallback callback) {
V8Request request = new V8Request(requestSequence.getAndIncrement(), cmd, args);
if (callback != null) {
synchronized (commandCallbacks) {
commandCallbacks.put(request.getSequence(), Pair.of(request, callback));
}
}
if (cmd.equals(V8Command.Continue) || cmd.equals(V8Command.Restartframe)) {
// Going to resume...
notifySuspended(false);
}
try {
connection.send(request);
return request;
} catch (IOException ex) {
return null;
}
}
public void suspend() {
sendCommandRequest(V8Command.Suspend, null);
}
public void resume() {
sendCommandRequest(V8Command.Continue, null);
}
public boolean isSuspended() {
assert !accessLock.isWriteLockedByCurrentThread();
synchronized (suspendedUpdateLock) {
return suspended;
}
}
@CheckForNull
public CallFrame getCurrentFrame() {
CallFrame f;
Lock rl = accessLock.readLock();
rl.lock();
try {
f = currentFrame;
//LOG.fine("getCurrentFrame(): currentFrame = "+f+", currentCallStack = "+currentCallStack);
if (f == null && currentCallStack != null) {
rl.unlock();
rl = null;
synchronized (currentFrameRetrieveLock) {
f = currentCallStack.getTopFrame();
if (f != null) {
Lock wl = accessLock.writeLock();
wl.lock();
try {
currentFrame = f;
} finally {
wl.unlock();
}
}
}
if (f != null) {
fireCurrentFrame(f);
return f;
}
}
//LOG.fine(" getCurrentFrame(): f = "+f+", isSuspended() = "+isSuspended());
if (f == null && isSuspended()) {
final CallFrame[] fRef = new CallFrame[] { null };
if (rl != null) {
rl.unlock();
rl = null;
}
// Synchronize not to start retrieving the current frame multiple times at once
synchronized (currentFrameRetrieveLock) {
rl = accessLock.readLock();
rl.lock();
try {
// Things might have changed:
if (currentFrame != null || !isSuspended()) {
return currentFrame;
}
} finally {
rl.unlock();
rl = null;
}
//if (currentFrame != null)
V8Request request = sendCommandRequest(V8Command.Frame, null, new CommandResponseCallback() {
@Override
public void notifyResponse(V8Request request, V8Response response) {
if (response != null && response.isSuccess()) {
Frame.ResponseBody frb = (Frame.ResponseBody) response.getBody();
fRef[0] = new CallFrame(V8Debugger.this, frb.getFrame(), new ReferencedValues(response), true);
}
synchronized (currentFrameRetrieveLock) {
currentFrameRetrieveLock.notifyAll();
}
}
});
if (request != null) {
try {
currentFrameRetrieveLock.wait();
} catch (InterruptedException iex) {}
}
f = fRef[0];
Lock wl = accessLock.writeLock();
wl.lock();
try {
currentFrame = f;
} finally {
wl.unlock();
}
}
fireCurrentFrame(f);
}
} finally {
if (rl != null) {
rl.unlock();
}
}
//LOG.fine("getCurrentFrame(): return "+f);
return f;
}
public void setCurrentFrame(@NonNull CallFrame callFrame) {
assert callFrame != null: "Only existing frames can be set";
Lock wl = accessLock.writeLock();
wl.lock();
try {
this.currentFrame = callFrame;
} finally {
wl.unlock();
}
fireCurrentFrame(callFrame);
}
private void fireCurrentFrame(CallFrame cf) {
for (Listener l : listeners) {
l.notifyCurrentFrame(cf);
}
}
public CallStack getCurrentCallStack() {
CallStack cs;
Lock rl = accessLock.readLock();
rl.lock();
try {
cs = currentCallStack;
//LOG.fine("getCurrentCallStack(): currentCallStack = "+cs+", isSuspended() = "+isSuspended());
if (cs == null && isSuspended()) {
final CallStack[] csRef = new CallStack[] { null };
rl.unlock();
rl = null;
// Synchronize not to start retrieving the current frame multiple times at once
synchronized (currentCallStackRetrieveLock) {
rl = accessLock.readLock();
rl.lock();
try {
// Things might have changed:
if (currentCallStack != null && suspended) {
//LOG.fine("getCurrentCallStack(): currentCallStack computed in the mean time: "+currentCallStack);
return currentCallStack;
}
} finally {
rl.unlock();
rl = null;
}
//LOG.fine("getCurrentCallStack(): retrieving the number of frames...");
// Find the number of frames first:
Backtrace.Arguments bta = new Backtrace.Arguments(0l, 1l, false, true);
final Object csRetrievingLock = new Object();
synchronized (csRetrievingLock) {
V8Request request = sendCommandRequest(V8Command.Backtrace, bta, new CommandResponseCallback() {
@Override
public void notifyResponse(V8Request request, V8Response response) {
if (response == null) {
synchronized (csRetrievingLock) {
csRetrievingLock.notifyAll();
}
return ;
}
Backtrace.ResponseBody btrb = (Backtrace.ResponseBody) response.getBody();
long numFrames = btrb.getTotalFrames();
//LOG.fine("getCurrentCallStack(): "+numFrames+" frames retrieved.");
if (numFrames == 0) {
synchronized (csRetrievingLock) {
csRef[0] = CallStack.EMPTY;
csRetrievingLock.notifyAll();
}
}
if (numFrames == 1l) {
synchronized (csRetrievingLock) {
csRef[0] = new CallStack(V8Debugger.this, btrb.getFrames(), response.getReferencedValues());
csRetrievingLock.notifyAll();
}
} else {
//LOG.fine("getCurrentCallStack(): retrieving "+numFrames+" frames...");
// Find numFrames frames now:
Backtrace.Arguments bta = new Backtrace.Arguments(0l, numFrames, false, true);
V8Request request2 = sendCommandRequest(V8Command.Backtrace, bta, new CommandResponseCallback() {
@Override
public void notifyResponse(V8Request request, V8Response response) {
if (response != null) {
Backtrace.ResponseBody btrb = (Backtrace.ResponseBody) response.getBody();
csRef[0] = new CallStack(V8Debugger.this, btrb.getFrames(), response.getReferencedValues());
//LOG.fine("getCurrentCallStack(): All frames retrieved: "+csRef[0]);
}
synchronized (csRetrievingLock) {
csRetrievingLock.notifyAll();
}
}
});
if (request2 == null) {
// Failed, notify
synchronized (csRetrievingLock) {
csRetrievingLock.notifyAll();
}
}
}
}
});
if (request != null) {
try {
csRetrievingLock.wait();
} catch (InterruptedException iex) {}
}
}
cs = csRef[0];
Lock wl = accessLock.writeLock();
wl.lock();
try {
currentCallStack = cs;
//LOG.fine("getCurrentCallStack(): set currentCallStack to "+cs);
} finally {
wl.unlock();
}
}
}
} finally {
if (rl != null) {
rl.unlock();
}
}
//LOG.fine("getCurrentCallStack(): return "+cs);
return cs;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
private void spawnResponseLoop() {
new RequestProcessor("V8Debugger Response Loop").post(new Runnable() {
@Override
public void run() {
responseLoop();
}
});
}
private void responseLoop() {
try {
connection.runEventLoop(new ClientConnection.Listener() {
@Override
public void header(Map<String, String> properties) {
for (Map.Entry pe : properties.entrySet()) {
LOG.fine(" "+pe.getKey() + " = " + pe.getValue());
}
}
@Override
public void response(V8Response response) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("response: "+response+", success = "+response.isSuccess()+", running = "+response.isRunning()+", body = "+response.getBody());
}
handleResponse(response);
}
@Override
public void event(V8Event event) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("event: "+event+", name = "+event.getKind());
}
handleEvent(event);
}
});
} catch (IOException ioex) {
LOG.log(Level.FINE, null, ioex);
Throwable cause = ioex.getCause();
if (cause != null) {
Exceptions.printStackTrace(cause);
}
} finally {
try {
connection.close();
} catch (IOException ioex) {
} finally {
finish();
}
}
}
private void handleResponse(V8Response response) {
long requestSequenceNum = response.getRequestSequence();
synchronized (commandCallbacks) {
Pair<V8Request, CommandResponseCallback> callback;
synchronized (commandCallbacks) {
callback = commandCallbacks.remove(requestSequenceNum);
}
if (callback != null) {
callback.second().notifyResponse(callback.first(), response);
}
}
String errorMessage = response.getErrorMessage();
if (errorMessage != null) {
if (errMsgHandler != null) {
errMsgHandler.errorResponse(errorMessage);
}
return ;
}
if (!response.isSuccess()) {
return ;
}
notifySuspended(!response.isRunning());
switch (response.getCommand()) {
}
}
private void handleEvent(V8Event event) {
PropertyBoolean running = event.isRunning();
V8Event.Kind eventKind = event.getKind();
if (running.hasValue()) {
notifySuspended(!event.isRunning().getValue());
} else if (eventKind == V8Event.Kind.Break ||
eventKind == V8Event.Kind.Exception) {
notifySuspended(true);
}
PropertyBoolean success = event.getSuccess();
if (success.hasValue() && !success.getValue() && event.getErrorMessage() != null) {
// an error is reported
if (errMsgHandler != null) {
errMsgHandler.errorEvent(event.getErrorMessage());
}
return ;
}
switch (eventKind) {
case AfterCompile:
AfterCompileEventBody aceb = (AfterCompileEventBody) event.getBody();
V8Script script = aceb.getScript();
scriptsHandler.add(script);
break;
case CompileError:
CompileErrorEventBody ceeb = (CompileErrorEventBody) event.getBody();
script = ceeb.getScript();
scriptsHandler.add(script);
break;
case ScriptCollected:
ScriptCollectedEventBody sceb = (ScriptCollectedEventBody) event.getBody();
long scriptId = sceb.getScriptId();
scriptsHandler.remove(scriptId);
break;
case Break:
BreakEventBody beb = (BreakEventBody) event.getBody();
long[] breakpoints = beb.getBreakpoints();
if (breakpoints != null && breakpoints.length > 0) {
if (runToBreakpointId > 0) {
for (long b : breakpoints) {
if (b == runToBreakpointId) {
clearRunTo();
if (breakpoints.length == 1) {
return ;
}
}
}
}
breakpointsHandler.event(beb);
}
//System.out.println("stopped at "+beb.getScript().getName()+", line = "+(beb.getSourceLine()+1)+" : "+beb.getSourceColumn()+"\ntext = "+beb.getSourceLineText());
break;
case Exception:
ExceptionEventBody eeb = (ExceptionEventBody) event.getBody();
//System.out.println("exception '"+eeb.getException()+"' stopped in "+eeb.getScript().getName()+", line = "+(eeb.getSourceLine()+1)+" : "+eeb.getSourceColumn()+"\ntext = "+eeb.getSourceLineText());
break;
default:
LOG.info("Unknown event: "+event.getKind());
}
}
private void notifySuspended(boolean suspended) {
assert !accessLock.isWriteLockedByCurrentThread();
LOG.fine("notifySuspended("+suspended+")");
boolean fireNullCF = false;
synchronized (suspendedUpdateLock) {
if (this.suspended == suspended) {
return ;
}
Lock wl = accessLock.writeLock();
wl.lock();
try {
if (!suspended) {
// Resumed
fireNullCF = currentFrame != null;
currentFrame = null;
currentCallStack = null;
}
this.suspended = suspended;
LOG.fine("notifySuspended(): suspended property is set to "+suspended);
} finally {
wl.unlock();
}
}
if (fireNullCF) {
for (Listener l : listeners) {
l.notifyCurrentFrame(null);
}
}
for (Listener l : listeners) {
l.notifySuspended(suspended);
}
}
private void notifyFinished() {
for (Listener l : listeners) {
l.notifyFinished();
}
}
private void finishCallbacks() {
List<Pair<V8Request, CommandResponseCallback>> callBacks = new ArrayList<>();
synchronized (commandCallbacks) {
callBacks.addAll(commandCallbacks.values());
}
for (Pair<V8Request, CommandResponseCallback> cb : callBacks) {
cb.second().notifyResponse(cb.first(), null);
}
}
private void setEngine(DebuggerEngine debuggerEngine) {
this.engine = debuggerEngine;
}
void setEngineDestructor(DebuggerEngine.Destructor destructor) {
this.engineDestructor = destructor;
}
private void initScripts() {
Scripts.Arguments sa = new Scripts.Arguments(null, null, false, null);
sendCommandRequest(V8Command.Scripts, sa, new CommandResponseCallback() {
@Override
public void notifyResponse(V8Request request, V8Response response) {
if (response != null) {
Scripts.ResponseBody srb = (Scripts.ResponseBody) response.getBody();
V8Script[] scripts = srb.getScripts();
scriptsHandler.add(scripts);
}
}
});
}
private void clearRunTo() {
if (runToBreakpointId > 0) {
ClearBreakpoint.Arguments cbargs = new ClearBreakpoint.Arguments(runToBreakpointId);
V8Request request = sendCommandRequest(V8Command.Clearbreakpoint, cbargs);
runToBreakpointId = -1;
}
}
public void runTo(FileObject fo, long line) {
clearRunTo();
if (fo == null) {
return ;
}
String serverPath = scriptsHandler.getServerPath(fo);
if (serverPath == null) {
return ;
}
SetBreakpoint.Arguments args = new SetBreakpoint.Arguments(
V8Breakpoint.Type.scriptName,
serverPath,
line, null, true,
null, null, null);
if (runToCallBack == null) {
runToCallBack = new RunToResponseCallback();
}
V8Request request = sendCommandRequest(V8Command.Setbreakpoint, args,
runToCallBack);
}
private final class RunToResponseCallback implements CommandResponseCallback {
@Override
public void notifyResponse(V8Request request, V8Response response) {
if (response != null) {
SetBreakpoint.ResponseBody sbrb = (SetBreakpoint.ResponseBody) response.getBody();
long id = sbrb.getBreakpoint();
runToBreakpointId = id;
resume();
}
}
}
@NbBundle.Messages("V8DebugProtocolPane=V8 Debug Protocol")
private final class CommListener implements IOListener {
private final OutputColor sentColor = OutputColor.rgb(0, 178, 0); //Color.GREEN.darker();
private final OutputColor receivedColor = OutputColor.rgb(0, 0, 255); //Color.BLUE;
private final InputOutput ioLogger = IOProvider.getDefault().getIO(Bundle.V8DebugProtocolPane(), false);
private final long startTime = System.currentTimeMillis();
@Override
public synchronized void sent(String str) {
long time = System.currentTimeMillis() - startTime;
ioLogger.getOut().append("Sent at "+(time/1000.0)+":");
ioLogger.getOut().print(str, sentColor);
ioLogger.getOut().println();
}
@Override
public synchronized void received(String str) {
long time = System.currentTimeMillis() - startTime;
ioLogger.getOut().append("Got at "+(time/1000.0)+":");
ioLogger.getOut().print(str, receivedColor);
ioLogger.getOut().println();
}
@Override
public void closed() {
ioLogger.getOut().close();
}
}
public interface Listener {
void notifySuspended(boolean suspended);
void notifyCurrentFrame(@NullAllowed CallFrame cf);
void notifyFinished();
}
public interface CommandResponseCallback {
/**
* Command response result.
* @param request The original request
* @param response The response, or <code>null</code> when the command was canceled (e.g. debugger killed)
*/
void notifyResponse(V8Request request, @NullAllowed V8Response response);
}
/**
* Register an implementation of this interface into the default lookup
* to handle the debugger protocol errors.
*/
public static interface ErrorMessageHandler {
void errorResponse(String error);
void errorEvent(String error);
}
}