/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.debug.core.dartium;
import com.google.dart.tools.core.NotYetImplementedException;
import com.google.dart.tools.debug.core.DartDebugCorePlugin;
import com.google.dart.tools.debug.core.DartDebugCorePlugin.BreakOnExceptions;
import com.google.dart.tools.debug.core.DartLaunchConfigWrapper;
import com.google.dart.tools.debug.core.DebugUIHelper;
import com.google.dart.tools.debug.core.breakpoints.DartBreakpoint;
import com.google.dart.tools.debug.core.source.UriToFileResolver;
import com.google.dart.tools.debug.core.util.IResourceResolver;
import com.google.dart.tools.debug.core.webkit.WebkitBreakpoint;
import com.google.dart.tools.debug.core.webkit.WebkitCallFrame;
import com.google.dart.tools.debug.core.webkit.WebkitCallback;
import com.google.dart.tools.debug.core.webkit.WebkitConnection;
import com.google.dart.tools.debug.core.webkit.WebkitConnection.WebkitConnectionListener;
import com.google.dart.tools.debug.core.webkit.WebkitDebugger.DebuggerListenerAdapter;
import com.google.dart.tools.debug.core.webkit.WebkitDebugger.PauseOnExceptionsType;
import com.google.dart.tools.debug.core.webkit.WebkitDebugger.PausedReasonType;
import com.google.dart.tools.debug.core.webkit.WebkitDom.DomListener;
import com.google.dart.tools.debug.core.webkit.WebkitDom.InspectorListener;
import com.google.dart.tools.debug.core.webkit.WebkitLocation;
import com.google.dart.tools.debug.core.webkit.WebkitPage;
import com.google.dart.tools.debug.core.webkit.WebkitRemoteObject;
import com.google.dart.tools.debug.core.webkit.WebkitResult;
import com.google.dart.tools.debug.core.webkit.WebkitScript;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.IBreakpointManagerListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IThread;
import java.io.IOException;
import java.util.List;
/**
* The IDebugTarget implementation for the Dartium debug elements.
*/
public class DartiumDebugTarget extends DartiumDebugElement implements IDebugTarget,
IBreakpointManagerListener {
private static DartiumDebugTarget activeTarget;
public static DartiumDebugTarget getActiveTarget() {
return activeTarget;
}
private static void setActiveTarget(DartiumDebugTarget target) {
activeTarget = target;
}
private String debugTargetName;
private String disconnectMessage;
private WebkitConnection connection;
private ILaunch launch;
private DartiumProcess process;
private IResourceResolver resourceResolver;
private boolean enableBreakpoints;
private DartiumDebugThread debugThread;
private DartBreakpointManager breakpointManager;
private CssScriptManager cssScriptManager;
private HtmlScriptManager htmlScriptManager;
private DartCodeManager dartCodeManager;
private boolean canSetScriptSource;
private SourceMapManager sourceMapManager;
private UriToFileResolver uriToFileResolver;
/**
* A copy constructor for DartiumDebugTarget.
*
* @param target
*/
public DartiumDebugTarget(DartiumDebugTarget target) {
this(
target.debugTargetName,
new WebkitConnection(target.connection),
target.launch,
null,
target.resourceResolver,
target.enableBreakpoints,
false);
this.process = target.process;
this.process.switchTo(this);
}
/**
* @param target
*/
public DartiumDebugTarget(String debugTargetName, WebkitConnection connection, ILaunch launch,
Process javaProcess, IResourceResolver resourceResolver, boolean enableBreakpoints,
boolean isRemote) {
super(null);
setActiveTarget(this);
this.debugTargetName = debugTargetName;
this.connection = connection;
this.launch = launch;
this.resourceResolver = resourceResolver;
this.enableBreakpoints = enableBreakpoints;
debugThread = new DartiumDebugThread(this);
if (javaProcess != null || isRemote) {
process = new DartiumProcess(this, debugTargetName, javaProcess);
}
if (enableBreakpoints) {
breakpointManager = new BreakpointManager(this);
} else {
breakpointManager = new BreakpointManager.NullBreakpointManager();
}
cssScriptManager = new CssScriptManager(this);
if (DartDebugCorePlugin.SEND_MODIFIED_HTML) {
htmlScriptManager = new HtmlScriptManager(this);
}
if (DartDebugCorePlugin.SEND_MODIFIED_DART) {
dartCodeManager = new DartCodeManager(this);
}
DartLaunchConfigWrapper wrapper = new DartLaunchConfigWrapper(launch.getLaunchConfiguration());
if (wrapper.getProject() != null) {
sourceMapManager = new SourceMapManager(wrapper.getProject());
}
uriToFileResolver = new UriToFileResolver(launch);
}
@Override
public void breakpointAdded(IBreakpoint breakpoint) {
throw new NotYetImplementedException();
}
@Override
public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
throw new NotYetImplementedException();
}
@Override
public void breakpointManagerEnablementChanged(boolean enabled) {
try {
getConnection().getDebugger().setBreakpointsActive(enableBreakpoints && enabled);
} catch (IOException e) {
DartDebugCorePlugin.logError(e);
}
}
@Override
public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
throw new NotYetImplementedException();
}
@Override
public boolean canDisconnect() {
return false;
}
@Override
public boolean canResume() {
return debugThread == null ? false : debugThread.canResume();
}
@Override
public boolean canSuspend() {
return debugThread == null ? false : debugThread.canSuspend();
}
@Override
public boolean canTerminate() {
return connection.isConnected();
}
@Override
public void disconnect() throws DebugException {
throw new UnsupportedOperationException("disconnect is not supported");
}
@Override
public void fireTerminateEvent() {
setActiveTarget(null);
breakpointManager.dispose(false);
cssScriptManager.dispose();
if (htmlScriptManager != null) {
htmlScriptManager.dispose();
}
if (dartCodeManager != null) {
dartCodeManager.dispose();
}
if (sourceMapManager != null) {
sourceMapManager.dispose();
}
debugThread = null;
// Check for null on system shutdown.
if (DebugPlugin.getDefault() != null) {
super.fireTerminateEvent();
}
}
/**
* @return the connection
*/
@Override
public WebkitConnection getConnection() {
return connection;
}
@Override
public IDebugTarget getDebugTarget() {
return this;
}
public boolean getEnableBreakpoints() {
return enableBreakpoints;
}
@Override
public ILaunch getLaunch() {
return launch;
}
@Override
public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
return null;
}
@Override
public String getName() {
if (disconnectMessage != null) {
return debugTargetName + " <" + disconnectMessage + ">";
} else {
return debugTargetName;
}
}
@Override
public IProcess getProcess() {
return process;
}
@Override
public IThread[] getThreads() throws DebugException {
if (debugThread != null) {
return new IThread[] {debugThread};
} else {
return new IThread[0];
}
}
public UriToFileResolver getUriToFileResolver() {
return uriToFileResolver;
}
@Override
public boolean hasThreads() throws DebugException {
return true;
}
@Override
public boolean isDisconnected() {
return false;
}
@Override
public boolean isSuspended() {
return debugThread == null ? false : debugThread.isSuspended();
}
@Override
public boolean isTerminated() {
return debugThread == null;
}
/**
* Recycle the current Dartium debug connection; attempt to reset it to a fresh state beforehand.
*
* @param url
* @throws IOException
*/
public void navigateToUrl(ILaunchConfiguration launchConfig, final String url,
boolean enableBreakpoints, IResourceResolver resolver) throws IOException {
this.resourceResolver = resolver;
IBreakpointManager eclipseBpManager = DebugPlugin.getDefault().getBreakpointManager();
connection.getDebugger().setBreakpointsActive(enableBreakpoints && eclipseBpManager.isEnabled());
connection.getDebugger().setPauseOnExceptions(getPauseType());
getConnection().getPage().navigate(url);
}
public void openConnection() throws IOException {
openConnection(null, false);
}
public void openConnection(final String url, boolean listenForInspectorDetach) throws IOException {
connection.addConnectionListener(new WebkitConnectionListener() {
@Override
public void connectionClosed(WebkitConnection connection) {
fireTerminateEvent();
}
});
connection.connect();
process.getStreamMonitor().connectTo(connection);
connection.getPage().addPageListener(new WebkitPage.PageListenerAdapter() {
@Override
public void loadEventFired(int timestamp) {
if (htmlScriptManager != null) {
htmlScriptManager.handleLoadEventFired();
}
}
});
connection.getPage().enable();
connection.getCSS().enable();
if (DartDebugCorePlugin.SEND_MODIFIED_HTML) {
connection.getDom().addDomListener(new DomListener() {
@Override
public void documentUpdated() {
if (htmlScriptManager != null) {
htmlScriptManager.handleDocumentUpdated();
}
}
});
}
if (listenForInspectorDetach) {
connection.getDom().addInspectorListener(new InspectorListener() {
@Override
public void detached(String reason) {
handleInspectorDetached(reason);
}
@Override
public void targetCrashed() {
handleTargetCrashed();
}
});
}
connection.getDebugger().addDebuggerListener(new DebuggerListenerAdapter() {
@Override
public void debuggerBreakpointResolved(WebkitBreakpoint breakpoint) {
breakpointManager.handleBreakpointResolved(breakpoint);
}
@Override
public void debuggerGlobalObjectCleared() {
breakpointManager.handleGlobalObjectCleared();
}
@Override
public void debuggerPaused(PausedReasonType reason, List<WebkitCallFrame> frames,
WebkitRemoteObject exception) {
if (exception != null && !DartDebugCorePlugin.getPlugin().getBreakOnJSException()
&& isJavaScriptException(frames, exception)) {
try {
// Continue VM execution.
getConnection().getDebugger().resume();
} catch (IOException e) {
}
} else {
if (exception != null) {
printExceptionToStdout(exception);
}
debugThread.handleDebuggerSuspended(reason, frames, exception);
}
}
@Override
public void debuggerResumed() {
debugThread.handleDebuggerResumed();
}
@Override
public void debuggerScriptParsed(WebkitScript script) {
checkForDebuggerExtension(script);
}
});
connection.getDebugger().enable();
IBreakpointManager eclipseBpManager = DebugPlugin.getDefault().getBreakpointManager();
eclipseBpManager.addBreakpointManagerListener(this);
getConnection().getDebugger().setBreakpointsActive(
enableBreakpoints && eclipseBpManager.isEnabled());
connection.getDebugger().canSetScriptSource(new WebkitCallback<Boolean>() {
@Override
public void handleResult(WebkitResult<Boolean> result) {
if (!result.isError() && result.getResult() != null) {
canSetScriptSource = result.getResult().booleanValue();
}
}
});
fireCreationEvent();
process.fireCreationEvent();
// Set our existing breakpoints and start listening for new breakpoints.
breakpointManager.connect();
// TODO(devoncarew): listen for changes to DartDebugCorePlugin.PREFS_BREAK_ON_EXCEPTIONS
if (url == null) {
connection.getDebugger().setPauseOnExceptions(getPauseType());
} else {
connection.getDebugger().setPauseOnExceptions(
getPauseType(),
createNavigateWebkitCallback(url));
}
}
/**
* Attempt to re-connect to a debug target. If successful, it will return a new
* DartiumDebugTarget.
*
* @return
* @throws IOException
*/
public DartiumDebugTarget reconnect() throws IOException {
DartiumDebugTarget newTarget = new DartiumDebugTarget(this);
newTarget.reopenConnection();
ILaunch launch = newTarget.getLaunch();
launch.addDebugTarget(newTarget);
for (IDebugTarget target : launch.getDebugTargets()) {
if (target.isTerminated()) {
launch.removeDebugTarget(target);
}
}
return newTarget;
}
public void reopenConnection() throws IOException {
openConnection(null, true);
}
@Override
public void resume() throws DebugException {
debugThread.resume();
}
@Override
public boolean supportsBreakpoint(IBreakpoint breakpoint) {
if (!(breakpoint instanceof DartBreakpoint)) {
return false;
}
return true;
}
public boolean supportsSetScriptSource() {
return canSetScriptSource;
}
@Override
public boolean supportsStorageRetrieval() {
return false;
}
@Override
public void suspend() throws DebugException {
debugThread.suspend();
}
@Override
public void terminate() throws DebugException {
try {
connection.close();
} catch (IOException e) {
}
process.terminate();
}
public void writeToStdout(String message) {
process.getStreamMonitor().messageAdded(message);
}
protected WebkitCallback<Boolean> createNavigateWebkitCallback(final String url) {
return new WebkitCallback<Boolean>() {
@Override
public void handleResult(WebkitResult<Boolean> result) {
// Once all other requests have been processed, then navigate to the given url.
try {
if (connection.isConnected()) {
connection.getPage().navigate(url);
}
} catch (IOException e) {
DartDebugCorePlugin.logError(e);
}
}
};
}
protected DartBreakpointManager getBreakpointManager() {
return breakpointManager;
}
protected IResourceResolver getResourceResolver() {
return resourceResolver;
}
protected SourceMapManager getSourceMapManager() {
return sourceMapManager;
}
protected WebkitConnection getWebkitConnection() {
return connection;
}
protected void handleInspectorDetached(String reason) {
// "replaced_with_devtools", "target_closed", ...
final String replacedWithDevTools = "replaced_with_devtools";
if (replacedWithDevTools.equalsIgnoreCase(reason)) {
// When the user opens the Webkit inspector our debug connection is closed.
// We warn the user when this happens, since it otherwise isn't apparent to them
// when the debugger connection is closing.
if (enableBreakpoints) {
// Only show this message if the user launched Dartium with debugging enabled.
disconnectMessage = "devtools disconnect";
DebugUIHelper.getHelper().handleDevtoolsDisconnect(this);
}
}
}
protected void handleTargetCrashed() {
process.getStreamMonitor().messageAdded("<debug target crashed>");
try {
terminate();
} catch (DebugException e) {
DartDebugCorePlugin.logInfo(e);
}
}
protected boolean isJavaScriptException(List<WebkitCallFrame> frames, WebkitRemoteObject exception) {
if (frames.size() == 0) {
return false;
}
WebkitLocation location = frames.get(0).getLocation();
WebkitScript script = getConnection().getDebugger().getScript(location.getScriptId());
if (script == null) {
return false;
}
String url = script.getUrl();
if (url.endsWith(".dart.js") || url.endsWith(".precompiled.js")) {
return false;
}
return url.endsWith(".js");
}
protected void printExceptionToStdout(final WebkitRemoteObject exception) {
try {
getConnection().getRuntime().callToString(
exception.getObjectId(),
new WebkitCallback<String>() {
@Override
public void handleResult(WebkitResult<String> result) {
if (!result.isError()) {
String text = result.getResult();
if (exception != null && exception.isPrimitive()) {
text = exception.getValue();
}
if (text != null) {
int index = text.indexOf('\n');
if (index != -1) {
text = text.substring(0, index).trim();
}
process.getStreamMonitor().messageAdded("Breaking on exception: " + text + "\n");
}
}
}
});
} catch (IOException e) {
DartDebugCorePlugin.logError(e);
}
}
protected boolean shouldUseSourceMapping() {
return DartDebugCorePlugin.getPlugin().getUseSourceMaps();
}
/**
* Check for the presence of Chrome extensions content scripts. It seems like many (all?) of these
* prevent debugging from working.
*
* @param script the Debugger.scriptParsed event
* @see dartbug.com/10298
*/
private void checkForDebuggerExtension(WebkitScript script) {
// {"method":"Debugger.scriptParsed","params":{"startLine":0,"libraryId":0,"endLine":154,
// "startColumn":0,"scriptId":"26","url":"chrome-extension://ognampngfcbddbfemdapefohjiobgbdl/data_loader.js",
// "isContentScript":true,"endColumn":1}}
if (script.isContentScript() && script.isChromeExtensionUrl()) {
DartDebugCorePlugin.logWarning("Chrome extension content script detected: " + script);
writeToStdout("WARNING: Chrome content script extension detected. Many of these extensions "
+ "interfere with the debug\nexperience, including preventing breakpoints from working. "
+ "These extensions include but are not limited\nto SpeedTracer and the WebGL inspector. "
+ "You can disable them in Dartium via Tools > Extensions.");
writeToStdout("(content script extension: " + script.getUrl() + ")");
}
}
private PauseOnExceptionsType getPauseType() {
if (!enableBreakpoints) {
return PauseOnExceptionsType.none;
}
final BreakOnExceptions boe = DartDebugCorePlugin.getPlugin().getBreakOnExceptions();
PauseOnExceptionsType pauseType = PauseOnExceptionsType.none;
if (boe == BreakOnExceptions.uncaught) {
pauseType = PauseOnExceptionsType.uncaught;
} else if (boe == BreakOnExceptions.all) {
pauseType = PauseOnExceptionsType.all;
}
return pauseType;
}
}