// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.sdk.internal.v8native; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; import org.chromium.sdk.Breakpoint; import org.chromium.sdk.DebugContext; import org.chromium.sdk.DebugEventListener; import org.chromium.sdk.InvalidContextException; import org.chromium.sdk.JavascriptVm; import org.chromium.sdk.JavascriptVm.ScriptsCallback; import org.chromium.sdk.JavascriptVm.SuspendCallback; import org.chromium.sdk.RelayOk; import org.chromium.sdk.SyncCallback; import org.chromium.sdk.Version; import org.chromium.sdk.internal.v8native.InternalContext.ContextDismissedCheckedException; import org.chromium.sdk.internal.v8native.protocol.V8ProtocolUtil; import org.chromium.sdk.internal.v8native.protocol.input.CommandResponse; import org.chromium.sdk.internal.v8native.protocol.input.SuccessCommandResponse; import org.chromium.sdk.internal.v8native.protocol.output.ContextlessDebuggerMessage; import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessageFactory; import org.chromium.sdk.util.AsyncFuture; import org.chromium.sdk.util.AsyncFuture.Callback; import org.chromium.sdk.util.AsyncFutureRef; import org.chromium.sdk.util.MethodIsBlockingException; import org.chromium.sdk.util.RelaySyncCallback; /** * A class that holds and administers main parts of debug protocol implementation. */ public class DebugSession { private static final Logger LOGGER = Logger.getLogger(DebugSession.class.getName()); /** The script manager for the associated tab. */ private final ScriptManager scriptManager; private final V8CommandProcessor v8CommandProcessor; private final ContextBuilder contextBuilder; /** Our manager. */ private DebugSessionManager sessionManager; /** Context owns breakpoint manager. */ private final BreakpointManager breakpointManager; private final ScriptManagerProxy scriptManagerProxy = new ScriptManagerProxy(this); private final DefaultResponseHandler defaultResponseHandler; private final JavascriptVm javascriptVm; private volatile Version vmVersion = null; public DebugSession(DebugSessionManager sessionManager, V8ContextFilter contextFilter, V8CommandOutput v8CommandOutput, JavascriptVm javascriptVm) { this.scriptManager = new ScriptManager(contextFilter, this); this.sessionManager = sessionManager; this.javascriptVm = javascriptVm; this.breakpointManager = new BreakpointManager(this); this.defaultResponseHandler = new DefaultResponseHandler(this); this.v8CommandProcessor = new V8CommandProcessor(v8CommandOutput, defaultResponseHandler, this); this.contextBuilder = new ContextBuilder(this); } public ScriptManager getScriptManager() { return scriptManager; } public V8CommandProcessor getV8CommandProcessor() { return v8CommandProcessor; } public DebugSessionManager getSessionManager() { return sessionManager; } public void onDebuggerDetached() { getSessionManager().onDebuggerDetached(); getScriptManager().reset(); contextBuilder.forceCancelContext(); } /** * Sends V8 command messages, but only those which doesn't depend on context. * Use {@code InternalContext} if you need to send context-specific commands. * @return */ public RelayOk sendMessageAsync(ContextlessDebuggerMessage message, boolean isImmediate, V8CommandProcessor.V8HandlerCallback commandCallback, SyncCallback syncCallback) { return v8CommandProcessor.sendV8CommandAsync(message, isImmediate, commandCallback, syncCallback); } JavascriptVm getJavascriptVm() { return javascriptVm; } public Version getVmVersion() { return vmVersion; } /** * @return the DebugEventListener associated with this context */ public DebugEventListener getDebugEventListener() { return getSessionManager().getDebugEventListener(); } public BreakpointManager getBreakpointManager() { return breakpointManager; } public ScriptManagerProxy getScriptManagerProxy() { return scriptManagerProxy; } public ContextBuilder getContextBuilder() { return contextBuilder; } /** * Drops current context and creates a new one. This is useful if context is known to have changed * (e.g. experimental feature LiveEdit may change current stack while execution is suspended). * The method is asynchronous and returns immediately. * Does nothing if currently there is no active context. Otherwise dismisses current context, * invokes {@link DebugEventListener#resumed()} and initiates downloading stack frame descriptions * and building new context. When the context is built, * calls {@link DebugEventListener#suspended(DebugContext)}. * <p> * Must be called from Dispatch Thread. * @return true if context has been actually dropped. */ public boolean recreateCurrentContext() { ContextBuilder.ExpectingBacktraceStep step = contextBuilder.startRebuildCurrentContext(); if (step == null) { return false; } defaultResponseHandler.getBreakpointProcessor().processNextStep(step); return true; } public void suspend(final SuspendCallback suspendCallback) { V8CommandProcessor.V8HandlerCallback v8Callback = new V8CommandCallbackBase() { @Override public void failure(String message) { if (suspendCallback != null) { suspendCallback.failure(new Exception(message)); } } @Override public void success(SuccessCommandResponse successResponse) { if (suspendCallback != null) { suspendCallback.success(); } ContextBuilder.ExpectingBreakEventStep step1 = contextBuilder.buildNewContextWhenIdle(); if (step1 == null) { return; } ContextBuilder.ExpectingBacktraceStep step2 = step1.setContextState(Collections.<Breakpoint>emptyList(), null); defaultResponseHandler.getBreakpointProcessor().processNextStep(step2); } }; sendMessageAsync(DebuggerMessageFactory.suspend(), true, v8Callback, null); } /** * A proxy to script manager that makes sure that all scripts have been pre-loaded from remote. * This is done only once per debug session. * TODO: consider loading all scripts synchronously on session start. */ public static class ScriptManagerProxy { private final DebugSession debugSession; private final AsyncFutureRef<Void> scriptsLoadedFuture = new AsyncFutureRef<Void>(); ScriptManagerProxy(DebugSession debugSession) { this.debugSession = debugSession; } public RelayOk getAllScripts(final ScriptsCallback callback, SyncCallback syncCallback) { if (!scriptsLoadedFuture.isInitialized()) { scriptsLoadedFuture.initializeRunning(new ScriptsRequester()); } // Operation is multi-step, so make sure that syncCallback won't be left uncalled. RelaySyncCallback relay = new RelaySyncCallback(syncCallback); final RelaySyncCallback.Guard guard = relay.newGuard(); Callback<Void> futureCallback = new Callback<Void>() { @Override public void done(Void res) { if (callback != null) { RelayOk relayOk = getAllScriptsAsync(callback, guard.getRelay()); guard.discharge(relayOk); } } }; return scriptsLoadedFuture.getAsync(futureCallback, guard.asSyncCallback()); } private RelayOk getAllScriptsAsync(final ScriptsCallback callback, RelaySyncCallback relay) { // We should call the callback from Dispatch thread (so that the whole collection // kept fresh during the call-back). return debugSession.getV8CommandProcessor().runInDispatchThread( new Runnable() { @Override public void run() { callback.success(debugSession.getScriptManager().allScripts()); } }, relay.getUserSyncCallback()); } private class ScriptsRequester implements AsyncFuture.Operation<Void> { @Override public RelayOk start(final Callback<Void> requestCallback, SyncCallback syncCallback) { V8Helper.ScriptLoadCallback scriptLoadCallback = new V8Helper.ScriptLoadCallback() { @Override public void success() { requestCallback.done(null); } @Override public void failure(final String message) { LOGGER.log(Level.SEVERE, null, new Exception("Failed to load scripts from remote: " + message)); requestCallback.done(null); } }; return V8Helper.reloadAllScriptsAsync(debugSession, scriptLoadCallback, syncCallback); } } } /** * Checks version of V8 and check if it in running state. */ public void startCommunication() throws MethodIsBlockingException { V8BlockingCallback<Void> callback = new V8BlockingCallback<Void>() { @Override public Void messageReceived(CommandResponse response) { SuccessCommandResponse successResponse = response.asSuccess(); if (successResponse == null) { return null; } Version vmVersion = V8ProtocolUtil.parseVersionResponse(successResponse); DebugSession.this.vmVersion = vmVersion; if (V8VersionFeatures.isRunningAccurate(vmVersion)) { Boolean running = successResponse.running(); if (running == Boolean.FALSE) { ContextBuilder.ExpectingBreakEventStep step1 = contextBuilder.buildNewContextWhenIdle(); // If step is not null -- we are already in process of building a context. if (step1 != null) { ContextBuilder.ExpectingBacktraceStep step2 = step1.setContextState(Collections.<Breakpoint>emptyList(), null); defaultResponseHandler.getBreakpointProcessor().processNextStep(step2); } } } return null; } @Override protected Void handleSuccessfulResponse(SuccessCommandResponse response) { throw new UnsupportedOperationException(); } }; V8Helper.callV8Sync(this.v8CommandProcessor, DebuggerMessageFactory.version(), callback); } public RelayOk sendLoopbackMessage(Runnable callback, SyncCallback syncCallback) { return this.v8CommandProcessor.runInDispatchThread(callback, syncCallback); } public void maybeRethrowContextException(ContextDismissedCheckedException e) { // TODO(peter.rybin): make some kind of option out of this final boolean strictPolicy = true; if (strictPolicy) { throw new InvalidContextException(e); } } }