org.chromium.debug.core/src/org/chromium/debug/core/model/DebugTargetImpl.java
changeset 2 e4420d2515f1
child 52 f577ea64429e
--- /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);
+    }
+  };
+}