package com.jetbrains.lang.dart.ide.runner.server.vmService;
import com.google.common.collect.Lists;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Ref;
import com.intellij.util.Alarm;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpointProperties;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator;
import com.intellij.xdebugger.frame.XExecutionStack;
import com.intellij.xdebugger.frame.XStackFrame;
import com.jetbrains.lang.dart.DartFileType;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartAsyncMarkerFrame;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartVmServiceEvaluator;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartVmServiceStackFrame;
import com.jetbrains.lang.dart.ide.runner.server.vmService.frame.DartVmServiceValue;
import org.dartlang.vm.service.VmService;
import org.dartlang.vm.service.consumer.*;
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.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class VmServiceWrapper implements Disposable {
public static final Logger LOG = Logger.getInstance(VmServiceWrapper.class.getName());
private static final long RESPONSE_WAIT_TIMEOUT = 3000; // millis
private final DartVmServiceDebugProcess myDebugProcess;
private final VmService myVmService;
private final DartVmServiceListener myVmServiceListener;
private final IsolatesInfo myIsolatesInfo;
private final DartVmServiceBreakpointHandler myBreakpointHandler;
private final Alarm myRequestsScheduler;
private long myVmServiceReceiverThreadId;
@Nullable private StepOption myLatestStep;
public VmServiceWrapper(@NotNull final DartVmServiceDebugProcess debugProcess,
@NotNull final VmService vmService,
@NotNull final DartVmServiceListener vmServiceListener,
@NotNull final IsolatesInfo isolatesInfo,
@NotNull final DartVmServiceBreakpointHandler breakpointHandler) {
myDebugProcess = debugProcess;
myVmService = vmService;
myVmServiceListener = vmServiceListener;
myIsolatesInfo = isolatesInfo;
myBreakpointHandler = breakpointHandler;
myRequestsScheduler = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
}
@Override
public void dispose() {
}
private void addRequest(@NotNull final Runnable runnable) {
if (!myRequestsScheduler.isDisposed()) {
myRequestsScheduler.addRequest(runnable, 0);
}
}
@Nullable
public StepOption getLatestStep() {
return myLatestStep;
}
private void assertSyncRequestAllowed() {
if (ApplicationManager.getApplication().isDispatchThread()) {
LOG.error("EDT should not be blocked by waiting for for the answer from the Dart debugger");
}
if (ApplicationManager.getApplication().isReadAccessAllowed()) {
LOG.error("Waiting for for the answer from the Dart debugger under read action may lead to EDT freeze");
}
if (myVmServiceReceiverThreadId == Thread.currentThread().getId()) {
LOG.error("Synchronous requests must not be made in Web Socket listening thread: answer will never be received");
}
}
public void handleDebuggerConnected() {
streamListen(VmService.DEBUG_STREAM_ID, new VmServiceConsumers.SuccessConsumerWrapper() {
@Override
public void received(final Success success) {
myVmServiceReceiverThreadId = Thread.currentThread().getId();
streamListen(VmService.ISOLATE_STREAM_ID, new VmServiceConsumers.SuccessConsumerWrapper() {
@Override
public void received(final Success success) {
getVm(new VmServiceConsumers.VmConsumerWrapper() {
@Override
public void received(final VM vm) {
if (vm.getIsolates().size() == 0) {
Logging.getLogger().logError("No isolates found after VM start: " + vm.getIsolates().size());
}
for (final IsolateRef isolateRef : vm.getIsolates()) {
getIsolate(isolateRef.getId(), new VmServiceConsumers.GetIsolateConsumerWrapper() {
@Override
public void received(final Isolate isolate) {
final Event event = isolate.getPauseEvent();
final EventKind eventKind = event.getKind();
// if event is not PauseStart it means that PauseStart event will follow later and will be handled by listener
handleIsolate(isolateRef, eventKind == EventKind.PauseStart);
// Handle the case of isolates paused when we connect (this can come up in remote debugging).
if (eventKind == EventKind.PauseBreakpoint ||
eventKind == EventKind.PauseException ||
eventKind == EventKind.PauseInterrupted) {
myDebugProcess.isolateSuspended(isolateRef);
ApplicationManager.getApplication().executeOnPooledThread(() -> {
final ElementList<Breakpoint> breakpoints =
eventKind == EventKind.PauseBreakpoint ? event.getPauseBreakpoints() : null;
final InstanceRef exception = eventKind == EventKind.PauseException ? event.getException() : null;
myVmServiceListener
.onIsolatePaused(isolateRef, breakpoints, exception, event.getTopFrame(), event.getAtAsyncSuspension());
});
}
}
});
}
}
});
}
});
}
});
if (myDebugProcess.isRemoteDebug()) {
streamListen(VmService.STDOUT_STREAM_ID, VmServiceConsumers.EMPTY_SUCCESS_CONSUMER);
streamListen(VmService.STDERR_STREAM_ID, VmServiceConsumers.EMPTY_SUCCESS_CONSUMER);
}
}
private void streamListen(@NotNull final String streamId, @NotNull final SuccessConsumer consumer) {
addRequest(() -> myVmService.streamListen(streamId, consumer));
}
private void getVm(@NotNull final VMConsumer consumer) {
addRequest(() -> myVmService.getVM(consumer));
}
private void getIsolate(@NotNull final String isolateId, @NotNull final GetIsolateConsumer consumer) {
addRequest(() -> myVmService.getIsolate(isolateId, consumer));
}
public void handleIsolate(@NotNull final IsolateRef isolateRef, final boolean isolatePausedStart) {
// We should auto-resume on a StartPaused event, if we're not remote debugging, and after breakpoints have been set.
final boolean newIsolate = myIsolatesInfo.addIsolate(isolateRef);
if (isolatePausedStart) {
myIsolatesInfo.setShouldInitialResume(isolateRef);
}
// Just to make sure that the main isolate is not handled twice, both from handleDebuggerConnected() and DartVmServiceListener.received(PauseStart)
if (newIsolate) {
addRequest(() -> myVmService.setExceptionPauseMode(isolateRef.getId(),
ExceptionPauseMode.Unhandled,
new VmServiceConsumers.SuccessConsumerWrapper() {
@Override
public void received(Success response) {
setInitialBreakpointsAndResume(isolateRef);
}
}));
}
else {
checkInitialResume(isolateRef);
}
}
private void checkInitialResume(IsolateRef isolateRef) {
if (myIsolatesInfo.getShouldInitialResume(isolateRef)) {
resumeIsolate(isolateRef.getId(), null);
}
}
private void setInitialBreakpointsAndResume(@NotNull final IsolateRef isolateRef) {
if (myDebugProcess.isRemoteDebug()) {
if (myDebugProcess.myRemoteProjectRootUri == null) {
// need to detect remote project root path before setting breakpoints
getIsolate(isolateRef.getId(), new VmServiceConsumers.GetIsolateConsumerWrapper() {
@Override
public void received(final Isolate isolate) {
myDebugProcess.guessRemoteProjectRoot(isolate.getLibraries());
doSetInitialBreakpointsAndResume(isolateRef);
}
});
}
else {
doSetInitialBreakpointsAndResume(isolateRef);
}
}
else {
doSetInitialBreakpointsAndResume(isolateRef);
}
}
private void doSetInitialBreakpointsAndResume(@NotNull final IsolateRef isolateRef) {
doSetBreakpointsForIsolate(myBreakpointHandler.getXBreakpoints(), isolateRef.getId(), () -> {
myIsolatesInfo.setBreakpointsSet(isolateRef);
checkInitialResume(isolateRef);
});
}
private void doSetBreakpointsForIsolate(@NotNull final Set<XLineBreakpoint<XBreakpointProperties>> xBreakpoints,
@NotNull final String isolateId,
@Nullable final Runnable onFinished) {
if (xBreakpoints.isEmpty()) {
if (onFinished != null) {
onFinished.run();
}
return;
}
final AtomicInteger counter = new AtomicInteger(xBreakpoints.size());
for (final XLineBreakpoint<XBreakpointProperties> xBreakpoint : xBreakpoints) {
addBreakpoint(isolateId, xBreakpoint.getSourcePosition(), new VmServiceConsumers.BreakpointConsumerWrapper() {
@Override
void sourcePositionNotApplicable() {
checkDone();
}
@Override
public void received(Breakpoint vmBreakpoint) {
myBreakpointHandler.vmBreakpointAdded(xBreakpoint, isolateId, vmBreakpoint);
checkDone();
}
@Override
public void onError(RPCError error) {
myBreakpointHandler.breakpointFailed(xBreakpoint);
checkDone();
}
private void checkDone() {
if (counter.decrementAndGet() == 0 && onFinished != null) {
onFinished.run();
}
}
});
}
}
public void addBreakpoint(@NotNull final String isolateId,
@Nullable final XSourcePosition position,
@NotNull final VmServiceConsumers.BreakpointConsumerWrapper consumer) {
if (position == null || position.getFile().getFileType() != DartFileType.INSTANCE) {
consumer.sourcePositionNotApplicable();
return;
}
addRequest(() -> {
final int line = position.getLine() + 1;
for (String uri : myDebugProcess.getUrisForFile(position.getFile())) {
myVmService.addBreakpointWithScriptUri(isolateId, uri, line, consumer);
}
});
}
public void addBreakpointForIsolates(@NotNull final XLineBreakpoint<XBreakpointProperties> xBreakpoint,
@NotNull final Collection<IsolatesInfo.IsolateInfo> isolateInfos) {
for (final IsolatesInfo.IsolateInfo isolateInfo : isolateInfos) {
addBreakpoint(isolateInfo.getIsolateId(), xBreakpoint.getSourcePosition(), new VmServiceConsumers.BreakpointConsumerWrapper() {
@Override
void sourcePositionNotApplicable() {
}
@Override
public void received(Breakpoint vmBreakpoint) {
myBreakpointHandler.vmBreakpointAdded(xBreakpoint, isolateInfo.getIsolateId(), vmBreakpoint);
}
@Override
public void onError(RPCError error) {
}
});
}
}
/**
* Reloaded scripts need to have their breakpoints re-applied; re-set all existing breakpoints.
*/
public void restoreBreakpointsForIsolate(@NotNull final String isolateId, @Nullable final Runnable onFinished) {
// Remove all existing VM breakpoints for this isolate.
myBreakpointHandler.removeAllVmBreakpoints(isolateId);
// Re-set existing breakpoints.
doSetBreakpointsForIsolate(myBreakpointHandler.getXBreakpoints(), isolateId, onFinished);
}
public void addTemporaryBreakpoint(@NotNull final XSourcePosition position,
@NotNull final String isolateId) {
addBreakpoint(isolateId, position, new VmServiceConsumers.BreakpointConsumerWrapper() {
@Override
void sourcePositionNotApplicable() {
}
@Override
public void received(Breakpoint vmBreakpoint) {
myBreakpointHandler.temporaryBreakpointAdded(isolateId, vmBreakpoint);
}
@Override
public void onError(RPCError error) {
}
});
}
public void removeBreakpoint(@NotNull final String isolateId, @NotNull final String vmBreakpointId) {
addRequest(() -> myVmService.removeBreakpoint(isolateId, vmBreakpointId, VmServiceConsumers.EMPTY_SUCCESS_CONSUMER));
}
public void resumeIsolate(@NotNull final String isolateId, @Nullable final StepOption stepOption) {
addRequest(() -> {
myLatestStep = stepOption;
myVmService.resume(isolateId, stepOption, null, VmServiceConsumers.EMPTY_SUCCESS_CONSUMER);
});
}
/**
* Drop to the indicated frame.
* <p>
* frameIndex specifies the stack frame to rewind to. Stack frame 0 is the currently executing
* function, so frameIndex must be at least 1.
*/
public void dropFrame(@NotNull final String isolateId, int frameIndex) {
addRequest(() -> {
myLatestStep = StepOption.Rewind;
myVmService.resume(isolateId, StepOption.Rewind, frameIndex, new SuccessConsumer() {
@Override
public void onError(RPCError error) {
myDebugProcess.getSession().getConsoleView()
.print("Error from drop frame: " + error.getMessage() + "\n", ConsoleViewContentType.ERROR_OUTPUT);
}
@Override
public void received(Success response) {
}
});
});
}
public void pauseIsolate(@NotNull final String isolateId) {
addRequest(() -> myVmService.pause(isolateId, VmServiceConsumers.EMPTY_SUCCESS_CONSUMER));
}
public void computeStackFrames(@NotNull final String isolateId,
final int firstFrameIndex,
@NotNull final XExecutionStack.XStackFrameContainer container,
@Nullable final InstanceRef exception) {
addRequest(() -> myVmService.getStack(isolateId, new StackConsumer() {
@Override
public void received(final Stack vmStack) {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
InstanceRef exceptionToAddToFrame = exception;
// Check to see if the VM passing in awaiter frames; if so, use them. Else check
// for async causal frames. Finally, fall back to using regular sync frames.
ElementList<Frame> elementList = vmStack.getAwaiterFrames();
if (elementList == null) {
elementList = vmStack.getAsyncCausalFrames();
if (elementList == null) {
elementList = vmStack.getFrames();
}
}
final List<Frame> vmFrames = Lists.newArrayList(elementList);
final List<XStackFrame> xStackFrames = new ArrayList<>(vmFrames.size());
for (final Frame vmFrame : vmFrames) {
if (vmFrame.getKind() == FrameKind.AsyncSuspensionMarker) {
// Render an asynchronous gap.
final XStackFrame markerFrame = new DartAsyncMarkerFrame();
xStackFrames.add(markerFrame);
}
else {
final DartVmServiceStackFrame stackFrame =
new DartVmServiceStackFrame(myDebugProcess, isolateId, vmFrame, vmFrames, exceptionToAddToFrame);
stackFrame.setIsDroppableFrame(vmFrame.getKind() == FrameKind.Regular);
xStackFrames.add(stackFrame);
if (!stackFrame.isInDartSdkPatchFile()) {
// The exception (if any) is added to the frame where debugger stops and to the upper frames.
exceptionToAddToFrame = null;
}
}
}
container.addStackFrames(firstFrameIndex == 0 ? xStackFrames : xStackFrames.subList(firstFrameIndex, xStackFrames.size()), true);
});
}
@Override
public void onError(final RPCError error) {
container.errorOccurred(error.getMessage());
}
}));
}
@Nullable
public Script getScriptSync(@NotNull final String isolateId, @NotNull final String scriptId) {
assertSyncRequestAllowed();
final Semaphore semaphore = new Semaphore();
semaphore.down();
final Ref<Script> resultRef = Ref.create();
addRequest(() -> myVmService.getObject(isolateId, scriptId, new GetObjectConsumer() {
@Override
public void received(Obj script) {
resultRef.set((Script)script);
semaphore.up();
}
@Override
public void received(Sentinel response) {
semaphore.up();
}
@Override
public void onError(RPCError error) {
semaphore.up();
}
}));
semaphore.waitFor(RESPONSE_WAIT_TIMEOUT);
return resultRef.get();
}
public void getObject(@NotNull final String isolateId, @NotNull final String objectId, @NotNull final GetObjectConsumer consumer) {
addRequest(() -> myVmService.getObject(isolateId, objectId, consumer));
}
public void getCollectionObject(@NotNull final String isolateId,
@NotNull final String objectId,
final int offset,
final int count,
@NotNull final GetObjectConsumer consumer) {
addRequest(() -> myVmService.getObject(isolateId, objectId, offset, count, consumer));
}
public void evaluateInFrame(@NotNull final String isolateId,
@NotNull final Frame vmFrame,
@NotNull final String expression,
@NotNull final XDebuggerEvaluator.XEvaluationCallback callback) {
addRequest(() -> myVmService.evaluateInFrame(isolateId, vmFrame.getIndex(), expression, new EvaluateInFrameConsumer() {
@Override
public void received(InstanceRef instanceRef) {
callback.evaluated(new DartVmServiceValue(myDebugProcess, isolateId, "result", instanceRef, null, null, false));
}
@Override
public void received(ErrorRef errorRef) {
callback.errorOccurred(DartVmServiceEvaluator.getPresentableError(errorRef.getMessage()));
}
@Override
public void onError(RPCError error) {
callback.errorOccurred(error.getMessage());
}
}));
}
@SuppressWarnings("SameParameterValue")
public void evaluateInTargetContext(@NotNull final String isolateId,
@NotNull final String targetId,
@NotNull final String expression,
@NotNull final EvaluateConsumer consumer) {
addRequest(() -> myVmService.evaluate(isolateId, targetId, expression, consumer));
}
}