// 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugEventListener;
import org.chromium.sdk.InvalidContextException;
import org.chromium.sdk.Script;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.Version;
import org.chromium.sdk.JavascriptVm.ScriptsCallback;
import org.chromium.sdk.JavascriptVm.SuspendCallback;
import org.chromium.sdk.internal.InternalContext.ContextDismissedCheckedException;
import org.chromium.sdk.internal.protocol.CommandResponse;
import org.chromium.sdk.internal.protocol.SuccessCommandResponse;
import org.chromium.sdk.internal.tools.v8.BreakpointManager;
import org.chromium.sdk.internal.tools.v8.DefaultResponseHandler;
import org.chromium.sdk.internal.tools.v8.V8BlockingCallback;
import org.chromium.sdk.internal.tools.v8.V8CommandCallbackBase;
import org.chromium.sdk.internal.tools.v8.V8CommandOutput;
import org.chromium.sdk.internal.tools.v8.V8CommandProcessor;
import org.chromium.sdk.internal.tools.v8.V8Helper;
import org.chromium.sdk.internal.tools.v8.V8ProtocolUtil;
import org.chromium.sdk.internal.tools.v8.request.ContextlessDebuggerMessage;
import org.chromium.sdk.internal.tools.v8.request.DebuggerMessageFactory;
/**
* A class that holds and administers main parts of debug protocol implementation.
*/
public class DebugSession {
/** The script manager for the associated tab. */
private final ScriptManager scriptManager;
private final V8CommandProcessor v8CommandProcessor;
/** A helper for performing complex V8-related actions. */
private final V8Helper v8Helper = new V8Helper(this);
private final ContextBuilder contextBuilder;
/** Our manager. */
private DebugSessionManager sessionManager;
/** Context owns breakpoint manager. */
private final BreakpointManager breakpointManager;
private final ScriptLoader scriptLoader = new ScriptLoader();
private final DefaultResponseHandler defaultResponseHandler;
public DebugSession(DebugSessionManager sessionManager, V8ContextFilter contextFilter,
V8CommandOutput v8CommandOutput) {
this.scriptManager = new ScriptManager(contextFilter, this);
this.sessionManager = sessionManager;
this.breakpointManager = new BreakpointManager(this);
this.defaultResponseHandler = new DefaultResponseHandler(this);
this.v8CommandProcessor = new V8CommandProcessor(v8CommandOutput, defaultResponseHandler);
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.
*/
public void sendMessageAsync(ContextlessDebuggerMessage message, boolean isImmediate,
V8CommandProcessor.V8HandlerCallback commandCallback, SyncCallback syncCallback) {
v8CommandProcessor.sendV8CommandAsync(message, isImmediate,
commandCallback, syncCallback);
}
/**
* Gets invoked when a navigation event is reported by the browser tab.
*/
public void navigated() {
getScriptManager().reset();
}
/**
* @return the DebugEventListener associated with this context
*/
public DebugEventListener getDebugEventListener() {
return getSessionManager().getDebugEventListener();
}
public BreakpointManager getBreakpointManager() {
return breakpointManager;
}
public ScriptLoader getScriptLoader() {
return scriptLoader;
}
public V8Helper getV8Helper() {
return v8Helper;
}
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);
}
public class ScriptLoader {
private final Object monitor = new Object();
/**
* Stores the callbacks that are waiting for result.
* This field being reset to null means that result is ready (loaded into ScriptManager)
* and no more callbacks are accepted.
*/
private List<ScriptsCallback> pendingCallbacks = new ArrayList<ScriptsCallback>(2);
private List<SyncCallback> pendingSyncCallbacks = new ArrayList<SyncCallback>(2);
/**
* Loads all scripts from the remote if necessary, and feeds them into the
* callback provided (if any).
*/
public void loadAllScripts(ScriptsCallback callback, SyncCallback syncCallback) {
boolean resultIsReady;
boolean sendMessage;
synchronized (monitor) {
if (pendingCallbacks == null) {
resultIsReady = true;
sendMessage = false;
} else {
resultIsReady = false;
sendMessage = pendingCallbacks.isEmpty();
pendingCallbacks.add(callback);
pendingSyncCallbacks.add(syncCallback);
}
}
if (resultIsReady) {
try {
if (callback != null) {
callback.success(getScriptManager().allScripts());
}
} finally {
if (syncCallback != null) {
syncCallback.callbackDone(null);
}
}
return;
}
if (sendMessage) {
sendAsyncMessage();
}
}
private void sendAsyncMessage() {
V8Helper.ScriptLoadCallback groupCallback = new V8Helper.ScriptLoadCallback() {
public void success() {
final Collection<Script> scripts = scriptManager.allScripts();
processCall(new CallbackCaller() {
@Override
void call(ScriptsCallback callback) {
callback.success(scripts);
}
});
}
public void failure(final String message) {
processCall(new CallbackCaller() {
@Override
void call(ScriptsCallback callback) {
callback.failure(message);
}
});
}
private void processCall(CallbackCaller caller) {
List<ScriptsCallback> savedCallbacks;
synchronized (monitor) {
savedCallbacks = pendingCallbacks;
pendingCallbacks = null;
}
for (ScriptsCallback callback : savedCallbacks) {
if (callback != null) {
caller.call(callback);
}
}
}
abstract class CallbackCaller {
abstract void call(ScriptsCallback callback);
}
};
SyncCallback groupSyncCallback = new SyncCallback() {
public void callbackDone(RuntimeException e) {
List<SyncCallback> savedCallbacks;
synchronized (monitor) {
savedCallbacks = pendingSyncCallbacks;
pendingSyncCallbacks = null;
}
for (SyncCallback callback : savedCallbacks) {
if (callback != null) {
callback.callbackDone(e);
}
}
}
};
V8Helper.reloadAllScriptsAsync(DebugSession.this, groupCallback, groupSyncCallback);
}
}
/**
* Checks version of V8 and check if it in running state.
*/
public void startCommunication() {
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);
if (V8VersionMilestones.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 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);
}
}
}