org.chromium.debug.core/src/org/chromium/debug/core/model/DebugTargetImpl.java
author TasneemS@US-TASNEEMS
Wed, 23 Dec 2009 17:13:18 -0800
changeset 2 e4420d2515f1
child 52 f577ea64429e
permissions -rw-r--r--
Initial version of WRT Debugger.

// 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);
    }
  };
}