package com.jetbrains.lang.dart.ide.runner.server.vmService;
import com.google.common.base.Charsets;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScopesCore;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.PathUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.HashSet;
import com.intellij.xdebugger.*;
import com.intellij.xdebugger.breakpoints.XBreakpointHandler;
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import com.intellij.xdebugger.frame.XStackFrame;
import com.intellij.xdebugger.frame.XSuspendContext;
import com.jetbrains.lang.dart.DartBundle;
import com.jetbrains.lang.dart.DartFileType;
import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService;
import com.jetbrains.lang.dart.ide.runner.DartConsoleFilter;
import com.jetbrains.lang.dart.ide.runner.actions.DartPopFrameAction;
import com.jetbrains.lang.dart.ide.runner.base.DartDebuggerEditorsProvider;
import com.jetbrains.lang.dart.ide.runner.server.OpenDartObservatoryUrlAction;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartVmServiceStackFrame;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartVmServiceSuspendContext;
import com.jetbrains.lang.dart.util.DartResolveUtil;
import com.jetbrains.lang.dart.util.DartUrlResolver;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TIntObjectHashMap;
import org.dartlang.vm.service.VmService;
import org.dartlang.vm.service.element.*;
import org.dartlang.vm.service.logging.Logging;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class DartVmServiceDebugProcess extends XDebugProcess {
private static final Logger LOG = Logger.getInstance(DartVmServiceDebugProcess.class.getName());
@Nullable private final ExecutionResult myExecutionResult;
@NotNull private final DartUrlResolver myDartUrlResolver;
@NotNull private final String myDebuggingHost;
private final int myObservatoryPort;
private boolean myVmConnected = false;
@NotNull private final XBreakpointHandler[] myBreakpointHandlers;
private final IsolatesInfo myIsolatesInfo;
private VmServiceWrapper myVmServiceWrapper;
@NotNull private final Set<String> mySuspendedIsolateIds = Collections.synchronizedSet(new THashSet<String>());
private String myLatestCurrentIsolateId;
private final Map<String, LightVirtualFile> myScriptIdToContentMap = new THashMap<>();
private final Map<String, TIntObjectHashMap<Pair<Integer, Integer>>> myScriptIdToLinesAndColumnsMap =
new THashMap<>();
@Nullable private final String myDASExecutionContextId;
private final boolean myRemoteDebug;
private final int myTimeout;
@Nullable private final VirtualFile myCurrentWorkingDirectory;
@Nullable protected String myRemoteProjectRootUri;
@NotNull private final OpenDartObservatoryUrlAction myOpenObservatoryAction =
new OpenDartObservatoryUrlAction(null, () -> myVmConnected && !getSession().isStopped());
public DartVmServiceDebugProcess(@NotNull final XDebugSession session,
@NotNull final String debuggingHost,
final int observatoryPort,
@Nullable final ExecutionResult executionResult,
@NotNull final DartUrlResolver dartUrlResolver,
@Nullable final String dasExecutionContextId,
final boolean remoteDebug,
final int timeout,
@Nullable final VirtualFile currentWorkingDirectory) {
super(session);
myDebuggingHost = debuggingHost;
myObservatoryPort = observatoryPort;
myExecutionResult = executionResult;
myDartUrlResolver = dartUrlResolver;
myRemoteDebug = remoteDebug;
myTimeout = timeout;
myCurrentWorkingDirectory = currentWorkingDirectory;
myIsolatesInfo = new IsolatesInfo();
final DartVmServiceBreakpointHandler breakpointHandler = new DartVmServiceBreakpointHandler(this);
myBreakpointHandlers = new XBreakpointHandler[]{breakpointHandler};
setLogger();
session.addSessionListener(new XDebugSessionListener() {
@Override
public void sessionPaused() {
stackFrameChanged();
}
@Override
public void stackFrameChanged() {
final XStackFrame stackFrame = getSession().getCurrentStackFrame();
myLatestCurrentIsolateId =
stackFrame instanceof DartVmServiceStackFrame ? ((DartVmServiceStackFrame)stackFrame).getIsolateId() : null;
}
});
myDASExecutionContextId = dasExecutionContextId;
if (remoteDebug) {
// TODO won't work since Dart SDK 1.22 because auth token in URL is required
scheduleConnect(getObservatoryUrl("ws", "/ws"));
}
else {
getProcessHandler().addProcessListener(new ProcessAdapter() {
@Override
public void onTextAvailable(final ProcessEvent event, final Key outputType) {
final String prefix = DartConsoleFilter.OBSERVATORY_LISTENING_ON + "http://";
if (event.getText().startsWith(prefix)) {
getProcessHandler().removeProcessListener(this);
final String urlBase = event.getText().substring(prefix.length());
scheduleConnect("ws://" + StringUtil.trimTrailing(urlBase.trim(), '/') + "/ws");
myOpenObservatoryAction.setUrl("http://" + urlBase);
}
}
});
}
if (remoteDebug) {
LOG.assertTrue(myExecutionResult == null && myDASExecutionContextId == null, myDASExecutionContextId + myExecutionResult);
}
else {
LOG.assertTrue(myExecutionResult != null && myDASExecutionContextId != null, myDASExecutionContextId + myExecutionResult);
}
}
public VmServiceWrapper getVmServiceWrapper() {
return myVmServiceWrapper;
}
public Collection<IsolatesInfo.IsolateInfo> getIsolateInfos() {
return myIsolatesInfo.getIsolateInfos();
}
private void setLogger() {
Logging.setLogger(new org.dartlang.vm.service.logging.Logger() {
@Override
public void logError(final String message) {
if (message.contains("\"code\":102,")) { // Cannot add breakpoint, already logged in logInformation()
return;
}
if (message.contains("\"method\":\"removeBreakpoint\"")) { // That's expected because we set one breakpoint twice
return;
}
getSession().getConsoleView().print("Error: " + message + "\n", ConsoleViewContentType.ERROR_OUTPUT);
LOG.error(message);
}
@Override
public void logError(final String message, final Throwable exception) {
getSession().getConsoleView().print("Error: " + message + "\n", ConsoleViewContentType.ERROR_OUTPUT);
LOG.error(message, exception);
}
@Override
public void logInformation(String message) {
if (message.length() > 500) {
message = message.substring(0, 300) + "..." + message.substring(message.length() - 200);
}
LOG.debug(message);
}
@Override
public void logInformation(final String message, final Throwable exception) {
LOG.debug(message, exception);
}
});
}
protected void scheduleConnect(@NotNull final String url) {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
long timeout = (long)myTimeout;
long startTime = System.currentTimeMillis();
try {
while (true) {
try {
connect(url);
break;
}
catch (IOException e) {
if (System.currentTimeMillis() > startTime + timeout) {
throw e;
}
else {
TimeoutUtil.sleep(50);
}
}
}
}
catch (IOException e) {
String message = "Failed to connect to the VM observatory service: " + e.toString() + "\n";
Throwable cause = e.getCause();
while (cause != null) {
message += "Caused by: " + cause.toString() + "\n";
final Throwable cause1 = cause.getCause();
if (cause1 != cause) {
cause = cause1;
}
}
getSession().getConsoleView().print(message, ConsoleViewContentType.ERROR_OUTPUT);
getSession().stop();
}
});
}
private void connect(@NotNull final String url) throws IOException {
final VmService vmService = VmService.connect(url);
final DartVmServiceListener vmServiceListener =
new DartVmServiceListener(this, (DartVmServiceBreakpointHandler)myBreakpointHandlers[0]);
vmService.addVmServiceListener(vmServiceListener);
myVmServiceWrapper =
new VmServiceWrapper(this, vmService, vmServiceListener, myIsolatesInfo, (DartVmServiceBreakpointHandler)myBreakpointHandlers[0]);
myVmServiceWrapper.handleDebuggerConnected();
myVmConnected = true;
}
@NotNull
@Deprecated // returns incorrect URL for Dart SDK 1.22+ because returned URL doesn't contain auth token
private String getObservatoryUrl(@NotNull final String scheme, @Nullable final String path) {
return scheme + "://" + myDebuggingHost + ":" + myObservatoryPort + StringUtil.notNullize(path);
}
@Override
protected ProcessHandler doGetProcessHandler() {
return myExecutionResult == null ? super.doGetProcessHandler() : myExecutionResult.getProcessHandler();
}
@NotNull
@Override
public ExecutionConsole createConsole() {
return myExecutionResult == null ? super.createConsole() : myExecutionResult.getExecutionConsole();
}
@NotNull
@Override
public XDebuggerEditorsProvider getEditorsProvider() {
return new DartDebuggerEditorsProvider();
}
@Override
@NotNull
public XBreakpointHandler<?>[] getBreakpointHandlers() {
return myBreakpointHandlers;
}
public boolean isRemoteDebug() {
return myRemoteDebug;
}
public void guessRemoteProjectRoot(@NotNull final ElementList<LibraryRef> libraries) {
final VirtualFile pubspec = myDartUrlResolver.getPubspecYamlFile();
final VirtualFile projectRoot = pubspec != null ? pubspec.getParent() : myCurrentWorkingDirectory;
if (projectRoot == null) return;
for (LibraryRef library : libraries) {
final String remoteUri = library.getUri();
if (remoteUri.startsWith(DartUrlResolver.DART_PREFIX)) continue;
if (remoteUri.startsWith(DartUrlResolver.PACKAGE_PREFIX)) continue;
final PsiFile[] localFilesWithSameName = ReadAction.compute(() -> {
final String remoteFileName = PathUtil.getFileName(remoteUri);
final GlobalSearchScope scope = GlobalSearchScopesCore.directoryScope(getSession().getProject(), projectRoot, true);
return FilenameIndex.getFilesByName(getSession().getProject(), remoteFileName, scope);
});
int howManyFilesMatch = 0;
for (PsiFile psiFile : localFilesWithSameName) {
final VirtualFile file = DartResolveUtil.getRealVirtualFile(psiFile);
if (file == null) continue;
LOG.assertTrue(file.getPath().startsWith(projectRoot.getPath() + "/"), file.getPath() + "," + projectRoot.getPath());
final String relPath = file.getPath().substring(projectRoot.getPath().length()); // starts with slash
if (remoteUri.endsWith(relPath)) {
howManyFilesMatch++;
myRemoteProjectRootUri = remoteUri.substring(0, remoteUri.length() - relPath.length());
}
}
if (howManyFilesMatch == 1) {
break; // we did the best guess we could
}
}
}
@Override
public void startStepOver(@Nullable XSuspendContext context) {
if (myLatestCurrentIsolateId != null && mySuspendedIsolateIds.contains(myLatestCurrentIsolateId)) {
DartVmServiceSuspendContext suspendContext = (DartVmServiceSuspendContext)context;
final StepOption stepOption = suspendContext != null && suspendContext.getAtAsyncSuspension() ? StepOption.OverAsyncSuspension
: StepOption.Over;
myVmServiceWrapper.resumeIsolate(myLatestCurrentIsolateId, stepOption);
}
}
@Override
public void startStepInto(@Nullable XSuspendContext context) {
if (myLatestCurrentIsolateId != null && mySuspendedIsolateIds.contains(myLatestCurrentIsolateId)) {
myVmServiceWrapper.resumeIsolate(myLatestCurrentIsolateId, StepOption.Into);
}
}
@Override
public void startStepOut(@Nullable XSuspendContext context) {
if (myLatestCurrentIsolateId != null && mySuspendedIsolateIds.contains(myLatestCurrentIsolateId)) {
myVmServiceWrapper.resumeIsolate(myLatestCurrentIsolateId, StepOption.Out);
}
}
public void dropFrame(DartVmServiceStackFrame frame) {
myVmServiceWrapper.dropFrame(frame.getIsolateId(), frame.getFrameIndex() + 1);
}
@Override
public void stop() {
myVmConnected = false;
if (myVmServiceWrapper != null) {
if (myDASExecutionContextId != null) {
DartAnalysisServerService.getInstance(getSession().getProject()).execution_deleteContext(myDASExecutionContextId);
}
Disposer.dispose(myVmServiceWrapper);
}
}
@Override
public void resume(@Nullable XSuspendContext context) {
for (String isolateId : new ArrayList<>(mySuspendedIsolateIds)) {
myVmServiceWrapper.resumeIsolate(isolateId, null);
}
}
@Override
public void startPausing() {
for (IsolatesInfo.IsolateInfo info : getIsolateInfos()) {
if (!mySuspendedIsolateIds.contains(info.getIsolateId())) {
myVmServiceWrapper.pauseIsolate(info.getIsolateId());
}
}
}
@Override
public void runToPosition(@NotNull XSourcePosition position, @Nullable XSuspendContext context) {
if (myLatestCurrentIsolateId != null && mySuspendedIsolateIds.contains(myLatestCurrentIsolateId)) {
// Set a temporary breakpoint and resume.
myVmServiceWrapper.addTemporaryBreakpoint(position, myLatestCurrentIsolateId);
myVmServiceWrapper.resumeIsolate(myLatestCurrentIsolateId, null);
}
}
public void isolateSuspended(@NotNull final IsolateRef isolateRef) {
mySuspendedIsolateIds.add(isolateRef.getId());
}
public boolean isIsolateSuspended(@NotNull final String isolateId) {
return mySuspendedIsolateIds.contains(isolateId);
}
public boolean isIsolateAlive(@NotNull final String isolateId) {
for (IsolatesInfo.IsolateInfo isolateInfo : myIsolatesInfo.getIsolateInfos()) {
if (isolateId.equals(isolateInfo.getIsolateId())) {
return true;
}
}
return false;
}
public void isolateResumed(@NotNull final IsolateRef isolateRef) {
mySuspendedIsolateIds.remove(isolateRef.getId());
}
public void isolateExit(@NotNull final IsolateRef isolateRef) {
myIsolatesInfo.deleteIsolate(isolateRef);
mySuspendedIsolateIds.remove(isolateRef.getId());
if (isolateRef.getId().equals(myLatestCurrentIsolateId)) {
resume(getSession().getSuspendContext()); // otherwise no way no resume them from UI
}
}
public void handleWriteEvent(String base64Data) {
String message = new String(Base64.getDecoder().decode(base64Data), Charsets.UTF_8);
getSession().getConsoleView().print(message, ConsoleViewContentType.NORMAL_OUTPUT);
}
@Override
public String getCurrentStateMessage() {
return getSession().isStopped()
? XDebuggerBundle.message("debugger.state.message.disconnected")
: myVmConnected
? XDebuggerBundle.message("debugger.state.message.connected")
: DartBundle.message("debugger.trying.to.connect.vm.at.0", getObservatoryUrl("ws", "/ws"));
}
@Override
public void registerAdditionalActions(@NotNull final DefaultActionGroup leftToolbar,
@NotNull final DefaultActionGroup topToolbar,
@NotNull final DefaultActionGroup settings) {
// For Run tool window this action is added in DartCommandLineRunningState.createActions()
topToolbar.addSeparator();
topToolbar.addAction(myOpenObservatoryAction);
topToolbar.addAction(new DartPopFrameAction());
}
@NotNull
public Collection<String> getUrisForFile(@NotNull final VirtualFile file) {
final Set<String> result = new HashSet<>();
String uriByIde = myDartUrlResolver.getDartUrlForFile(file);
// If dart:, short circuit the results.
if (uriByIde.startsWith(DartUrlResolver.DART_PREFIX)) {
result.add(uriByIde);
return result;
}
// file:
if (uriByIde.startsWith(DartUrlResolver.FILE_PREFIX)) {
result.add(threeSlashize(uriByIde));
}
else {
result.add(uriByIde);
result.add(threeSlashize(new File(file.getPath()).toURI().toString()));
}
// straight path - used by some VM embedders
result.add(file.getPath());
// package: (if applicable)
if (myDASExecutionContextId != null) {
final String uriByServer =
DartAnalysisServerService.getInstance(getSession().getProject()).execution_mapUri(myDASExecutionContextId, file.getPath(), null);
if (uriByServer != null) {
result.add(uriByServer);
}
}
// remote prefix (if applicable)
if (myRemoteProjectRootUri != null) {
final VirtualFile pubspec = myDartUrlResolver.getPubspecYamlFile();
if (pubspec != null) {
final String projectPath = pubspec.getParent().getPath();
final String filePath = file.getPath();
if (filePath.startsWith(projectPath)) {
result.add(myRemoteProjectRootUri + filePath.substring(projectPath.length()));
}
}
else if (myCurrentWorkingDirectory != null) {
// Handle projects with no pubspecs.
final String projectPath = myCurrentWorkingDirectory.getPath();
final String filePath = file.getPath();
if (filePath.startsWith(projectPath)) {
result.add(myRemoteProjectRootUri + filePath.substring(projectPath.length()));
}
}
}
return result;
}
@Nullable
public XSourcePosition getSourcePosition(@NotNull final String isolateId, @NotNull final ScriptRef scriptRef, int tokenPos) {
VirtualFile file = ReadAction.compute(() -> {
String uri = scriptRef.getUri();
if (myDASExecutionContextId != null && !isDartPatchUri(uri)) {
final String path =
DartAnalysisServerService.getInstance(getSession().getProject()).execution_mapUri(myDASExecutionContextId, null, uri);
if (path != null) {
return LocalFileSystem.getInstance().findFileByPath(path);
}
}
final VirtualFile pubspec = myDartUrlResolver.getPubspecYamlFile();
if (myRemoteProjectRootUri != null && uri.startsWith(myRemoteProjectRootUri) && pubspec != null) {
final String localRootUri = StringUtil.trimEnd(myDartUrlResolver.getDartUrlForFile(pubspec.getParent()), '/');
LOG.assertTrue(localRootUri.startsWith(DartUrlResolver.FILE_PREFIX), localRootUri);
uri = localRootUri + uri.substring(myRemoteProjectRootUri.length());
}
return myDartUrlResolver.findFileByDartUrl(uri);
});
if (file == null) {
file = myScriptIdToContentMap.get(scriptRef.getId());
}
TIntObjectHashMap<Pair<Integer, Integer>> tokenPosToLineAndColumn = myScriptIdToLinesAndColumnsMap.get(scriptRef.getId());
if (file != null && tokenPosToLineAndColumn != null) {
final Pair<Integer, Integer> lineAndColumn = tokenPosToLineAndColumn.get(tokenPos);
if (lineAndColumn == null) return XDebuggerUtil.getInstance().createPositionByOffset(file, 0);
return XDebuggerUtil.getInstance().createPosition(file, lineAndColumn.first, lineAndColumn.second);
}
final Script script = myVmServiceWrapper.getScriptSync(isolateId, scriptRef.getId());
if (script == null) return null;
if (file == null) {
file = new LightVirtualFile(PathUtil.getFileName(script.getUri()), DartFileType.INSTANCE, script.getSource());
((LightVirtualFile)file).setWritable(false);
myScriptIdToContentMap.put(scriptRef.getId(), (LightVirtualFile)file);
}
if (tokenPosToLineAndColumn == null) {
tokenPosToLineAndColumn = createTokenPosToLineAndColumnMap(script.getTokenPosTable());
myScriptIdToLinesAndColumnsMap.put(scriptRef.getId(), tokenPosToLineAndColumn);
}
final Pair<Integer, Integer> lineAndColumn = tokenPosToLineAndColumn.get(tokenPos);
if (lineAndColumn == null) return XDebuggerUtil.getInstance().createPositionByOffset(file, 0);
return XDebuggerUtil.getInstance().createPosition(file, lineAndColumn.first, lineAndColumn.second);
}
private static boolean isDartPatchUri(@NotNull final String uri) {
// dart:_builtin or dart:core-patch/core_patch.dart
return uri.startsWith("dart:_") || uri.startsWith("dart:") && uri.contains("-patch/");
}
@NotNull
private static TIntObjectHashMap<Pair<Integer, Integer>> createTokenPosToLineAndColumnMap(@NotNull final List<List<Integer>> tokenPosTable) {
// Each subarray consists of a line number followed by (tokenPos, columnNumber) pairs
// see https://github.com/dart-lang/vm_service_drivers/blob/master/dart/tool/service.md#script
final TIntObjectHashMap<Pair<Integer, Integer>> result = new TIntObjectHashMap<>();
for (List<Integer> lineAndPairs : tokenPosTable) {
final Iterator<Integer> iterator = lineAndPairs.iterator();
int line = Math.max(0, iterator.next() - 1);
while (iterator.hasNext()) {
final int tokenPos = iterator.next();
final int column = Math.max(0, iterator.next() - 1);
result.put(tokenPos, Pair.create(line, column));
}
}
return result;
}
@NotNull
private static String threeSlashize(@NotNull final String uri) {
if (!uri.startsWith("file:")) return uri;
if (uri.startsWith("file:///")) return uri;
if (uri.startsWith("file://")) return "file:///" + uri.substring("file://".length());
if (uri.startsWith("file:/")) return "file:///" + uri.substring("file:/".length());
if (uri.startsWith("file:")) return "file:///" + uri.substring("file:".length());
return uri;
}
}