// 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.CallFrame;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.ExceptionData;
import org.chromium.sdk.JsEvaluateContext;
import org.chromium.sdk.Script;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.internal.protocol.SuccessCommandResponse;
import org.chromium.sdk.internal.tools.v8.V8CommandCallbackBase;
import org.chromium.sdk.internal.tools.v8.V8CommandProcessor;
import org.chromium.sdk.internal.tools.v8.V8CommandProcessor.V8HandlerCallback;
import org.chromium.sdk.internal.tools.v8.request.DebuggerMessage;
import org.chromium.sdk.internal.tools.v8.request.DebuggerMessageFactory;
public class ContextBuilder {
private final DebugSession debugSession;
/**
* Context building process comes though sequence of steps. No one should
* be able to work with any step exception the current one.
*/
private Object currentStep = null;
ContextBuilder(DebugSession debugSession) {
this.debugSession = debugSession;
}
public interface ExpectingBreakEventStep {
InternalContext getInternalContext();
/**
* Stores the breakpoints associated with V8 suspension event (empty if an
* exception or a step end).
*
* @param breakpointsHit the breakpoints that were hit
*/
ExpectingBacktraceStep setContextState(Collection<Breakpoint> breakpointsHit,
ExceptionData exceptionData);
}
public interface ExpectingBacktraceStep {
InternalContext getInternalContext();
DebugContext setFrames(FrameMirror[] frameMirrors);
}
/**
* Starting point of building new DebugContext process. One should traverse
* list of steps to get the result.
* @return object representing first step of context building process
*/
public ExpectingBreakEventStep buildNewContext() {
assertStep(null);
final PreContext preContext = new PreContext();
final DebugContextData contextData = new DebugContextData();
return new ExpectingBreakEventStep() {
{
currentStep = this;
}
public InternalContext getInternalContext() {
return preContext;
}
public ExpectingBacktraceStep setContextState(Collection<Breakpoint> breakpointsHit,
ExceptionData exceptionData) {
assertStep(this);
DebugContext.State state;
if (exceptionData == null) {
state = DebugContext.State.NORMAL;
} else {
state = DebugContext.State.EXCEPTION;
}
contextData.contextState = state;
contextData.breakpointsHit = Collections.unmodifiableCollection(breakpointsHit);
contextData.exceptionData = exceptionData;
return new ExpectingBacktraceStep() {
{
currentStep = this;
}
public InternalContext getInternalContext() {
return preContext;
}
public DebugContext setFrames(FrameMirror[] frameMirrors) {
assertStep(this);
contextData.frames = new Frames(frameMirrors, preContext);
preContext.createContext(contextData);
DebugContext userContext = preContext.getContext();
currentStep = userContext;
return userContext;
}
};
}
};
}
public ExpectingBreakEventStep buildNewContextWhenIdle() {
if (currentStep == null) {
return buildNewContext();
} else {
return null;
}
}
/**
* Debug session is stopped. Cancel context in any state.
*/
public void forceCancelContext() {
// TODO(peter.rybin): complete it
}
public void buildSequenceFailure() {
// this means we can't go on debugging
// TODO(peter.rybin): implement
throw new RuntimeException();
}
private void contextDismissed(DebugContext userContext) {
assertStep(userContext);
currentStep = null;
}
private void assertStep(Object step) {
if (currentStep != step) {
throw new IllegalStateException("Expected " + step + ", but was " + currentStep);
}
}
private class PreContext implements InternalContext {
private final HandleManager handleManager = new HandleManager();
private final ValueLoader valueLoader = new ValueLoader(this);
/**
* We synchronize {@link #isValid} state with commands that are being sent
* using this monitor.
*/
private final Object sendContextCommandsMonitor = new Object();
private volatile boolean isValid = true;
private UserContext context = null;
public boolean isValid() {
return isValid;
}
public DebugSession getDebugSession() {
return debugSession;
}
public ContextBuilder getContextBuilder() {
return ContextBuilder.this;
}
void assertValid() {
if (!isValid) {
throw new IllegalStateException("This instance of DebugContext cannot be used anymore");
}
}
/**
* Check if context is valid right now. Throws exception if we are in a strict mode.
* Ignores otherwise.
*/
void assertValidForUser() {
if (!isValid) {
debugSession.maybeRethrowContextException(null);
}
}
public UserContext getContext() {
if (context == null) {
throw new IllegalStateException();
}
return context;
}
public CallFrameImpl getTopFrameImpl() {
assertValid();
return getContext().data.frames.getCallFrames().get(0);
}
public HandleManager getHandleManager() {
// tolerates dismissed context
return handleManager;
}
public ValueLoader getValueLoader() {
return valueLoader;
}
void createContext(DebugContextData contextData) {
if (context != null) {
throw new IllegalStateException();
}
context = new UserContext(contextData);
}
public void sendV8CommandAsync(DebuggerMessage message, boolean isImmediate,
V8HandlerCallback commandCallback, SyncCallback syncCallback)
throws ContextDismissedCheckedException {
synchronized (sendContextCommandsMonitor) {
if (!isValid) {
throw new ContextDismissedCheckedException();
}
debugSession.getV8CommandProcessor().sendV8CommandAsync(message, isImmediate,
commandCallback, syncCallback);
}
}
private void sendMessageAsyncAndIvalidate(DebuggerMessage message,
V8CommandProcessor.V8HandlerCallback commandCallback, boolean isImmediate,
SyncCallback syncCallback) {
synchronized (sendContextCommandsMonitor) {
assertValid();
debugSession.getV8CommandProcessor().sendV8CommandAsync(message, isImmediate,
commandCallback, syncCallback);
isValid = false;
}
}
private boolean sendNoMessageAndInvalidate() {
synchronized (sendContextCommandsMonitor) {
if (!isValid) {
return false;
}
isValid = false;
return true;
}
}
private class UserContext implements DebugContext {
private final DebugContextData data;
public UserContext(DebugContextData contextData) {
this.data = contextData;
}
public State getState() {
assertValidForUser();
return data.contextState;
}
public List<? extends CallFrame> getCallFrames() {
assertValidForUser();
return data.frames.getCallFrames();
}
public Collection<Breakpoint> getBreakpointsHit() {
assertValidForUser();
if (data.breakpointsHit == null) {
throw new RuntimeException();
}
return data.breakpointsHit;
}
public ExceptionData getExceptionData() {
assertValidForUser();
return data.exceptionData;
}
Collection<Breakpoint> getBreakpointsHitSafe() {
return data.breakpointsHit;
}
ExceptionData getExceptionDataSafe() {
return data.exceptionData;
}
public JsEvaluateContext getGlobalEvaluateContext() {
return evaluateContext;
}
/**
* @throws IllegalStateException if context has already been continued
*/
public void continueVm(StepAction stepAction, int stepCount,
final ContinueCallback callback) {
if (stepAction == null) {
throw new NullPointerException();
}
DebuggerMessage message = DebuggerMessageFactory.goOn(stepAction, stepCount);
V8CommandProcessor.V8HandlerCallback commandCallback
= new V8CommandCallbackBase() {
@Override
public void success(SuccessCommandResponse successResponse) {
contextDismissed(UserContext.this);
if (callback != null) {
callback.success();
}
getDebugSession().getDebugEventListener().resumed();
}
@Override
public void failure(String message) {
synchronized (sendContextCommandsMonitor) {
// resurrected
isValid = true;
}
if (callback != null) {
callback.failure(message);
}
}
};
sendMessageAsyncAndIvalidate(message, commandCallback, true, null);
}
/**
* Behaves as continue but does not send any messages to remote VM.
* This brings ContextBuilder to state when we are about to build a new context.
*/
boolean continueLocally() {
if (!sendNoMessageAndInvalidate()) {
return false;
}
contextDismissed(UserContext.this);
getDebugSession().getDebugEventListener().resumed();
return true;
}
InternalContext getInternalContextForTests() {
return PreContext.this;
}
private final JsEvaluateContext evaluateContext = new JsEvaluateContextImpl() {
@Override
protected Integer getFrameIdentifier() {
return null;
}
@Override
public InternalContext getInternalContext() {
return PreContext.this;
}
};
}
}
/**
* Simple structure of data which DebugConext implementation uses.
*/
private static class DebugContextData {
private Frames frames;
/** The breakpoints hit before suspending. */
private volatile Collection<Breakpoint> breakpointsHit;
DebugContext.State contextState;
/** The JavaScript exception state. */
private ExceptionData exceptionData;
}
private class Frames {
/** The frame mirrors while on a breakpoint. */
private final FrameMirror[] frameMirrors;
/** The cached call frames constructed using frameMirrors. */
private final List<CallFrameImpl> unmodifableFrames;
private boolean scriptsLinkedToFrames;
Frames(FrameMirror[] frameMirrors0, InternalContext internalContext) {
this.frameMirrors = frameMirrors0;
this.scriptsLinkedToFrames = false;
int frameCount = frameMirrors.length;
List<CallFrameImpl> frameList = new ArrayList<CallFrameImpl>(frameCount);
for (int i = 0; i < frameCount; ++i) {
frameList.add(new CallFrameImpl(frameMirrors[i], i, internalContext));
}
this.unmodifableFrames = Collections.unmodifiableList(frameList);
}
synchronized List<CallFrameImpl> getCallFrames() {
if (!scriptsLinkedToFrames) {
// We expect that ALL the V8 scripts are loaded so we can
// hook them up to the call frames.
int frameCount = frameMirrors.length;
for (int i = 0; i < frameCount; ++i) {
hookupScriptToFrame(i);
}
scriptsLinkedToFrames = true;
}
return unmodifableFrames;
}
/**
* Associates a script found in the ScriptManager with the given frame.
*
* @param frameIndex to associate a script with
*/
private void hookupScriptToFrame(int frameIndex) {
FrameMirror frame = frameMirrors[frameIndex];
if (frame != null && frame.getScript() == null) {
Script script = debugSession.getScriptManager().findById(frame.getScriptId());
if (script != null) {
frame.setScript(script);
}
}
}
}
/**
* Drops the current context and initiates reading data and building a new one.
* Must be called from Dispatch thread.
* @return ExpectingBacktraceStep or null if there is no active context currently
*/
ExpectingBacktraceStep startRebuildCurrentContext() {
// We can use currentStep as long as we are being operated from Dispatch thread.
if (currentStep instanceof PreContext.UserContext == false) {
return null;
}
PreContext.UserContext userContext = (PreContext.UserContext) currentStep;
if (!userContext.continueLocally()) {
return null;
}
return buildNewContext().setContextState(userContext.getBreakpointsHitSafe(),
userContext.getExceptionDataSafe());
}
static InternalContext getInternalContextForTests(DebugContext debugContext) {
PreContext.UserContext userContext = (PreContext.UserContext) debugContext;
return userContext.getInternalContextForTests();
}
}