--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/org.chromium.debug.core/src/org/chromium/debug/core/model/DebugTargetImpl.java Wed Dec 23 17:13:18 2009 -0800
@@ -0,0 +1,657 @@
+// 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.debug.core.model;
+
+import java.util.Collection;
+
+import org.chromium.debug.core.ChromiumDebugPlugin;
+import org.chromium.debug.core.util.ChromiumDebugPluginUtil;
+import org.chromium.sdk.Breakpoint;
+import org.chromium.sdk.CallFrame;
+import org.chromium.sdk.DebugContext;
+import org.chromium.sdk.DebugEventListener;
+import org.chromium.sdk.ExceptionData;
+import org.chromium.sdk.JavascriptVm;
+import org.chromium.sdk.Script;
+import org.chromium.sdk.DebugContext.State;
+import org.chromium.sdk.DebugContext.StepAction;
+import org.chromium.sdk.JavascriptVm.BreakpointCallback;
+import org.chromium.sdk.JavascriptVm.ScriptsCallback;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.debug.core.DebugEvent;
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.IBreakpointManager;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchListener;
+import org.eclipse.debug.core.model.IBreakpoint;
+import org.eclipse.debug.core.model.IMemoryBlock;
+import org.eclipse.debug.core.model.IProcess;
+import org.eclipse.debug.core.model.ISourceLocator;
+import org.eclipse.debug.core.model.IStackFrame;
+import org.eclipse.debug.core.model.IThread;
+
+/**
+ * An IDebugTarget implementation for remote JavaScript debugging.
+ * Can debug any target that supports the ChromeDevTools protocol.
+ */
+public class DebugTargetImpl extends DebugElementImpl implements IChromiumDebugTarget {
+
+ private static final IThread[] EMPTY_THREADS = new IThread[0];
+
+ private static final long OPERATION_TIMEOUT_MS = 15000L;
+
+ private final ILaunch launch;
+
+ private final JavascriptThread[] threads;
+
+ private JavascriptVmEmbedder vmEmbedder = STUB_VM_EMBEDDER;
+
+ private ResourceManager resourceManager;
+
+ private BreakpointRegistry breakpointRegistry;
+
+ private IProject debugProject = null;
+
+ private DebugContext debugContext;
+
+ private boolean isSuspended = false;
+
+ private boolean isDisconnected = false;
+
+
+ public DebugTargetImpl(ILaunch launch) {
+ super(null);
+ this.launch = launch;
+ this.threads = new JavascriptThread[] { new JavascriptThread(this) };
+ }
+
+
+ /**
+ * Loads browser tabs, consults the {@code selector} which of the tabs to
+ * attach to, and if any has been selected, requests an attachment to the tab.
+ *
+ * @param projectNameBase to create for the browser scripts
+ * @param remoteServer embedding application we are connected with
+ * @param attachCallback to invoke on successful attachment
+ * @param monitor to report the progress to
+ * @return whether the target has attached to a tab
+ * @throws CoreException
+ */
+ public boolean attach(String projectNameBase,
+ JavascriptVmEmbedder.ConnectionToRemote remoteServer, DestructingGuard destructingGuard,
+ Runnable attachCallback, IProgressMonitor monitor) throws CoreException {
+ monitor.beginTask("", 2); //$NON-NLS-1$
+ JavascriptVmEmbedder.VmConnector connector = remoteServer.selectVm();
+ if (connector == null) {
+ return false;
+ }
+ monitor.worked(1);
+ return performAttach(projectNameBase, connector, destructingGuard, attachCallback);
+ }
+
+ private boolean performAttach(String projectNameBase, JavascriptVmEmbedder.VmConnector connector,
+ DestructingGuard destructingGuard, Runnable attachCallback) throws CoreException {
+ final JavascriptVmEmbedder embedder = connector.attach(embedderListener, debugEventListener);
+
+ Destructable embedderDestructor = new Destructable() {
+ public void destruct() {
+ embedder.getJavascriptVm().detach();
+ }
+ };
+
+ destructingGuard.addValue(embedderDestructor);
+
+ vmEmbedder = embedder;
+
+ // We might want to add some url-specific suffix here
+ String projectName = projectNameBase;
+ // We'd like to know when launch is removed to remove our project.
+ DebugPlugin.getDefault().getLaunchManager().addLaunchListener(launchListener);
+ this.debugProject = ChromiumDebugPluginUtil.createEmptyProject(projectName);
+ this.breakpointRegistry = new BreakpointRegistry();
+ this.resourceManager = new ResourceManager(debugProject, breakpointRegistry);
+ onAttach(projectName, attachCallback);
+ return true;
+ }
+
+ private void onAttach(String projectName, Runnable attachCallback) {
+ DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener(this);
+ reloadScriptsAndPossiblyResume(attachCallback);
+ }
+
+ private void reloadScriptsAndPossiblyResume(final Runnable attachCallback) {
+ reloadScripts(true, new Runnable() {
+ public void run() {
+ try {
+ if (attachCallback != null) {
+ attachCallback.run();
+ }
+ } finally {
+ fireCreationEvent();
+ }
+ Job job = new Job("Update debugger state") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ debugEventListener.resumedByDefault();
+ return Status.OK_STATUS;
+ }
+ };
+ job.schedule();
+ }
+ });
+ }
+
+ private void reloadScripts(boolean isSync, final Runnable runnable) {
+ Runnable command = new Runnable() {
+ public void run() {
+ vmEmbedder.getJavascriptVm().getScripts(new ScriptsCallback() {
+ public void failure(String errorMessage) {
+ ChromiumDebugPlugin.logError(errorMessage);
+ }
+
+ public void success(Collection<Script> scripts) {
+ if (!vmEmbedder.getJavascriptVm().isAttached()) {
+ return;
+ }
+ for (Script script : scripts) {
+ getResourceManager().addScript(script);
+ }
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+
+ });
+ }
+ };
+ if (isSync) {
+ command.run();
+ return;
+ }
+ Thread t = new Thread(command);
+ t.setDaemon(true);
+ t.start();
+ try {
+ t.join(OPERATION_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ ChromiumDebugPlugin.log(e);
+ }
+ }
+
+ public String getName() throws DebugException {
+ if (vmEmbedder == null) {
+ return ""; //$NON-NLS-1$
+ }
+ return vmEmbedder.getTargetName();
+ }
+
+ public IProcess getProcess() {
+ return null;
+ }
+
+ public JavascriptVmEmbedder getJavascriptEmbedder() {
+ return vmEmbedder;
+ }
+
+ public IThread[] getThreads() throws DebugException {
+ return isDisconnected()
+ ? EMPTY_THREADS
+ : threads;
+ }
+
+ public boolean hasThreads() throws DebugException {
+ return getThreads().length > 0;
+ }
+
+ public boolean supportsBreakpoint(IBreakpoint breakpoint) {
+ return ChromiumDebugPlugin.DEBUG_MODEL_ID.equals(breakpoint.getModelIdentifier()) &&
+ !isDisconnected();
+ }
+
+ @Override
+ public DebugTargetImpl getDebugTarget() {
+ return this;
+ }
+
+ @Override
+ public ILaunch getLaunch() {
+ return launch;
+ }
+
+ @Override
+ public String getModelIdentifier() {
+ return ChromiumDebugPlugin.DEBUG_MODEL_ID;
+ }
+
+ public boolean canTerminate() {
+ return !isTerminated();
+ }
+
+ public boolean isTerminated() {
+ return isDisconnected();
+ }
+
+ public void terminate() throws DebugException {
+ disconnect();
+ }
+
+ public boolean canResume() {
+ return !isDisconnected() && isSuspended();
+ }
+
+ public synchronized boolean isSuspended() {
+ return isSuspended;
+ }
+
+ private synchronized void setSuspended(boolean isSuspended) {
+ this.isSuspended = isSuspended;
+ }
+
+ public void suspended(int detail) {
+ setSuspended(true);
+ getThread().reset();
+ fireSuspendEvent(detail);
+ }
+
+ public void resume() throws DebugException {
+ debugContext.continueVm(StepAction.CONTINUE, 1, null);
+ // Let's pretend Chromium does respond to the "continue" request immediately
+ resumed(DebugEvent.CLIENT_REQUEST);
+ }
+
+ public void resumed(int detail) {
+ fireResumeEvent(detail);
+ }
+
+ public boolean canSuspend() {
+ return !isDisconnected() && !isSuspended();
+ }
+
+ public void suspend() throws DebugException {
+ vmEmbedder.getJavascriptVm().suspend(null);
+ }
+
+ public boolean canDisconnect() {
+ return !isDisconnected();
+ }
+
+ public void disconnect() throws DebugException {
+ if (!canDisconnect()) {
+ return;
+ }
+ removeAllBreakpoints();
+ if (!vmEmbedder.getJavascriptVm().detach()) {
+ ChromiumDebugPlugin.logWarning(Messages.DebugTargetImpl_BadResultWhileDisconnecting);
+ }
+ // This is a duplicated call to disconnected().
+ // The primary one comes from V8DebuggerToolHandler#onDebuggerDetached
+ // but we want to make sure the target becomes disconnected even if
+ // there is a browser failure and it does not respond.
+ debugEventListener.disconnected();
+ }
+
+ public synchronized boolean isDisconnected() {
+ return isDisconnected;
+ }
+
+ public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
+ return null;
+ }
+
+ public boolean supportsStorageRetrieval() {
+ return false;
+ }
+
+ public IProject getDebugProject() {
+ return debugProject;
+ }
+
+ /**
+ * Fires a debug event
+ *
+ * @param event to be fired
+ */
+ public void fireEvent(DebugEvent event) {
+ DebugPlugin debugPlugin = DebugPlugin.getDefault();
+ if (debugPlugin != null) {
+ debugPlugin.fireDebugEventSet(new DebugEvent[] { event });
+ }
+ }
+
+ public void fireEventForThread(int kind, int detail) {
+ try {
+ IThread[] threads = getThreads();
+ if (threads.length > 0) {
+ fireEvent(new DebugEvent(threads[0], kind, detail));
+ }
+ } catch (DebugException e) {
+ // Actually, this is not thrown in our getThreads()
+ return;
+ }
+ }
+
+ public void fireCreationEvent() {
+ setDisconnected(false);
+ fireEventForThread(DebugEvent.CREATE, DebugEvent.UNSPECIFIED);
+ }
+
+ public synchronized void setDisconnected(boolean disconnected) {
+ isDisconnected = disconnected;
+ }
+
+ public void fireResumeEvent(int detail) {
+ setSuspended(false);
+ fireEventForThread(DebugEvent.RESUME, detail);
+ fireEvent(new DebugEvent(this, DebugEvent.RESUME, detail));
+ }
+
+ public void fireSuspendEvent(int detail) {
+ setSuspended(true);
+ fireEventForThread(DebugEvent.SUSPEND, detail);
+ fireEvent(new DebugEvent(this, DebugEvent.SUSPEND, detail));
+ }
+
+ public void fireTerminateEvent() {
+ // TODO(peter.rybin): from Alexander Pavlov: I think you need to fire a terminate event after
+ // this line, for consolePseudoProcess if one is not null.
+ fireEventForThread(DebugEvent.TERMINATE, DebugEvent.UNSPECIFIED);
+ fireEvent(new DebugEvent(this, DebugEvent.TERMINATE, DebugEvent.UNSPECIFIED));
+ fireEvent(new DebugEvent(getLaunch(), DebugEvent.TERMINATE, DebugEvent.UNSPECIFIED));
+ }
+
+ public void breakpointAdded(IBreakpoint breakpoint) {
+ if (!supportsBreakpoint(breakpoint)) {
+ return;
+ }
+ try {
+ if (breakpoint.isEnabled()) {
+ // Class cast is ensured by the supportsBreakpoint implementation
+ final ChromiumLineBreakpoint lineBreakpoint = (ChromiumLineBreakpoint) breakpoint;
+ IFile file = (IFile) breakpoint.getMarker().getResource();
+ if (getResourceManager().isAddingFile(file)) {
+ return; // restoring breakpoints in progress
+ }
+ final Script script = getResourceManager().getScript(file);
+ if (script == null) {
+ // Might be a script from a different debug target
+ return;
+ }
+ final int line = (lineBreakpoint.getLineNumber() - 1) + script.getStartLine();
+ BreakpointCallback callback = new BreakpointCallback() {
+ public void success(Breakpoint breakpoint) {
+ lineBreakpoint.setBreakpoint(breakpoint);
+ breakpointRegistry.add(script, line, breakpoint);
+ }
+
+ public void failure(String errorMessage) {
+ ChromiumDebugPlugin.logError(errorMessage);
+ }
+ };
+ // ILineBreakpoint lines are 1-based while V8 lines are 0-based
+ JavascriptVm javascriptVm = vmEmbedder.getJavascriptVm();
+ if (script.getName() != null) {
+ javascriptVm.setBreakpoint(Breakpoint.Type.SCRIPT_NAME,
+ script.getName(),
+ line,
+ Breakpoint.EMPTY_VALUE,
+ breakpoint.isEnabled(),
+ lineBreakpoint.getCondition(),
+ lineBreakpoint.getIgnoreCount(),
+ callback);
+ } else {
+ javascriptVm.setBreakpoint(Breakpoint.Type.SCRIPT_ID,
+ String.valueOf(script.getId()),
+ line,
+ Breakpoint.EMPTY_VALUE,
+ breakpoint.isEnabled(),
+ lineBreakpoint.getCondition(),
+ lineBreakpoint.getIgnoreCount(),
+ callback);
+ }
+ }
+ } catch (CoreException e) {
+ ChromiumDebugPlugin.log(e);
+ }
+ }
+
+ public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
+ if (!supportsBreakpoint(breakpoint)) {
+ return;
+ }
+ // Class cast is ensured by the supportsBreakpoint implementation
+ ((ChromiumLineBreakpoint) breakpoint).changed();
+ }
+
+ public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
+ if (!supportsBreakpoint(breakpoint)) {
+ return;
+ }
+ try {
+ if (breakpoint.isEnabled()) {
+ // Class cast is ensured by the supportsBreakpoint implementation
+ ChromiumLineBreakpoint lineBreakpoint = (ChromiumLineBreakpoint) breakpoint;
+ lineBreakpoint.clear();
+ breakpointRegistry.remove(
+ getResourceManager().getScript((IFile) breakpoint.getMarker().getResource()),
+ lineBreakpoint.getLineNumber() - 1,
+ lineBreakpoint.getBrowserBreakpoint());
+ }
+ } catch (CoreException e) {
+ ChromiumDebugPlugin.log(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object getAdapter(Class adapter) {
+ if (ILaunch.class.equals(adapter)) {
+ return this.launch;
+ }
+ return super.getAdapter(adapter);
+ }
+
+ public IResourceManager getResourceManager() {
+ return resourceManager;
+ }
+
+ public JavascriptThread getThread() {
+ return isDisconnected()
+ ? null
+ : threads[0];
+ }
+
+ private static void breakpointsHit(Collection<? extends Breakpoint> breakpointsHit) {
+ if (breakpointsHit.isEmpty()) {
+ return;
+ }
+ IBreakpoint[] breakpoints =
+ DebugPlugin.getDefault().getBreakpointManager().getBreakpoints(
+ ChromiumDebugPlugin.DEBUG_MODEL_ID);
+ for (IBreakpoint breakpoint : breakpoints) {
+ ChromiumLineBreakpoint jsBreakpoint = (ChromiumLineBreakpoint) breakpoint;
+ if (breakpointsHit.contains(jsBreakpoint.getBrowserBreakpoint())) {
+ jsBreakpoint.setIgnoreCount(-1); // reset ignore count as we've hit it
+ }
+ }
+ }
+
+ private static String trim(String text, int maxLength) {
+ if (text == null || text.length() <= maxLength) {
+ return text;
+ }
+ return text.substring(0, maxLength - 3) + "..."; //$NON-NLS-1$
+ }
+
+ public DebugContext getDebugContext() {
+ return debugContext;
+ }
+
+ public ISourceLocator getSourceLocator() {
+ return sourceLocator;
+ }
+
+ private void removeAllBreakpoints() {
+ IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
+ IBreakpoint[] breakpoints =
+ breakpointManager.getBreakpoints(ChromiumDebugPlugin.DEBUG_MODEL_ID);
+ for (IBreakpoint bp : breakpoints) {
+ ChromiumLineBreakpoint clb = (ChromiumLineBreakpoint) bp;
+ if (clb.getBrowserBreakpoint() != null &&
+ clb.getBrowserBreakpoint().getId() != Breakpoint.INVALID_ID) {
+ clb.getBrowserBreakpoint().clear(null);
+ }
+ }
+ try {
+ breakpointManager.removeBreakpoints(breakpoints, true);
+ } catch (CoreException e) {
+ ChromiumDebugPlugin.log(e);
+ }
+ }
+
+ private final DebugEventListenerImpl debugEventListener = new DebugEventListenerImpl();
+
+ class DebugEventListenerImpl implements DebugEventListener {
+ // Synchronizes calls from ReaderThread of Connection and one call from some worker thread
+ private final Object suspendResumeMonitor = new Object();
+ private boolean alreadyResumedOrSuspended = false;
+
+ public void disconnected() {
+ if (!isDisconnected()) {
+ setDisconnected(true);
+ DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener(
+ DebugTargetImpl.this);
+ fireTerminateEvent();
+ }
+ }
+
+ public void resumedByDefault() {
+ synchronized (suspendResumeMonitor) {
+ if (!alreadyResumedOrSuspended) {
+ resumed();
+ }
+ }
+ }
+
+ public void resumed() {
+ synchronized (suspendResumeMonitor) {
+ DebugTargetImpl.this.resumed(DebugEvent.CLIENT_REQUEST);
+ alreadyResumedOrSuspended = true;
+ }
+ }
+
+ public void scriptLoaded(Script newScript) {
+ getResourceManager().addScript(newScript);
+ }
+
+ public void suspended(DebugContext context) {
+ synchronized (suspendResumeMonitor) {
+ DebugTargetImpl.this.debugContext = context;
+ breakpointsHit(context.getBreakpointsHit());
+ int suspendedDetail;
+ if (context.getState() == State.EXCEPTION) {
+ logExceptionFromContext(context);
+ suspendedDetail = DebugEvent.BREAKPOINT;
+ } else {
+ if (context.getBreakpointsHit().isEmpty()) {
+ suspendedDetail = DebugEvent.STEP_END;
+ } else {
+ suspendedDetail = DebugEvent.BREAKPOINT;
+ }
+ }
+ DebugTargetImpl.this.suspended(suspendedDetail);
+
+ alreadyResumedOrSuspended = true;
+ }
+ }
+ }
+
+ private void logExceptionFromContext(DebugContext context) {
+ ExceptionData exceptionData = context.getExceptionData();
+ CallFrame topFrame = context.getCallFrames().get(0);
+ Script script = topFrame.getScript();
+ ChromiumDebugPlugin.logError(
+ Messages.DebugTargetImpl_LogExceptionFormat,
+ exceptionData.isUncaught()
+ ? Messages.DebugTargetImpl_Uncaught
+ : Messages.DebugTargetImpl_Caught,
+ exceptionData.getExceptionMessage(),
+ script != null ? script.getName() : "<unknown>", //$NON-NLS-1$
+ topFrame.getLineNumber(),
+ trim(exceptionData.getSourceText(), 80));
+ }
+
+ private final JavascriptVmEmbedder.Listener embedderListener =
+ new JavascriptVmEmbedder.Listener() {
+ public void reset() {
+ getResourceManager().clear();
+ fireEvent(new DebugEvent(this, DebugEvent.CHANGE, DebugEvent.STATE));
+ }
+ public void closed() {
+ debugEventListener.disconnected();
+ }
+ };
+
+ private final ILaunchListener launchListener = new ILaunchListener() {
+ public void launchAdded(ILaunch launch) {
+ }
+ public void launchChanged(ILaunch launch) {
+ }
+ // TODO(peter.rybin): maybe have one instance of listener for all targets?
+ public void launchRemoved(ILaunch launch) {
+ if (launch != DebugTargetImpl.this.launch) {
+ return;
+ }
+ DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this);
+ if (debugProject != null) {
+ ChromiumDebugPluginUtil.deleteVirtualProjectAsync(debugProject);
+ }
+ }
+ };
+
+ private final static JavascriptVmEmbedder STUB_VM_EMBEDDER = new JavascriptVmEmbedder() {
+ public JavascriptVm getJavascriptVm() {
+ //TODO(peter.rybin): decide and redo this exception
+ throw new UnsupportedOperationException();
+ }
+
+ public String getTargetName() {
+ //TODO(peter.rybin): decide and redo this exception
+ throw new UnsupportedOperationException();
+ }
+
+ public String getThreadName() {
+ //TODO(peter.rybin): decide and redo this exception
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ /**
+ * This very simple source locator works because we provide our own source files.
+ * We'll have to try harder, once we link with resource js files.
+ */
+ private final ISourceLocator sourceLocator = new ISourceLocator() {
+ public Object getSourceElement(IStackFrame stackFrame) {
+ if (stackFrame instanceof StackFrame == false) {
+ return null;
+ }
+ StackFrame jsStackFrame = (StackFrame) stackFrame;
+
+ Script script = jsStackFrame.getCallFrame().getScript();
+ if (script == null) {
+ return null;
+ }
+
+ return resourceManager.getResource(script);
+ }
+ };
+}