diff --git a/src/som/interpreter/actors/Actor.java b/src/som/interpreter/actors/Actor.java index d5a0cfa69..6f2da1293 100644 --- a/src/som/interpreter/actors/Actor.java +++ b/src/som/interpreter/actors/Actor.java @@ -15,6 +15,8 @@ import som.VM; import som.interpreter.objectstorage.ObjectTransitionSafepoint; import som.primitives.ObjectPrims.IsValue; +import som.vm.Activity; +import som.vm.ActivityThread; import som.vm.VmSettings; import som.vmobjects.SAbstractObject; import som.vmobjects.SArray.STransferArray; @@ -41,7 +43,7 @@ * - grabs the current mailbox * - and sequentially executes all messages */ -public class Actor { +public class Actor implements Activity { public static Actor createActor() { if (VmSettings.DEBUG_MODE) { @@ -265,7 +267,7 @@ public ForkJoinWorkerThread newThread(final ForkJoinPool pool) { } } - public static final class ActorProcessingThread extends ForkJoinWorkerThread { + public static final class ActorProcessingThread extends ForkJoinWorkerThread implements ActivityThread { public EventualMessage currentMessage; private static AtomicInteger threadIdGen = new AtomicInteger(0); protected Actor currentlyExecutingActor; @@ -285,6 +287,11 @@ protected ActorProcessingThread(final ForkJoinPool pool) { } } + @Override + public Activity getActivity() { + return currentMessage.getTarget(); + } + protected long generateActorId() { long result = (threadId << THREAD_ID_SHIFT) | nextActorId; nextActorId++; @@ -371,6 +378,11 @@ public static final void forceSwapBuffers() { } } + @Override + public String getName() { + return toString(); + } + @Override public String toString() { return "Actor"; diff --git a/src/som/primitives/processes/ChannelPrimitives.java b/src/som/primitives/processes/ChannelPrimitives.java index cb1d39e61..3606293a2 100644 --- a/src/som/primitives/processes/ChannelPrimitives.java +++ b/src/som/primitives/processes/ChannelPrimitives.java @@ -1,5 +1,6 @@ package som.primitives.processes; +import java.util.HashSet; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; import java.util.concurrent.ForkJoinWorkerThread; @@ -26,6 +27,8 @@ import som.primitives.Primitive; import som.primitives.arrays.ToArgumentsArrayNode; import som.primitives.arrays.ToArgumentsArrayNodeFactory; +import som.vm.Activity; +import som.vm.ActivityThread; import som.vm.Symbols; import som.vm.VmSettings; import som.vm.constants.KernelObj; @@ -63,6 +66,12 @@ public static void resetClassReferences() { Out = null; OutId = null; } + private static final HashSet activeProcesses = new HashSet<>(); + + public static HashSet getActiveProcesses() { + return activeProcesses; + } + private static final class ProcessThreadFactory implements ForkJoinWorkerThreadFactory { @Override public ForkJoinWorkerThread newThread(final ForkJoinPool pool) { @@ -70,11 +79,19 @@ public ForkJoinWorkerThread newThread(final ForkJoinPool pool) { } } - public static final class ProcessThread extends ForkJoinWorkerThread { + public static final class ProcessThread extends ForkJoinWorkerThread + implements ActivityThread { + private Process current; + ProcessThread(final ForkJoinPool pool) { super(pool); } + + @Override + public Activity getActivity() { + return current; + } } - private static final class Process implements Runnable { + public static final class Process implements Activity, Runnable { private final SObjectWithClass obj; Process(final SObjectWithClass obj) { @@ -83,9 +100,28 @@ private static final class Process implements Runnable { @Override public void run() { - SInvokable disp = (SInvokable) obj.getSOMClass().lookupMessage( - Symbols.symbolFor("run"), AccessModifier.PROTECTED); - disp.invoke(obj); + ((ProcessThread) Thread.currentThread()).current = this; + + synchronized (activeProcesses) { + activeProcesses.add(this); + } + + try { + SInvokable disp = (SInvokable) obj.getSOMClass().lookupMessage( + Symbols.symbolFor("run"), AccessModifier.PROTECTED); + disp.invoke(obj); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + synchronized (activeProcesses) { + activeProcesses.remove(this); + } + } + } + + @Override + public String getName() { + return obj.getSOMClass().getName().getString(); } } diff --git a/src/som/primitives/threading/ThreadPrimitives.java b/src/som/primitives/threading/ThreadPrimitives.java index 5f55768c8..64ae64e05 100644 --- a/src/som/primitives/threading/ThreadPrimitives.java +++ b/src/som/primitives/threading/ThreadPrimitives.java @@ -11,6 +11,8 @@ import som.primitives.Primitive; import som.primitives.arrays.ToArgumentsArrayNode; import som.primitives.arrays.ToArgumentsArrayNodeFactory; +import som.vm.Activity; +import som.vm.ActivityThread; import som.vm.constants.Nil; import som.vmobjects.SArray; import som.vmobjects.SBlock; @@ -113,7 +115,8 @@ public final SClass doSClass(final SClass module) { } } - public static final class SomThread extends Thread { + public static final class SomThread extends Thread + implements Activity, ActivityThread { private final Object[] args; private final SBlock block; @@ -131,5 +134,10 @@ public void run() { ObjectTransitionSafepoint.INSTANCE.unregister(); } } + + @Override + public Activity getActivity() { + return this; + } } } diff --git a/src/som/vm/Activity.java b/src/som/vm/Activity.java new file mode 100644 index 000000000..9625da383 --- /dev/null +++ b/src/som/vm/Activity.java @@ -0,0 +1,6 @@ +package som.vm; + + +public interface Activity { + String getName(); +} diff --git a/src/som/vm/ActivityThread.java b/src/som/vm/ActivityThread.java new file mode 100644 index 000000000..44fe53e68 --- /dev/null +++ b/src/som/vm/ActivityThread.java @@ -0,0 +1,6 @@ +package som.vm; + + +public interface ActivityThread { + Activity getActivity(); +} diff --git a/src/som/vmobjects/SObject.java b/src/som/vmobjects/SObject.java index b74d46a5b..96262de48 100644 --- a/src/som/vmobjects/SObject.java +++ b/src/som/vmobjects/SObject.java @@ -116,11 +116,11 @@ public static final class SMutableObject extends SObject { private long primField4; private long primField5; - @SuppressWarnings("unused") private Object field1; - @SuppressWarnings("unused") private Object field2; - @SuppressWarnings("unused") private Object field3; - @SuppressWarnings("unused") private Object field4; - @SuppressWarnings("unused") private Object field5; + private Object field1; + private Object field2; + private Object field3; + private Object field4; + private Object field5; // this field exists because HotSpot reorders fields, and we need to keep // the layouts in sync to avoid having to manage different offsets for diff --git a/src/tools/debugger/FrontendConnector.java b/src/tools/debugger/FrontendConnector.java index a4359ff09..4c5ec3460 100644 --- a/src/tools/debugger/FrontendConnector.java +++ b/src/tools/debugger/FrontendConnector.java @@ -14,7 +14,6 @@ import org.java_websocket.WebSocket; import com.google.gson.Gson; -import com.oracle.truffle.api.debug.SuspendedEvent; import com.oracle.truffle.api.instrumentation.Instrumenter; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.Source; @@ -26,8 +25,8 @@ import som.vmobjects.SSymbol; import tools.SourceCoordinate; import tools.SourceCoordinate.TaggedSourceCoordinate; -import tools.concurrency.ActorExecutionTrace; import tools.Tagging; +import tools.concurrency.ActorExecutionTrace; import tools.debugger.frontend.Suspension; import tools.debugger.message.Message; import tools.debugger.message.Message.OutgoingMessage; @@ -36,8 +35,8 @@ import tools.debugger.message.SourceMessage.SourceData; import tools.debugger.message.StackTraceResponse; import tools.debugger.message.StoppedMessage; -import tools.debugger.message.SuspendedEventMessage; import tools.debugger.message.SymbolMessage; +import tools.debugger.message.ThreadsResponse; import tools.debugger.message.VariablesResponse; import tools.debugger.session.AsyncMessageReceiverBreakpoint; import tools.debugger.session.Breakpoints; @@ -193,6 +192,12 @@ private void sendBufferedSources( } } + + + public synchronized void sendThreads(final int requestId) { + send(ThreadsResponse.create(webDebugger.getAllActivities(), requestId)); + } + public void sendLoadedSource(final Source source, final Map>>> loadedSourcesTags, final Map> rootNodes) { @@ -230,13 +235,6 @@ public void awaitClient() { log("[DEBUGGER] Debugger connected."); } - public void sendSuspendedEvent(final Suspension suspension) { - sendTracingData(); - send(SuspendedEventMessage.create( - suspension.getEvent(), - SUSPENDED_EVENT_ID_PREFIX + suspension.activityId)); - } - public void sendStackTrace(final int startFrame, final int levels, final Suspension suspension, final int requestId) { send(StackTraceResponse.create(startFrame, levels, suspension, requestId)); @@ -250,7 +248,6 @@ public void sendScopes(final int frameId, final Suspension suspension, public void sendVariables(final int varRef, final int requestId, final Suspension suspension) { send(VariablesResponse.create(varRef, requestId, suspension)); } - private static final String SUSPENDED_EVENT_ID_PREFIX = "se-"; public void sendStoppedMessage(final Suspension suspension) { send(StoppedMessage.create(suspension)); @@ -294,29 +291,18 @@ public Suspension getSuspension(final int activityId) { return webDebugger.getSuspension(activityId); } - public Suspension getSuspension(final String suspendedEventId) { - int activityId = Integer.valueOf(suspendedEventId.substring(SUSPENDED_EVENT_ID_PREFIX.length())); - return webDebugger.getSuspension(activityId); - } - public Suspension getSuspensionForGlobalId(final int globalId) { return webDebugger.getSuspension(Suspension.getActivityIdFromGlobalId(globalId)); } - public SuspendedEvent getSuspendedEvent(final String id) { - int activityId = Integer.valueOf(id.substring(SUSPENDED_EVENT_ID_PREFIX.length())); - return webDebugger.getSuspendedEvent(activityId); - } - static void log(final String str) { // Checkstyle: stop System.out.println(str); // Checkstyle: resume } - public void completeConnection(final WebSocket conn, final boolean debuggerProtocol) { + public void completeConnection(final WebSocket conn) { clientConnected.complete(conn); - webDebugger.useDebuggerProtocol(debuggerProtocol); } public void shutdown() { diff --git a/src/tools/debugger/WebDebugger.java b/src/tools/debugger/WebDebugger.java index 573e4d96f..cb3df4772 100644 --- a/src/tools/debugger/WebDebugger.java +++ b/src/tools/debugger/WebDebugger.java @@ -1,9 +1,11 @@ package tools.debugger; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -19,9 +21,10 @@ import com.oracle.truffle.api.source.Source; import com.oracle.truffle.api.source.SourceSection; -import som.interpreter.actors.Actor.ActorProcessingThread; -import som.primitives.processes.ChannelPrimitives.ProcessThread; -import som.primitives.threading.ThreadPrimitives.SomThread; +import som.interpreter.actors.Actor; +import som.primitives.processes.ChannelPrimitives; +import som.vm.Activity; +import som.vm.ActivityThread; import tools.debugger.frontend.Suspension; import tools.debugger.message.InitialBreakpointsMessage; import tools.debugger.message.Message.IncommingMessage; @@ -37,8 +40,9 @@ import tools.debugger.message.StepMessage.StepOver; import tools.debugger.message.StepMessage.Stop; import tools.debugger.message.StoppedMessage; -import tools.debugger.message.SuspendedEventMessage; import tools.debugger.message.SymbolMessage; +import tools.debugger.message.ThreadsRequest; +import tools.debugger.message.ThreadsResponse; import tools.debugger.message.UpdateBreakpoint; import tools.debugger.message.VariablesRequest; import tools.debugger.message.VariablesResponse; @@ -65,18 +69,17 @@ public class WebDebugger extends TruffleInstrument implements SuspendedCallback private FrontendConnector connector; private Instrumenter instrumenter; private Breakpoints breakpoints; - private boolean debuggerProtocol; private final Map>>> loadedSourcesTags = new HashMap<>(); private final Map> rootNodes = new HashMap<>(); private int nextActivityId = 0; - private final Map activityToSuspension = new HashMap<>(); - private final Map idToSuspension = new HashMap<>(); + private final Map activityToSuspension = new HashMap<>(); + private final Map idToSuspension = new HashMap<>(); + private final WeakHashMap activities = new WeakHashMap<>(); - public void useDebuggerProtocol(final boolean debuggerProtocol) { - this.debuggerProtocol = debuggerProtocol; - } + /** Actors that have been suspended at least once. */ + private final Set suspendedActors = Collections.newSetFromMap(new WeakHashMap<>()); public void reportSyntaxElement(final Class type, final SourceSection source) { @@ -106,22 +109,34 @@ public void prepareSteppingAfterNextRootNode() { breakpoints.prepareSteppingAfterNextRootNode(); } - synchronized SuspendedEvent getSuspendedEvent(final int activityId) { - Suspension suspension = idToSuspension.get(activityId); - assert suspension != null; - assert suspension.getEvent() != null; - return suspension.getEvent(); + private int getActivityId(final Activity a) { + return activities.computeIfAbsent(a, key -> { nextActivityId++; return nextActivityId; }); + } + + synchronized Map getAllActivities() { + synchronized (suspendedActors) { + for (Actor a : suspendedActors) { + getActivityId(a); + } + } + + Set processes = ChannelPrimitives.getActiveProcesses(); + synchronized (processes) { + for (ChannelPrimitives.Process p : processes) { + getActivityId(p); + } + } + return activities; } Suspension getSuspension(final int activityId) { return idToSuspension.get(activityId); } - private synchronized Suspension getSuspension(final Object activity) { + private synchronized Suspension getSuspension(final Activity activity) { Suspension suspension = activityToSuspension.get(activity); if (suspension == null) { - int id = nextActivityId; - nextActivityId += 1; + int id = getActivityId(activity); suspension = new Suspension(activity, id); activityToSuspension.put(activity, suspension); @@ -130,18 +145,19 @@ private synchronized Suspension getSuspension(final Object activity) { return suspension; } + private Suspension getSuspension() { Thread thread = Thread.currentThread(); - Object current; - if (thread instanceof ActorProcessingThread) { - current = ((ActorProcessingThread) thread).currentMessage.getTarget(); - } else if (thread instanceof SomThread) { - current = thread; - } else if (thread instanceof ProcessThread) { - current = thread; + Activity current; + if (thread instanceof ActivityThread) { + current = ((ActivityThread) thread).getActivity(); + if (current instanceof Actor) { + synchronized (suspendedActors) { + suspendedActors.add((Actor) current); + } + } } else { - assert thread.getClass() == Thread.class : "Should support other thread subclasses explicitly"; - current = thread; + throw new RuntimeException("Support for " + thread.getClass().getName() + " not yet implemented."); } return getSuspension(current); } @@ -151,11 +167,7 @@ public void onSuspend(final SuspendedEvent e) { Suspension suspension = getSuspension(); suspension.update(e); - if (debuggerProtocol) { - connector.sendStoppedMessage(suspension); - } else { - connector.sendSuspendedEvent(suspension); - } + connector.sendStoppedMessage(suspension); suspension.suspend(); } @@ -199,12 +211,12 @@ public Breakpoints getBreakpoints() { public static Gson createJsonProcessor() { ClassHierarchyAdapterFactory outMsgAF = new ClassHierarchyAdapterFactory<>(OutgoingMessage.class, "type"); outMsgAF.register("source", SourceMessage.class); - outMsgAF.register("suspendEvent", SuspendedEventMessage.class); outMsgAF.register("StoppedEvent", StoppedMessage.class); - outMsgAF.register("symbolMessage", SymbolMessage.class); + outMsgAF.register("SymbolMessage", SymbolMessage.class); outMsgAF.register("StackTraceResponse", StackTraceResponse.class); outMsgAF.register("ScopesResponse", ScopesResponse.class); outMsgAF.register("VariablesResponse", VariablesResponse.class); + outMsgAF.register("ThreadsResponse", ThreadsResponse.class); ClassHierarchyAdapterFactory inMsgAF = new ClassHierarchyAdapterFactory<>(IncommingMessage.class, "action"); inMsgAF.register(INITIAL_BREAKPOINTS, InitialBreakpointsMessage.class); @@ -217,6 +229,7 @@ public static Gson createJsonProcessor() { inMsgAF.register("StackTraceRequest", StackTraceRequest.class); inMsgAF.register("ScopesRequest", ScopesRequest.class); inMsgAF.register("VariablesRequest", VariablesRequest.class); + inMsgAF.register("ThreadsRequest", ThreadsRequest.class); ClassHierarchyAdapterFactory breakpointAF = new ClassHierarchyAdapterFactory<>(BreakpointInfo.class, "type"); breakpointAF.register(LineBreakpoint.class); diff --git a/src/tools/debugger/message/InitialBreakpointsMessage.java b/src/tools/debugger/message/InitialBreakpointsMessage.java index 386ffb2be..781172d23 100644 --- a/src/tools/debugger/message/InitialBreakpointsMessage.java +++ b/src/tools/debugger/message/InitialBreakpointsMessage.java @@ -10,14 +10,8 @@ public class InitialBreakpointsMessage extends IncommingMessage { private final BreakpointInfo[] breakpoints; - /** - * The client is using a protocol similar to VS code. - */ - private final boolean debuggerProtocol; - public InitialBreakpointsMessage(final BreakpointInfo[] breakpoints) { this.breakpoints = breakpoints; - this.debuggerProtocol = false; } public BreakpointInfo[] getBreakpoints() { @@ -29,6 +23,6 @@ public void process(final FrontendConnector connector, final WebSocket conn) { for (BreakpointInfo bp : breakpoints) { bp.registerOrUpdate(connector); } - connector.completeConnection(conn, debuggerProtocol); + connector.completeConnection(conn); } } diff --git a/src/tools/debugger/message/StackTraceResponse.java b/src/tools/debugger/message/StackTraceResponse.java index 4a25a83c5..a3dad95da 100644 --- a/src/tools/debugger/message/StackTraceResponse.java +++ b/src/tools/debugger/message/StackTraceResponse.java @@ -23,6 +23,15 @@ private StackTraceResponse(final StackFrame[] stackFrames, super(requestId); this.stackFrames = stackFrames; this.totalFrames = totalFrames; + + boolean assertsOn = false; + assert assertsOn = true; + + if (assertsOn) { + for (StackFrame sf : stackFrames) { + assert sf != null; + } + } } static class StackFrame { @@ -49,8 +58,12 @@ static class StackFrame { /** An optional end column of the range covered by the stack frame. */ private final int endColumn; + /** An optional number of characters in the range. */ + private final int length; + StackFrame(final int id, final String name, final String sourceUri, - final int line, final int column, final int endLine, final int endColumn) { + final int line, final int column, final int endLine, + final int endColumn, final int length) { this.id = id; this.name = name; this.sourceUri = sourceUri; @@ -58,6 +71,7 @@ static class StackFrame { this.column = column; this.endLine = endLine; this.endColumn = endColumn; + this.length = length; } } @@ -70,11 +84,17 @@ public static StackTraceResponse create(final int startFrame, final int levels, skipFrames = startFrame; } - StackFrame[] arr = new StackFrame[Math.min(frames.size(), levels) - skipFrames]; + int numFrames = levels; + if (numFrames == 0) { numFrames = Integer.MAX_VALUE; } + numFrames = Math.min(frames.size(), numFrames); + numFrames -= skipFrames; + + StackFrame[] arr = new StackFrame[numFrames]; - for (int frameId = skipFrames; frameId < frames.size() && frameId < levels; frameId += 1) { + for (int i = 0; i < numFrames; i += 1) { + int frameId = i + skipFrames; StackFrame f = createFrame(suspension, frameId, frames.get(frameId)); - arr[frameId - skipFrames] = f; + arr[i] = f; } return new StackTraceResponse(arr, frames.size(), requestId); @@ -95,19 +115,22 @@ private static StackFrame createFrame(final Suspension suspension, int column; int endLine; int endColumn; + int length; if (ss != null) { sourceUri = ss.getSource().getURI().toString(); line = ss.getStartLine(); column = ss.getStartColumn(); endLine = ss.getEndLine(); endColumn = ss.getEndColumn(); + length = ss.getCharLength(); } else { sourceUri = null; line = 0; column = 0; endLine = 0; endColumn = 0; + length = 0; } - return new StackFrame(id, name, sourceUri, line, column, endLine, endColumn); + return new StackFrame(id, name, sourceUri, line, column, endLine, endColumn, length); } } diff --git a/src/tools/debugger/message/StepMessage.java b/src/tools/debugger/message/StepMessage.java index 6bd966a8c..10a2c606e 100644 --- a/src/tools/debugger/message/StepMessage.java +++ b/src/tools/debugger/message/StepMessage.java @@ -5,24 +5,25 @@ import com.oracle.truffle.api.debug.SuspendedEvent; import tools.debugger.FrontendConnector; +import tools.debugger.frontend.Suspension; import tools.debugger.message.Message.IncommingMessage; public abstract class StepMessage extends IncommingMessage { - private final String suspendEvent; + private final int activityId; /** * Note: meant for serialization. */ protected StepMessage() { - suspendEvent = null; + activityId = -1; } @Override public void process(final FrontendConnector connector, final WebSocket conn) { - SuspendedEvent event = connector.getSuspendedEvent(suspendEvent); - assert event != null : "didn't find SuspendEvent"; - process(event); - connector.getSuspension(suspendEvent).resume(); + Suspension susp = connector.getSuspension(activityId); + assert susp.getEvent() != null : "didn't find SuspendEvent"; + process(susp.getEvent()); + susp.resume(); } public abstract void process(SuspendedEvent event); diff --git a/src/tools/debugger/message/StoppedMessage.java b/src/tools/debugger/message/StoppedMessage.java index 79c772662..de4acde14 100644 --- a/src/tools/debugger/message/StoppedMessage.java +++ b/src/tools/debugger/message/StoppedMessage.java @@ -1,7 +1,7 @@ package tools.debugger.message; import som.interpreter.actors.Actor; -import som.primitives.processes.ChannelPrimitives.ProcessThread; +import som.primitives.processes.ChannelPrimitives; import som.primitives.threading.ThreadPrimitives.SomThread; import som.vm.NotYetImplementedException; import tools.debugger.frontend.Suspension; @@ -63,7 +63,7 @@ public static StoppedMessage create(final Suspension suspension) { type = ActivityType.Actor; } else if (suspension.getActivity() instanceof SomThread) { type = ActivityType.Thread; - } else if (suspension.getActivity() instanceof ProcessThread) { + } else if (suspension.getActivity() instanceof ChannelPrimitives.Process) { type = ActivityType.Process; } else { // need to support this type of activity first diff --git a/src/tools/debugger/message/SuspendedEventMessage.java b/src/tools/debugger/message/SuspendedEventMessage.java deleted file mode 100644 index 91698b8a2..000000000 --- a/src/tools/debugger/message/SuspendedEventMessage.java +++ /dev/null @@ -1,136 +0,0 @@ -package tools.debugger.message; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import com.oracle.truffle.api.Truffle; -import com.oracle.truffle.api.debug.DebugStackFrame; -import com.oracle.truffle.api.debug.DebugValue; -import com.oracle.truffle.api.debug.SuspendedEvent; -import com.oracle.truffle.api.frame.FrameInstance.FrameAccess; -import com.oracle.truffle.api.frame.MaterializedFrame; -import com.oracle.truffle.api.source.SourceSection; - -import som.primitives.ObjectPrims.HaltPrim; -import tools.SourceCoordinate; -import tools.SourceCoordinate.FullSourceCoordinate; -import tools.debugger.frontend.Suspension; -import tools.debugger.message.Message.OutgoingMessage; - - -@SuppressWarnings({"unused", "deprecation"}) -public class SuspendedEventMessage extends OutgoingMessage { - - public static SuspendedEventMessage create(final SuspendedEvent e, - final String eventId) { - String sourceUri = getSuspendedSourceUri(e); - Frame[] frames = getStack(e); - TopFrame topFrame = getTopFrame(e, e.getFrame()); - - return new SuspendedEventMessage(sourceUri, eventId, getStack(e), topFrame); - } - - private final String id; - private final String sourceUri; - - private final Frame[] stack; - private final TopFrame topFrame; - - protected SuspendedEventMessage(final String uri, final String eventId, - final Frame[] stack, final TopFrame topFrame) { - this.sourceUri = uri; - this.id = eventId; - this.stack = stack; - this.topFrame = topFrame; - } - - protected static class Frame { - private final FullSourceCoordinate sourceSection; - private final String methodName; - - protected Frame(final FullSourceCoordinate sourceSection, - final String methodName) { - this.sourceSection = sourceSection; - this.methodName = methodName; - } - } - - protected static class TopFrame { - private final String[] arguments; - private final Map slots; - - protected TopFrame(final String[] arguments, final Map slots) { - this.arguments = arguments; - this.slots = slots; - } - } - - private static TopFrame getTopFrame(final SuspendedEvent e, - final MaterializedFrame topFrame) { - com.oracle.truffle.api.frame.Frame[] frame = new com.oracle.truffle.api.frame.Frame[1]; - if (isHaltPrimitive(e)) { - int[] skipFrames = new int[]{Suspension.FRAMES_SKIPPED_FOR_HALT}; - - Truffle.getRuntime().iterateFrames(frameInstance -> { - if (skipFrames[0] > 0) { - skipFrames[0] -= 1; - return null; - } - frame[0] = frameInstance.getFrame(FrameAccess.READ_ONLY, true); - return frameInstance; - }); - } else { - frame[0] = topFrame; - } - - Object[] arguments = frame[0].getArguments(); - String[] args = new String[arguments.length]; - - for (int i = 0; i < arguments.length; i += 1) { - args[i] = Objects.toString(arguments[i]); - } - - Map slots = new HashMap<>(); - - for (DebugValue v : e.getTopStackFrame()) { - slots.put(v.getName(), v.as(String.class)); - } - - return new TopFrame(args, slots); - } - - private static Frame[] getStack(final SuspendedEvent e) { - int skipFrames = 0; - if (isHaltPrimitive(e)) { - skipFrames = 2; - } - - ArrayList frames = new ArrayList<>(); - for (DebugStackFrame f : e.getStackFrames()) { - if (skipFrames > 0) { - skipFrames -= 1; - continue; - } - frames.add(new Frame(SourceCoordinate.create(f.getSourceSection()), - f.getName())); - } - return frames.toArray(new Frame[0]); - } - - /** - * @deprecated because it should be only implemented in {@link Suspension} but requires a few more changes - */ - @Deprecated - public static boolean isHaltPrimitive(final SuspendedEvent e) { - return e.getNode() instanceof HaltPrim; - } - - private static String getSuspendedSourceUri(final SuspendedEvent e) { - SourceSection suspendedSection = e.getSourceSection(); - assert suspendedSection != null : "TODO: what is this case, was supported before. Not sure whether the top frame has another source section"; - String sourceUri = suspendedSection.getSource().getURI().toString(); - return sourceUri; - } -} diff --git a/src/tools/debugger/message/SymbolMessage.java b/src/tools/debugger/message/SymbolMessage.java index f06634a1f..b2f92f0c0 100644 --- a/src/tools/debugger/message/SymbolMessage.java +++ b/src/tools/debugger/message/SymbolMessage.java @@ -5,17 +5,21 @@ import som.vmobjects.SSymbol; import tools.debugger.message.Message.OutgoingMessage; + +/** + * Message with a map to resolve symbol ids to their name. + */ public class SymbolMessage extends OutgoingMessage { private final String[] symbols; - private final int[] ids; + private final int[] ids; - public SymbolMessage(final ArrayList symbolstowrite) { - this.symbols = new String[symbolstowrite.size()]; - this.ids = new int[symbolstowrite.size()]; + public SymbolMessage(final ArrayList symbols) { + this.symbols = new String[symbols.size()]; + this.ids = new int[symbols.size()]; int i = 0; - for (SSymbol s : symbolstowrite) { - symbols[i] = s.getString(); + for (SSymbol s : symbols) { + this.symbols[i] = s.getString(); ids[i] = s.getSymbolId(); i++; } diff --git a/src/tools/debugger/message/ThreadsRequest.java b/src/tools/debugger/message/ThreadsRequest.java new file mode 100644 index 000000000..2a596e4d2 --- /dev/null +++ b/src/tools/debugger/message/ThreadsRequest.java @@ -0,0 +1,19 @@ +package tools.debugger.message; + +import org.java_websocket.WebSocket; + +import tools.debugger.FrontendConnector; +import tools.debugger.message.Message.Request; + + +public class ThreadsRequest extends Request { + + ThreadsRequest(final int requestId) { + super(requestId); + } + + @Override + public void process(final FrontendConnector connector, final WebSocket conn) { + connector.sendThreads(requestId); + } +} diff --git a/src/tools/debugger/message/ThreadsResponse.java b/src/tools/debugger/message/ThreadsResponse.java new file mode 100644 index 000000000..59a44af74 --- /dev/null +++ b/src/tools/debugger/message/ThreadsResponse.java @@ -0,0 +1,39 @@ +package tools.debugger.message; + +import java.util.Map; +import java.util.Map.Entry; + +import som.vm.Activity; +import tools.debugger.message.Message.Response; + + +@SuppressWarnings("unused") +public final class ThreadsResponse extends Response { + private final Thread[] threads; + + private ThreadsResponse(final Thread[] threads, final int requestId) { + super(requestId); + this.threads = threads; + } + + private static class Thread { + private final int id; + private final String name; + + Thread(final int id, final String name) { + this.id = id; + this.name = name; + } + } + + public static ThreadsResponse create(final Map activities, + final int requestId) { + Thread[] arr = new Thread[activities.size()]; + int i = 0; + for (Entry e : activities.entrySet()) { + arr[i] = new Thread(e.getValue(), e.getKey().getName()); + i += 1; + } + return new ThreadsResponse(arr, requestId); + } +} diff --git a/src/tools/debugger/message/VariablesResponse.java b/src/tools/debugger/message/VariablesResponse.java index 325ebe130..5cb7e8c4a 100644 --- a/src/tools/debugger/message/VariablesResponse.java +++ b/src/tools/debugger/message/VariablesResponse.java @@ -19,9 +19,12 @@ @SuppressWarnings("unused") public final class VariablesResponse extends Response { private final Variable[] variables; + private final int variablesReference; - private VariablesResponse(final int requestId, final Variable[] variables) { + private VariablesResponse(final int requestId, final int variablesReference, + final Variable[] variables) { super(requestId); + this.variablesReference = variablesReference; this.variables = variables; } @@ -52,7 +55,7 @@ public static VariablesResponse create(final int varRef, final int requestId, } else { results = createFromObject(scopeOrObject, suspension); } - return new VariablesResponse(requestId, results.toArray(new Variable[0])); + return new VariablesResponse(requestId, varRef, results.toArray(new Variable[0])); } private static ArrayList createFromObject(final Object obj, diff --git a/tests/debugger/JsonTests.java b/tests/debugger/JsonTests.java index ff2fc113a..2e30e9718 100644 --- a/tests/debugger/JsonTests.java +++ b/tests/debugger/JsonTests.java @@ -127,7 +127,7 @@ public void asyncMessageRcvBreakpointSerialize() { assertEquals(ASYNC_MSG_RCV_BP, result); } - private static final String EMPTY_INITAL_BP = "{\"breakpoints\":[],\"debuggerProtocol\":false,\"action\":\"initialBreakpoints\"}"; + private static final String EMPTY_INITAL_BP = "{\"breakpoints\":[],\"action\":\"initialBreakpoints\"}"; @Test public void initialBreakpointsMessageEmptySerialize() { diff --git a/tools/kompos/index.html b/tools/kompos/index.html index e07c921c0..c2e525eb1 100644 --- a/tools/kompos/index.html +++ b/tools/kompos/index.html @@ -237,6 +237,9 @@ + + + diff --git a/tools/kompos/src/controller.ts b/tools/kompos/src/controller.ts index 12b30a4f5..62ceac0c7 100644 --- a/tools/kompos/src/controller.ts +++ b/tools/kompos/src/controller.ts @@ -1,198 +1,33 @@ -/* jshint -W097 */ -"use strict"; +import { SourceMessage, StoppedMessage, SymbolMessage, SectionBreakpointType, + StackTraceResponse, ScopesResponse, ThreadsResponse, + VariablesResponse } from "./messages"; +import { VmConnection } from "./vm-connection"; -import {Debugger} from "./debugger"; -import {SourceMessage, SuspendEventMessage, SymbolMessage, - SectionBreakpointType} from "./messages"; -import {LineBreakpoint, MessageBreakpoint, - createLineBreakpoint, createMsgBreakpoint} from "./breakpoints"; -import {dbgLog} from "./source"; -import {displayMessageHistory, resetLinks, updateStrings, updateData} from "./visualizations"; -import {View} from "./view"; -import {VmConnection} from "./vm-connection"; - -/** - * The controller binds the domain model and the views, and mediates their - * interaction. - */ +/** A basic controller, providing an interface, but not providing any behavior. */ export class Controller { - private dbg: Debugger; - private view: View; - private vmConnection: VmConnection; + protected readonly vmConnection: VmConnection; - constructor(dbg, view, vmConnection: VmConnection) { - this.dbg = dbg; - this.view = view; + constructor(vmConnection: VmConnection) { this.vmConnection = vmConnection; - - vmConnection.setController(this); + this.vmConnection.setController(this); } - toggleConnection() { - if (this.vmConnection.isConnected()) { - this.vmConnection.disconnect(); - } else { - this.vmConnection.connect(); - } - } + public onConnect() {} + public onClose() {} + public onError() {} - onConnect() { - dbgLog("[WS] open"); - resetLinks(); - this.dbg.setResumed(); - this.view.onConnect(); - const bps = this.dbg.getEnabledBreakpoints(); - dbgLog("Send breakpoints: " + bps.length); - this.vmConnection.sendInitialBreakpoints(bps); - } + public onReceivedSource(_msg: SourceMessage) {} + public onStoppedEvent(_msg: StoppedMessage) {} + public onSymbolMessage(_msg: SymbolMessage) {} + public onStackTrace(_msg: StackTraceResponse) {} + public onScopes(_msg: ScopesResponse) {} + public onThreads(_msg: ThreadsResponse) {} + public onVariables(_msg: VariablesResponse) {} + public onUnknownMessage(_msg: any) {} - onClose() { - dbgLog("[WS] close"); - this.view.onClose(); - } - - onError() { - dbgLog("[WS] error"); - } + public onTracingData(_data: DataView) {} - onReceivedSource(msg: SourceMessage) { - let newSources = this.dbg.addSources(msg); - this.view.displaySources(newSources); - - for (let source of msg.sources) { - const bps = this.dbg.getEnabledBreakpointsForSource(source.name); - for (const bp of bps) { - switch (bp.data.type) { - case "LineBreakpoint": - this.view.updateLineBreakpoint( bp); - break; - case "MessageSenderBreakpoint": - case "MessageReceiverBreakpoint": - this.view.updateSendBreakpoint( bp); - break; - case "AsyncMessageReceiverBreakpoint": - this.view.updateAsyncMethodRcvBreakpoint( bp); - break; - case "PromiseResolverBreakpoint" || "PromiseResolutionBreakpoint": - this.view.updatePromiseBreakpoint( bp); - break; - default: - console.error("Unsupported breakpoint type: " + JSON.stringify(bp.data)); - break; - } - } - } - } - - onExecutionSuspension(msg: SuspendEventMessage) { - this.dbg.setSuspended(msg.id); - this.view.switchDebuggerToSuspendedState(); - this.view.displaySuspendEvent( - msg, this.dbg.getSourceId(msg.stack[0].sourceSection.uri)); - } - - onSymbolMessage(msg: SymbolMessage) { - updateStrings(msg); - } - - onTracingData(data: DataView) { - updateData(data); - displayMessageHistory(); - } - - onUnknownMessage(msg: any) { - dbgLog("[WS] unknown message of type:" + msg.type); - } - - private toggleBreakpoint(key, newBp) { - const sourceId = this.view.getActiveSourceId(); - const source = this.dbg.getSource(sourceId); - - let breakpoint = this.dbg.getBreakpoint(source, key, newBp); - breakpoint.toggle(); - - this.vmConnection.updateBreakpoint(breakpoint); - return breakpoint; - } - - onToggleLineBreakpoint(line: number, clickedSpan) { - dbgLog("updateBreakpoint"); - - let dbg = this.dbg, - breakpoint = this.toggleBreakpoint(line, - function (source) { return createLineBreakpoint(source, - dbg.getSourceId(source.uri), line, clickedSpan); }); - - this.view.updateLineBreakpoint( breakpoint); - } - - onToggleSendBreakpoint(sectionId: string, type: SectionBreakpointType) { - dbgLog("send-op breakpoint: " + type); - - let id = sectionId + ":" + type, - sourceSection = this.dbg.getSection(sectionId), - breakpoint = this.toggleBreakpoint(id, function (source) { - return createMsgBreakpoint(source, sourceSection, sectionId, type); }); - - this.view.updateSendBreakpoint( breakpoint); - } - - onToggleMethodAsyncRcvBreakpoint(sectionId: string) { - dbgLog("async method rcv bp: " + sectionId); - - const id = sectionId + ":async-rcv", - sourceSection = this.dbg.getSection(sectionId), - breakpoint = this.toggleBreakpoint(id, function (source) { - return createMsgBreakpoint(source, sourceSection, sectionId, "AsyncMessageReceiverBreakpoint"); }); - - this.view.updateAsyncMethodRcvBreakpoint( breakpoint); - } - - onTogglePromiseBreakpoint(sectionId: string, type: SectionBreakpointType) { - dbgLog("promise breakpoint: " + type); - - let id = sectionId + ":" + type, - sourceSection = this.dbg.getSection(sectionId), - breakpoint = this.toggleBreakpoint(id, function (source) { - return createMsgBreakpoint(source, sourceSection, sectionId, type); }); - - this.view.updatePromiseBreakpoint( breakpoint); - } - - resumeExecution() { - if (!this.dbg.isSuspended()) { return; } - this.dbg.setResumed(); - this.vmConnection.sendDebuggerAction("resume", this.dbg.lastSuspendEventId); - this.view.onContinueExecution(); - } - - pauseExecution() { - if (this.dbg.isSuspended()) { return; } - // TODO - } - - stopExecution() { - // TODO - } - - stepInto() { - if (!this.dbg.isSuspended()) { return; } - this.dbg.setResumed(); - this.view.onContinueExecution(); - this.vmConnection.sendDebuggerAction("stepInto", this.dbg.lastSuspendEventId); - } - - stepOver() { - if (!this.dbg.isSuspended()) { return; } - this.dbg.setResumed(); - this.view.onContinueExecution(); - this.vmConnection.sendDebuggerAction("stepOver", this.dbg.lastSuspendEventId); - } - - returnFromExecution() { - if (!this.dbg.isSuspended()) { return; } - this.dbg.setResumed(); - this.view.onContinueExecution(); - this.vmConnection.sendDebuggerAction("return", this.dbg.lastSuspendEventId); - } + public onToggleSendBreakpoint(_sectionId: string, _type: SectionBreakpointType) {} + public onToggleMethodAsyncRcvBreakpoint(_sectionId: string) {} + public onTogglePromiseBreakpoint(_sectionId: string, _type: SectionBreakpointType) {} } diff --git a/tools/kompos/src/debugger.ts b/tools/kompos/src/debugger.ts index d12b2260e..0c6d76f8f 100644 --- a/tools/kompos/src/debugger.ts +++ b/tools/kompos/src/debugger.ts @@ -9,7 +9,7 @@ export function isRelevant(sc: TaggedSourceCoordinate) { } export class Debugger { - public lastSuspendEventId?: string; + public activityId?: number; private suspended: boolean; @@ -33,7 +33,6 @@ export class Debugger { constructor() { this.suspended = false; - this.lastSuspendEventId = null; this.uriToSourceId = {}; this.sources = {}; this.sections = {}; @@ -127,10 +126,10 @@ export class Debugger { return bps; } - setSuspended(eventId) { + setSuspended(activityId: number) { console.assert(!this.suspended); - this.suspended = true; - this.lastSuspendEventId = eventId; + this.suspended = true; + this.activityId = activityId; } setResumed() { diff --git a/tools/kompos/src/main.js b/tools/kompos/src/main.js index 46749f4ee..8da4d6415 100644 --- a/tools/kompos/src/main.js +++ b/tools/kompos/src/main.js @@ -12,12 +12,12 @@ requirejs.config({ var ctrl; requirejs( - ['vm-connection', 'controller', 'debugger', 'view', 'visualizations', - 'breakpoints'], - function(vmConn, cont, d, vw) { + ['vm-connection', 'controller', 'ui-controller', 'debugger', 'view', + 'visualizations', 'breakpoints'], + function(vmConn, cont, uiCont, d, vw) { var view = new vw.View(), - vmConnection = new vmConn.VmConnection(), + vmConnection = new vmConn.VmConnection(true), dbg = new d.Debugger(); - ctrl = new cont.Controller(dbg, view, vmConnection); + ctrl = new uiCont.UiController(dbg, view, vmConnection); ctrl.toggleConnection(); }); diff --git a/tools/kompos/src/messages.ts b/tools/kompos/src/messages.ts index 8ef283a79..8f3ac1e16 100644 --- a/tools/kompos/src/messages.ts +++ b/tools/kompos/src/messages.ts @@ -39,36 +39,15 @@ export interface Method { sourceSection: SourceCoordinate; } -export interface Frame { - sourceSection: FullSourceCoordinate; - methodName: string; -} - -export interface TopFrame { - arguments: string[]; - slots: IdMap; -} - -export type Message = SourceMessage | SuspendEventMessage | +export type Message = SourceMessage | SymbolMessage | UpdateSourceSections | StoppedMessage | - StackTraceResponse | ScopesResponse; + StackTraceResponse | ScopesResponse | ThreadsResponse | VariablesResponse; export interface SourceMessage { type: "source"; sources: Source[]; } -export interface SuspendEventMessage { - type: "suspendEvent"; - - /** id of SuspendEvent, to be recognized in backend. */ - id: string; - sourceUri: string; - - stack: Frame[]; - topFrame: TopFrame; -} - export type StoppedReason = "step" | "breakpoint" | "exception" | "pause"; export type ActivityType = "Actor"; @@ -93,7 +72,7 @@ export interface SourceInfo { } export interface SymbolMessage { - type: "symbolMessage"; + type: "SymbolMessage"; symbols: string[]; ids: number[]; } @@ -145,16 +124,12 @@ export function createSectionBreakpointData(sourceUri: string, line: number, } export type Respond = InitialBreakpointsResponds | UpdateBreakpoint | - StepMessage | StackTraceRequest | ScopesRequest | VariablesRequest; + StepMessage | StackTraceRequest | ScopesRequest | VariablesRequest | + ThreadsRequest; export interface InitialBreakpointsResponds { action: "initialBreakpoints"; breakpoints: BreakpointData[]; - - /** - * Use a VSCode-like debugger protocol. - */ - debuggerProtocol: boolean; } interface UpdateBreakpoint { @@ -166,9 +141,25 @@ export type StepType = "stepInto" | "stepOver" | "return" | "resume" | "stop"; export interface StepMessage { action: StepType; - // TODO: should be renamed to suspendEventId - /** Id of the corresponding suspend event. */ - suspendEvent: string; + + /** Id of the suspended activity. */ + activityId: number; +} + +export interface ThreadsRequest { + action: "ThreadsRequest"; + requestId: number; +} + +export interface Thread { + id: number; + name: string; +} + +export interface ThreadsResponse { + type: "ThreadsResponse"; + requestId: number; + threads: Thread[]; } export interface StackTraceRequest { @@ -200,8 +191,11 @@ export interface StackFrame { /** Optional, end line of the range covered by the stack frame. */ endLine: number; - /** Optional end column of the range covered by the stack frame. */ + /** Optional, end column of the range covered by the stack frame. */ endColumn: number; + + /** Optional, number of characters in the range */ + length: number; } export interface StackTraceResponse { @@ -248,6 +242,7 @@ export interface VariablesRequest { export interface VariablesResponse { type: "VariablesResponse"; variables: Variable[]; + variablesReference: number; requestId: number; } diff --git a/tools/kompos/src/ui-controller.ts b/tools/kompos/src/ui-controller.ts new file mode 100644 index 000000000..3796b3fb9 --- /dev/null +++ b/tools/kompos/src/ui-controller.ts @@ -0,0 +1,213 @@ +/* jshint -W097 */ +"use strict"; + +import {Controller} from "./controller"; +import {Debugger} from "./debugger"; +import {SourceMessage, SymbolMessage, StoppedMessage, StackTraceResponse, + SectionBreakpointType, ScopesResponse, VariablesResponse } from "./messages"; +import {LineBreakpoint, MessageBreakpoint, + createLineBreakpoint, createMsgBreakpoint} from "./breakpoints"; +import {dbgLog} from "./source"; +import {displayMessageHistory, resetLinks, updateStrings, updateData} from "./visualizations"; +import {View} from "./view"; +import {VmConnection} from "./vm-connection"; + +/** + * The controller binds the domain model and the views, and mediates their + * interaction. + */ +export class UiController extends Controller { + private dbg: Debugger; + private view: View; + + constructor(dbg, view, vmConnection: VmConnection) { + super(vmConnection); + this.dbg = dbg; + this.view = view; + } + + toggleConnection() { + if (this.vmConnection.isConnected()) { + this.vmConnection.disconnect(); + } else { + this.vmConnection.connect(); + } + } + + onConnect() { + dbgLog("[WS] open"); + resetLinks(); + this.dbg.setResumed(); + this.view.onConnect(); + const bps = this.dbg.getEnabledBreakpoints(); + dbgLog("Send breakpoints: " + bps.length); + this.vmConnection.sendInitialBreakpoints(bps.map(b => b.data)); + } + + onClose() { + dbgLog("[WS] close"); + this.view.onClose(); + } + + onError() { + dbgLog("[WS] error"); + } + + onReceivedSource(msg: SourceMessage) { + let newSources = this.dbg.addSources(msg); + this.view.displaySources(newSources); + + for (let source of msg.sources) { + const bps = this.dbg.getEnabledBreakpointsForSource(source.name); + for (const bp of bps) { + switch (bp.data.type) { + case "LineBreakpoint": + this.view.updateLineBreakpoint( bp); + break; + case "MessageSenderBreakpoint": + case "MessageReceiverBreakpoint": + this.view.updateSendBreakpoint( bp); + break; + case "AsyncMessageReceiverBreakpoint": + this.view.updateAsyncMethodRcvBreakpoint( bp); + break; + case "PromiseResolverBreakpoint" || "PromiseResolutionBreakpoint": + this.view.updatePromiseBreakpoint( bp); + break; + default: + console.error("Unsupported breakpoint type: " + JSON.stringify(bp.data)); + break; + } + } + } + } + + public onStoppedEvent(msg: StoppedMessage) { + this.dbg.setSuspended(msg.activityId); + this.vmConnection.requestActivityList(); + this.vmConnection.requestStackTrace(msg.activityId); + } + + public onStackTrace(msg: StackTraceResponse) { + this.vmConnection.requestScope(msg.stackFrames[0].id); + this.view.switchDebuggerToSuspendedState(); + this.view.displayStackTrace( + msg, this.dbg.getSourceId(msg.stackFrames[0].sourceUri)); + } + + public onScopes(msg: ScopesResponse) { + for (let s of msg.scopes) { + this.vmConnection.requestVariables(s.variablesReference); + this.view.displayScope(s); + } + } + + public onVariables(msg: VariablesResponse) { + this.view.displayVariables(msg.variablesReference, msg.variables); + } + + onSymbolMessage(msg: SymbolMessage) { + updateStrings(msg); + } + + onTracingData(data: DataView) { + updateData(data); + displayMessageHistory(); + } + + onUnknownMessage(msg: any) { + dbgLog("[WS] unknown message of type:" + msg.type); + } + + private toggleBreakpoint(key, newBp) { + const sourceId = this.view.getActiveSourceId(); + const source = this.dbg.getSource(sourceId); + + let breakpoint = this.dbg.getBreakpoint(source, key, newBp); + breakpoint.toggle(); + + this.vmConnection.updateBreakpoint(breakpoint.data); + return breakpoint; + } + + onToggleLineBreakpoint(line: number, clickedSpan) { + dbgLog("updateBreakpoint"); + + let dbg = this.dbg, + breakpoint = this.toggleBreakpoint(line, + function (source) { return createLineBreakpoint(source, + dbg.getSourceId(source.uri), line, clickedSpan); }); + + this.view.updateLineBreakpoint( breakpoint); + } + + onToggleSendBreakpoint(sectionId: string, type: SectionBreakpointType) { + dbgLog("send-op breakpoint: " + type); + + let id = sectionId + ":" + type, + sourceSection = this.dbg.getSection(sectionId), + breakpoint = this.toggleBreakpoint(id, function (source) { + return createMsgBreakpoint(source, sourceSection, sectionId, type); }); + + this.view.updateSendBreakpoint( breakpoint); + } + + onToggleMethodAsyncRcvBreakpoint(sectionId: string) { + dbgLog("async method rcv bp: " + sectionId); + + const id = sectionId + ":async-rcv", + sourceSection = this.dbg.getSection(sectionId), + breakpoint = this.toggleBreakpoint(id, function (source) { + return createMsgBreakpoint(source, sourceSection, sectionId, "AsyncMessageReceiverBreakpoint"); }); + + this.view.updateAsyncMethodRcvBreakpoint( breakpoint); + } + + onTogglePromiseBreakpoint(sectionId: string, type: SectionBreakpointType) { + dbgLog("promise breakpoint: " + type); + + let id = sectionId + ":" + type, + sourceSection = this.dbg.getSection(sectionId), + breakpoint = this.toggleBreakpoint(id, function (source) { + return createMsgBreakpoint(source, sourceSection, sectionId, type); }); + + this.view.updatePromiseBreakpoint( breakpoint); + } + + resumeExecution() { + if (!this.dbg.isSuspended()) { return; } + this.dbg.setResumed(); + this.vmConnection.sendDebuggerAction("resume", this.dbg.activityId); + this.view.onContinueExecution(); + } + + pauseExecution() { + if (this.dbg.isSuspended()) { return; } + // TODO + } + + stopExecution() { + // TODO + } + + stepInto() { + if (!this.dbg.isSuspended()) { return; } + this.dbg.setResumed(); + this.view.onContinueExecution(); + this.vmConnection.sendDebuggerAction("stepInto", this.dbg.activityId); + } + + stepOver() { + if (!this.dbg.isSuspended()) { return; } + this.dbg.setResumed(); + this.view.onContinueExecution(); + this.vmConnection.sendDebuggerAction("stepOver", this.dbg.activityId); + } + + returnFromExecution() { + if (!this.dbg.isSuspended()) { return; } + this.dbg.setResumed(); + this.view.onContinueExecution(); + this.vmConnection.sendDebuggerAction("return", this.dbg.activityId); + } +} diff --git a/tools/kompos/src/view.ts b/tools/kompos/src/view.ts index 584fb7317..1a354b13f 100644 --- a/tools/kompos/src/view.ts +++ b/tools/kompos/src/view.ts @@ -2,8 +2,8 @@ "use strict"; import {Controller} from "./controller"; -import {Source, Method, SuspendEventMessage, IdMap, Frame, SourceCoordinate, - TaggedSourceCoordinate, getSectionId} from "./messages"; +import {Source, Method, IdMap, StackFrame, SourceCoordinate, StackTraceResponse, + TaggedSourceCoordinate, Scope, getSectionId, Variable} from "./messages"; import {Breakpoint, MessageBreakpoint, LineBreakpoint} from "./breakpoints"; declare var ctrl: Controller; @@ -404,19 +404,10 @@ function showSource(source: Source, sourceId: string) { files.appendChild(newFileElement); } -function showVar(name: string, value: string, list: Element) { - const entry = nodeFromTemplate("frame-state-tpl"); - let t = $(entry).find("th"); - t.html(name); - t = $(entry).find("td"); - t.html(value); - list.appendChild(entry); -} - -function showFrame(frame: Frame, i: number, list: Element) { - let stackEntry = frame.methodName; - if (frame.sourceSection) { - stackEntry += ":" + frame.sourceSection.startLine + ":" + frame.sourceSection.startColumn; +function showFrame(frame: StackFrame, i: number, list: Element) { + let stackEntry = frame.name; + if (frame.line) { + stackEntry += ":" + frame.line + ":" + frame.column; } const entry = nodeFromTemplate("stack-frame-tpl"); entry.setAttribute("id", "frame-" + i); @@ -481,14 +472,47 @@ export class View { enableMethodBreakpointHover(sourceFile); } - displaySuspendEvent(data: SuspendEventMessage, sourceId: string) { + private getScopeId(varRef: number) { + return "scope:" + varRef; + } + + public displayScope(s: Scope) { + const list = document.getElementById("frame-state"); + const entry = nodeFromTemplate("scope-head-tpl"); + entry.id = this.getScopeId(s.variablesReference); + let t = $(entry).find("th"); + t.html(s.name); + list.appendChild(entry); + } + + private createVarElement(name: string, value: string, varRef: number): Element { + const entry = nodeFromTemplate("frame-state-tpl"); + entry.id = this.getScopeId(varRef); + let t = $(entry).find("th"); + t.html(name); + t = $(entry).find("td"); + t.html(value); + return entry; + } + + public displayVariables(varRef: number, vars: Variable[]) { + const scopeEntry = document.getElementById(this.getScopeId(varRef)); + + for (const v of vars) { + scopeEntry.insertAdjacentElement( + "afterend", + this.createVarElement(v.name, v.value, v.variablesReference)); + } + } + + displayStackTrace(data: StackTraceResponse, sourceId: string) { let list = document.getElementById("stack-frames"); while (list.lastChild) { list.removeChild(list.lastChild); } - for (let i = 0; i < data.stack.length; i++) { - showFrame(data.stack[i], i, list); + for (let i = 0; i < data.stackFrames.length; i++) { + showFrame(data.stackFrames[i], i, list); } list = document.getElementById("frame-state"); @@ -496,14 +520,13 @@ export class View { list.removeChild(list.lastChild); } - showVar("Arguments", data.topFrame.arguments.join(", "), list); - - for (const varName in data.topFrame.slots) { - showVar(varName, data.topFrame.slots[varName], list); - } + const line = data.stackFrames[0].line, + column = data.stackFrames[0].column, + length = data.stackFrames[0].length; // highlight current node - let ssId = getSectionId(sourceId, data.stack[0].sourceSection); + let ssId = getSectionId(sourceId, + {startLine: line, startColumn: column, charLength: length}); let ss = document.getElementById(ssId); $(ss).addClass("DbgCurrentNode"); diff --git a/tools/kompos/src/vm-connection.ts b/tools/kompos/src/vm-connection.ts index fccb68d8b..49e7a94b1 100644 --- a/tools/kompos/src/vm-connection.ts +++ b/tools/kompos/src/vm-connection.ts @@ -4,25 +4,30 @@ import * as WebSocket from "ws"; import {Controller} from "./controller"; -import {Message, Respond} from "./messages"; -import {Breakpoint} from "./breakpoints"; +import {Message, Respond, StepType, BreakpointData} from "./messages"; + +const DBG_PORT = 7977; +const TRACE_PORT = 7978; +const LOCAL_WS_URL = "ws://localhost"; /** * Encapsulates the connection to the VM via a web socket and encodes * the communication protocol, currently using JSON. */ export class VmConnection { - private socket: WebSocket; - private binarySocket: WebSocket; - private controller: Controller; - - constructor() { - this.socket = null; - this.binarySocket = null; - this.controller = null; + private socket: WebSocket; + private traceDataSocket: WebSocket; + private readonly useTraceData: boolean; + private controller: Controller; + + constructor(useTraceData: boolean) { + this.socket = null; + this.traceDataSocket = null; + this.controller = null; + this.useTraceData = useTraceData; } - setController(controller: Controller) { + public setController(controller: Controller) { this.controller = controller; } @@ -30,75 +35,133 @@ export class VmConnection { return this.socket !== null && this.socket.readyState === WebSocket.OPEN; } - connect() { - console.assert(this.socket === null || this.socket.readyState === WebSocket.CLOSED); - this.socket = new WebSocket("ws://localhost:7977"); + private connectTraceDataSocket() { + if (!this.useTraceData) { return; } - console.assert(this.binarySocket === null || this.binarySocket.readyState === WebSocket.CLOSED); - this.binarySocket = new WebSocket("ws://localhost:7978"); - ( this.binarySocket).binaryType = "arraybuffer"; // workaround, typescript dosn't recognize this property + console.assert(this.traceDataSocket === null || this.traceDataSocket.readyState === WebSocket.CLOSED); + this.traceDataSocket = new WebSocket(LOCAL_WS_URL + ":" + TRACE_PORT); + ( this.traceDataSocket).binaryType = "arraybuffer"; // workaround, typescript dosn't recognize this property const controller = this.controller; - this.socket.onopen = function () { - controller.onConnect(); + this.traceDataSocket.onmessage = function (e) { + const data: DataView = new DataView(e.data); + controller.onTracingData(data); + }; + } + + protected onOpen() { + if (!this.controller) { return; } + this.controller.onConnect(); + } + + connect() { + console.assert(this.socket === null || this.socket.readyState === WebSocket.CLOSED); + this.socket = new WebSocket(LOCAL_WS_URL + ":" + DBG_PORT); + const ctrl = this.controller; + + this.socket.onopen = () => { + this.onOpen(); }; - this.socket.onclose = function () { - controller.onClose(); + this.socket.onclose = () => { + if (!ctrl) { return; } + ctrl.onClose(); }; - this.socket.onerror = function () { - controller.onError(); + this.socket.onerror = () => { + if (!ctrl) { return; } + ctrl.onError(); }; - this.socket.onmessage = function (e) { + this.socket.onmessage = (e) => { + if (!ctrl) { return; } + const data: Message = JSON.parse(e.data); switch (data.type) { case "source": - controller.onReceivedSource(data); + ctrl.onReceivedSource(data); + break; + case "StoppedEvent": + ctrl.onStoppedEvent(data); + break; + case "SymbolMessage": + ctrl.onSymbolMessage(data); + break; + case "StackTraceResponse": + ctrl.onStackTrace(data); + break; + case "ScopesResponse": + ctrl.onScopes(data); break; - case "suspendEvent": - controller.onExecutionSuspension(data); + case "ThreadsResponse": + ctrl.onThreads(data); break; - case "symbolMessage": - controller.onSymbolMessage(data); + case "VariablesResponse": + ctrl.onVariables(data); break; default: - controller.onUnknownMessage(data); + ctrl.onUnknownMessage(data); break; } }; - this.binarySocket.onmessage = function (e) { - const data: DataView = new DataView(e.data); - controller.onTracingData(data); - }; + this.connectTraceDataSocket(); } disconnect() { console.assert(this.isConnected()); } - sendInitialBreakpoints(breakpoints: Breakpoint[]) { + sendInitialBreakpoints(breakpoints: BreakpointData[]) { this.send({ action: "initialBreakpoints", - breakpoints: breakpoints.map(b => b.data), - debuggerProtocol: false + breakpoints: breakpoints }); } - updateBreakpoint(breakpoint: Breakpoint) { + updateBreakpoint(breakpoint: BreakpointData) { this.send({ action: "updateBreakpoint", - breakpoint: breakpoint.data + breakpoint: breakpoint }); }; - sendDebuggerAction(action, lastSuspendEventId) { + sendDebuggerAction(action: StepType, activityId: number) { this.send({ action: action, - suspendEvent: lastSuspendEventId}); + activityId: activityId}); + } + + public requestActivityList() { + // requestId is currently only used in VS code adapter + this.send({action: "ThreadsRequest", requestId: 0}); + } + + public requestStackTrace(activityId: number) { + this.send({ + action: "StackTraceRequest", + activityId: activityId, + startFrame: 0, // from the top + levels: 0, // request all + requestId: 0 // only used in VS code adapter currently + }); + } + + public requestScope(scopeId: number) { + this.send({ + action: "ScopesRequest", + frameId: scopeId, + requestId: 0 // only used in VS code + }); + } + + public requestVariables(variablesReference: number) { + this.send({ + action: "VariablesRequest", + variablesReference: variablesReference, + requestId: 0 // only used in VS code + }); } private send(respond: Respond) { diff --git a/tools/kompos/tests/basic-protocol.ts b/tools/kompos/tests/basic-protocol.ts index b7f98e168..e668639f9 100644 --- a/tools/kompos/tests/basic-protocol.ts +++ b/tools/kompos/tests/basic-protocol.ts @@ -4,12 +4,12 @@ import { expect } from "chai"; import * as fs from "fs"; import {X_OK} from "constants"; -import { SomConnection, closeConnection, SOM, OnMessageEvent, PING_PONG_URI, - expectStack, expectSourceCoordinate, expectFullSourceCoordinate, - HandleFirstSuspendEvent, startSomAndConnect, send } from "./test-setup"; +import { SOM, PING_PONG_URI, ControllerWithInitialBreakpoints, + TestConnection, HandleStoppedAndGetStackTrace, + expectStack, expectSourceCoordinate } from "./test-setup"; -import {SourceMessage, SuspendEventMessage, - StepMessage, createLineBreakpointData, createSectionBreakpointData} from "../src/messages"; +import { SourceMessage, createLineBreakpointData, + createSectionBreakpointData } from "../src/messages"; let connectionPossible = false; function onlyWithConnection(fn) { @@ -32,12 +32,12 @@ describe("Basic Project Setup", () => { }); it("should be possible to connect", done => { - const connectionP = startSomAndConnect(); - connectionP.then(connection => { - closeConnection(connection, done); + const conn = new TestConnection(); + conn.fullyConnected.then(_ => { + conn.close(done); connectionPossible = true; }); - connectionP.catch(reason => { + conn.fullyConnected.catch(reason => { done(reason); }); }); @@ -45,29 +45,27 @@ describe("Basic Project Setup", () => { }); describe("Basic Protocol", function() { - let connectionP: Promise = null; + let conn: TestConnection; + const closeConnectionAfterSuite = (done) => { - connectionP.then(c => { closeConnection(c, done); }); - connectionP.catch(reason => done(reason)); + conn.fullyConnected.then(_ => { conn.close(done); }); + conn.fullyConnected.catch(reason => done(reason)); }; describe("source message", () => { - // Capture first source message for testing - let firstSourceCaptured = false; - let getSourceData: (event: OnMessageEvent) => void; - let sourceP = new Promise((resolve, _reject) => { - getSourceData = (event: OnMessageEvent) => { - if (firstSourceCaptured) { return; } - const data = JSON.parse(event.data); - if (data.type === "source") { - firstSourceCaptured = true; - resolve(data); - } - }; - }); + let sourceP: Promise; before("Start SOMns and Connect", () => { - connectionP = startSomAndConnect(getSourceData, []); + conn = new TestConnection(); + const ctrl = new ControllerWithInitialBreakpoints([], conn); + let firstSourceCaptured = false; + sourceP = new Promise((resolve, _reject) => { + ctrl.onReceivedSource = (msg: SourceMessage) => { + if (firstSourceCaptured) { return; }; + firstSourceCaptured = true; + resolve(msg); + }; + }); }); after(closeConnectionAfterSuite); @@ -112,221 +110,118 @@ describe("Basic Protocol", function() { })); }); - describe("setting a line breakpoint", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createLineBreakpointData(PING_PONG_URI, 70, true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept line breakpoint, and halt on expected line", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 7, "PingPong>>#benchmark", 70); - }); - })); - - it("should have a well structured suspended event", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectFullSourceCoordinate(msg.stack[0].sourceSection); - expect(msg.id).to.equal("se-0"); - expect(msg.sourceUri).to.equal(PING_PONG_URI); - expect(msg.topFrame.arguments[0]).to.equal("a PingPong"); - expect(msg.topFrame.slots["Local(ping)"]).to.equal("a Nil"); - }); - })); - }); - - describe("setting a source section sender breakpoint", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, - "MessageSenderBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept send breakpoint, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Ping>>#start", 16); - }); - })); - }); - - describe("setting a source section asynchronous method receiver breakpoint", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 57, 9, 88, - "AsyncMessageReceiverBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept async method receiver breakpoint, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Pong>>#ping:", 57); - }); - })); - }); - - describe("setting a source section receiver breakpoint", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, - "MessageReceiverBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept send breakpoint, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Pong>>#ping:", 57); - }); - })); - }); - - describe("setting a source section promise resolver breakpoint for normal resolution", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 73, 17, 3, - "PromiseResolverBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept promise resolver breakpoint, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Ping>>#start", 15); - }); - })); - }); - - describe("setting a source section promise resolution breakpoint for normal resolution", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 73, 17, 3, - "PromiseResolutionBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept promise resolution breakpoint, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Thing>>#println", 71); + const breakpointTests = [ + {suite: "setting a line breakpoint", + breakpoint: createLineBreakpointData(PING_PONG_URI, 70, true), + test: "should accept line breakpoint, and halt on expected line", + stackLength: 7, + topMethod: "PingPong>>#benchmark", + line: 70}, + + {suite: "setting a source section sender breakpoint", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, "MessageSenderBreakpoint", true), + test: "should accept send breakpoint, and halt on expected source section", + stackLength: 2, + topMethod: "Ping>>#start", + line: 16}, + + {suite: "setting a source section asynchronous method receiver breakpoint", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 57, 9, 88, "AsyncMessageReceiverBreakpoint", true), + test: "should accept async method receiver breakpoint, and halt on expected source section", + stackLength: 2, + topMethod: "Pong>>#ping:", + line: 57}, + + {suite: "setting a source section receiver breakpoint", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, "MessageReceiverBreakpoint", true), + test: "should accept send breakpoint, and halt on expected source section", + stackLength: 2, + topMethod: "Pong>>#ping:", + line: 57}, + + {suite: "setting a source section promise resolver breakpoint for normal resolution", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 73, 17, 3, "PromiseResolverBreakpoint", true), + test: "should accept promise resolver breakpoint, and halt on expected source section", + stackLength: 2, + topMethod: "Ping>>#start", + line: 15}, + + {suite: "setting a source section promise resolution breakpoint for normal resolution", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 73, 17, 3, "PromiseResolutionBreakpoint", true), + test: "should accept promise resolution breakpoint, and halt on expected source section", + stackLength: 2, + topMethod: "Thing>>#println", + line: 71}, + + {suite: "setting a source section promise resolver breakpoint for null resolution", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, "PromiseResolverBreakpoint", true), + test: "should accept promise resolver breakpoint for null resolution, and halt on expected source section", + stackLength: 2, + topMethod: "Pong>>#ping:", + line: 57}, + + {suite: "setting a source section promise resolver breakpoint for chained resolution", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 26, 20, 3, "PromiseResolverBreakpoint", true), + test: "should accept promise resolver breakpoint for chained resolution, and halt on expected source section", + stackLength: 2, + topMethod: "Ping>>#validate:", + line: 31}, + + {suite: "setting a source section promise resolution breakpoint for chained resolution", + breakpoint: createSectionBreakpointData(PING_PONG_URI, 26, 20, 3, "PromiseResolutionBreakpoint", true), + test: "should accept promise resolution breakpoint for chained resolution, and halt on expected source section", + stackLength: 2, + topMethod: "Ping>>#$blockMethod@27@33:", + line: 27} + ]; + + breakpointTests.forEach(desc => { + describe(desc.suite, () => { + let ctrl: HandleStoppedAndGetStackTrace; + + before("Start SOMns and Connect", () => { + conn = new TestConnection(); + ctrl = new HandleStoppedAndGetStackTrace([desc.breakpoint], conn); }); - })); - }); - describe("setting a source section promise resolver breakpoint for null resolution", () => { - const event = new HandleFirstSuspendEvent(); + after(closeConnectionAfterSuite); - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, - "PromiseResolverBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept promise resolver breakpoint for null resolution, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Pong>>#ping:", 57); - }); - })); - }); - - describe("setting a source section promise resolver breakpoint for chained resolution", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 26, 20, 3, - "PromiseResolverBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); - }); - - after(closeConnectionAfterSuite); - - it("should accept promise resolver breakpoint for chained resolution, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Ping>>#validate:", 31); - }); - })); - }); - - describe("setting a source section promise resolution breakpoint for chained resolution", () => { - const event = new HandleFirstSuspendEvent(); - - before("Start SOMns and Connect", () => { - const breakpoint = createSectionBreakpointData(PING_PONG_URI, 26, 20, 3, - "PromiseResolutionBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint]); + it(desc.test, onlyWithConnection(() => { + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, desc.stackLength, desc.topMethod, desc.line); + }); + })); }); - - after(closeConnectionAfterSuite); - - it("should accept promise resolution breakpoint for chained resolution, and halt on expected source section", onlyWithConnection(() => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 2, "Ping>>#$blockMethod@27@33:", 27); - }); - })); }); describe("stepping", () => { // Capture suspended events - const suspendPs: Promise[] = []; - const numSuspends = 4; - const resolves = []; - for (let i = 0; i < numSuspends; i++) { - suspendPs.push(new Promise((res, _rej) => resolves.push(res))); - } - - let capturedEvents = 0; - let getSuspendEvent: (event: OnMessageEvent) => void; - - getSuspendEvent = (event: OnMessageEvent) => { - if (capturedEvents > numSuspends) { return; } - const data = JSON.parse(event.data); - if (data.type === "suspendEvent") { - resolves[capturedEvents](data); - capturedEvents += 1; - } - }; + let ctrl: HandleStoppedAndGetStackTrace; before("Start SOMns and Connect", () => { const breakpoint = createSectionBreakpointData(PING_PONG_URI, 16, 14, 3, "MessageSenderBreakpoint", true); - connectionP = startSomAndConnect(getSuspendEvent, [breakpoint]); + conn = new TestConnection(); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, 4); }); after(closeConnectionAfterSuite); it("should stop initially at breakpoint", onlyWithConnection(() => { - return suspendPs[0].then(msg => { - expectStack(msg.stack, 2, "Ping>>#start", 16); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 2, "Ping>>#start", 16); }); })); it("should single stepping", onlyWithConnection(() => { return new Promise((resolve, _reject) => { - suspendPs[0].then(msg => { - const step: StepMessage = {action: "stepInto", suspendEvent: msg.id}; - connectionP.then(con => { - send(con.socket, step); }); + ctrl.stackP.then(_ => { + conn.fullyConnected.then(_ => { + conn.sendDebuggerAction("stepInto", ctrl.stoppedActivities[0]); + }); - const p = suspendPs[1].then(msgAfterStep => { - expectStack(msgAfterStep.stack, 2, "Ping>>#start", 17); + const p = ctrl.getStackP(1).then(msgAfterStep => { + expectStack(msgAfterStep.stackFrames, 2, "Ping>>#start", 17); }); resolve(p); }); @@ -336,33 +231,33 @@ describe("Basic Protocol", function() { it("should be possible to dynamically activate line breakpoints", onlyWithConnection(() => { return Promise.all([ - suspendPs[1].then(msgAfterStep => { - connectionP.then(con => { + ctrl.getStackP(1).then(_ => { + conn.fullyConnected.then(_ => { // set another breakpoint, after stepping, and with connection const lbp = createLineBreakpointData(PING_PONG_URI, 22, true); - send(con.socket, {action: "updateBreakpoint", breakpoint: lbp}); - send(con.socket, {action: "resume", suspendEvent: msgAfterStep.id}); + conn.updateBreakpoint(lbp); + conn.sendDebuggerAction("resume", ctrl.stoppedActivities[1]); }); }), - suspendPs[2].then(msgLineBP => { - expectStack(msgLineBP.stack, 2, "Ping>>#ping", 22); + ctrl.getStackP(2).then(msgLineBP => { + expectStack(msgLineBP.stackFrames, 2, "Ping>>#ping", 22); })]); })); it("should be possible to disable a line breakpoint", onlyWithConnection(() => { return new Promise((resolve, _reject) => { - suspendPs[2].then(msgAfterStep => { - connectionP.then(con => { + ctrl.getStackP(2).then(_ => { + conn.fullyConnected.then(_ => { const lbp22 = createLineBreakpointData(PING_PONG_URI, 23, true); - send(con.socket, {action: "updateBreakpoint", breakpoint: lbp22}); + conn.updateBreakpoint(lbp22); const lbp21 = createLineBreakpointData(PING_PONG_URI, 22, false); - send(con.socket, {action: "updateBreakpoint", breakpoint: lbp21}); - send(con.socket, {action: "resume", suspendEvent: msgAfterStep.id}); + conn.updateBreakpoint(lbp21); + conn.sendDebuggerAction("resume", ctrl.stoppedActivities[2]); - const p = suspendPs[3].then(msgLineBP => { - expectStack(msgLineBP.stack, 2, "Ping>>#ping", 23); + const p = ctrl.getStackP(3).then(msgLineBP => { + expectStack(msgLineBP.stackFrames, 2, "Ping>>#ping", 23); }); resolve(p); }); diff --git a/tools/kompos/tests/csp.ts b/tools/kompos/tests/csp.ts index c8995bfc1..737a9d13e 100644 --- a/tools/kompos/tests/csp.ts +++ b/tools/kompos/tests/csp.ts @@ -1,90 +1,86 @@ import { resolve } from "path"; -import { SomConnection, closeConnection, expectStack, - HandleFirstSuspendEvent, startSomAndConnect } from "./test-setup"; +import { HandleStoppedAndGetStackTrace, TestConnection, expectStack } from "./test-setup"; import {createSectionBreakpointData} from "../src/messages"; const CSP_FILE = resolve("tests/pingpong-csp.som"); const CSP_URI = "file:" + CSP_FILE; describe("Setting CSP Breakpoints", () => { - let connectionP: Promise = null; + let conn: TestConnection; + let ctrl: HandleStoppedAndGetStackTrace; + const closeConnectionAfterSuite = (done) => { - connectionP.then(c => { closeConnection(c, done); }); - connectionP.catch(reason => done(reason)); + conn.fullyConnected.then(_ => { conn.close(done); }); + conn.fullyConnected.catch(reason => done(reason)); }; describe("Setting Before Read Breakpoint", () => { - const event = new HandleFirstSuspendEvent(); before("Start SOMns and Connect", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 13, 12, 4, "MessageSenderBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint], - null, null, CSP_FILE); + conn = new TestConnection(["halt"], null, CSP_FILE); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); }); after(closeConnectionAfterSuite); it("should break on #read", () => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 1, "Ping>>#run", 13); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 1, "Ping>>#run", 13); }); }); }); describe("Setting After Write Breakpoint", () => { - const event = new HandleFirstSuspendEvent(); before("Start SOMns and Connect", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 13, 12, 4, "ChannelOppositeBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint], - null, null, CSP_FILE); + conn = new TestConnection(["halt"], null, CSP_FILE); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); }); after(closeConnectionAfterSuite); it("should break after #write:", () => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 6, "PingPongCSP>>#main:", 24); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 6, "PingPongCSP>>#main:", 24); }); }); }); describe("Setting Before Write Breakpoint", () => { - const event = new HandleFirstSuspendEvent(); - before("Start SOMns and Connect", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 12, 13, 12, "MessageSenderBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint], - null, null, CSP_FILE); + conn = new TestConnection(["halt"], null, CSP_FILE); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); }); after(closeConnectionAfterSuite); it("should break on #write:", () => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 1, "Ping>>#run", 12); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 1, "Ping>>#run", 12); }); }); }); describe("Setting After Read Breakpoint", () => { - const event = new HandleFirstSuspendEvent(); before("Start SOMns and Connect", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 12, 13, 12, "ChannelOppositeBreakpoint", true); - connectionP = startSomAndConnect(event.getSuspendEvent, [breakpoint], - null, null, CSP_FILE); + conn = new TestConnection(["halt"], null, CSP_FILE); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); }); after(closeConnectionAfterSuite); it("should break after #read", () => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 6, "PingPongCSP>>#main:", 23); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 6, "PingPongCSP>>#main:", 23); }); }); }); diff --git a/tools/kompos/tests/som-integration.ts b/tools/kompos/tests/som-integration.ts index 0b2a5cc75..b008b50ea 100644 --- a/tools/kompos/tests/som-integration.ts +++ b/tools/kompos/tests/som-integration.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { spawn } from "child_process"; -import { SomConnection, closeConnection, SOM, execSom, HandleFirstSuspendEvent, - startSomAndConnect, expectStack } from "./test-setup"; +import { SOM, HandleStoppedAndGetStackTrace, TestConnection, execSom, + expectStack } from "./test-setup"; describe("Command-line Behavior", function() { @@ -44,25 +44,25 @@ ERROR: MessageNotUnderstood(Integer>>#foobar)\n"); }); describe("Language Debugger Integration", function() { + let conn: TestConnection; + let ctrl: HandleStoppedAndGetStackTrace; - let connectionP: Promise = null; const closeConnectionAfterSuite = (done) => { - connectionP.then(c => { closeConnection(c, done); }); - connectionP.catch(reason => done(reason)); + conn.fullyConnected.then(_ => { conn.close(done); }); + conn.fullyConnected.catch(reason => done(reason)); }; describe("execute `1 halt` and get suspended event", () => { - const event = new HandleFirstSuspendEvent(); - before("Start SOMns and Connect", () => { - connectionP = startSomAndConnect(event.getSuspendEvent, [], ["halt"]); + conn = new TestConnection(["halt"]); + ctrl = new HandleStoppedAndGetStackTrace([], conn); }); after(closeConnectionAfterSuite); it("should halt on expected source section", () => { - return event.suspendP.then(msg => { - expectStack(msg.stack, 7, "PingPongApp>>#testHalt", 84); + return ctrl.stackP.then(msg => { + expectStack(msg.stackFrames, 7, "PingPongApp>>#testHalt", 84); }); }); }); diff --git a/tools/kompos/tests/test-setup.ts b/tools/kompos/tests/test-setup.ts index 5ae6aaa18..5f4a7f3f4 100644 --- a/tools/kompos/tests/test-setup.ts +++ b/tools/kompos/tests/test-setup.ts @@ -3,10 +3,11 @@ import { ChildProcess, spawn, spawnSync, SpawnSyncReturns } from "child_process" import { resolve } from "path"; import * as WebSocket from "ws"; -import {SuspendEventMessage, BreakpointData, Respond, SourceCoordinate, - FullSourceCoordinate, Frame} from "../src/messages"; +import {Controller} from "../src/controller"; +import {BreakpointData, SourceCoordinate, StoppedMessage, StackTraceResponse, + FullSourceCoordinate, StackFrame} from "../src/messages"; +import {VmConnection} from "../src/vm-connection"; -const DEBUGGER_PORT = 7977; const SOM_BASEPATH = "../../"; export const SOM = SOM_BASEPATH + "som"; export const PING_PONG_URI = "file:" + resolve("tests/pingpong.som"); @@ -14,11 +15,12 @@ export const PING_PONG_URI = "file:" + resolve("tests/pingpong.som"); const PRINT_SOM_OUTPUT = false; const PRINT_CMD_LINE = false; -export function expectStack(stack: Frame[], length: number, methodName: string, +export function expectStack(stack: StackFrame[], length: number, methodName: string, startLine: number) { expect(stack).lengthOf(length); - expect(stack[0].methodName).to.equal(methodName); - expect(stack[0].sourceSection.startLine).to.equal(startLine); + expect(stack[0]).to.be.not.null; + expect(stack[0].name).to.equal(methodName); + expect(stack[0].line).to.equal(startLine); } export function expectSourceCoordinate(section: SourceCoordinate) { @@ -42,23 +44,90 @@ export interface OnMessageHandler { (event: OnMessageEvent): void; } -export interface SomConnection { - somProc: ChildProcess; - socket: WebSocket; - closed: boolean; +export class TestConnection extends VmConnection { + private somProc: ChildProcess; + private closed: boolean; + private connectionResolver; + public readonly fullyConnected: Promise; + + constructor(extraArgs?: string[], triggerDebugger?: boolean, testFile?: string) { + super(false); + this.closed = false; + this.startSom(extraArgs, triggerDebugger, testFile); + this.fullyConnected = this.initConnection(); + } + + private startSom(extraArgs?: string[], triggerDebugger?: boolean, testFile?: string) { + if (!testFile) { testFile = "tests/pingpong.som"; } + let args = ["-G", "-t1", "-wd", testFile]; + if (triggerDebugger) { args = ["-d"].concat(args); }; + if (extraArgs) { args = args.concat(extraArgs); } + + if (PRINT_CMD_LINE) { + console.log("[CMD]" + SOM + " " + args.join(" ")); + } + + this.somProc = spawn(SOM, args); + } + + protected onOpen() { + super.onOpen(); + this.connectionResolver(true); + } + + private initConnection(): Promise { + const promise = new Promise((resolve, reject) => { + this.connectionResolver = resolve; + let connecting = false; + + if (PRINT_SOM_OUTPUT) { + this.somProc.stderr.on("data", (data) => { console.error(data.toString()); }); + } + + this.somProc.stdout.on("data", (data) => { + const dataStr = data.toString(); + if (PRINT_SOM_OUTPUT) { + console.log(dataStr); + } + if (dataStr.includes("Started HTTP Server") && !connecting) { + connecting = true; + this.connect(); + } + if (dataStr.includes("Failed starting WebSocket and/or HTTP Server")) { + reject(new Error("SOMns failed to starting WebSocket and/or HTTP Server")); + } + }); + }); + return promise; + } + + public close(done: MochaDone) { + if (this.closed) { + done(); + return; + } + + this.somProc.on("exit", _code => { + this.closed = true; + // wait until process is shut down, to make sure all ports are closed + done(); + }); + this.somProc.kill(); + } } -export function closeConnection(connection: SomConnection, done: MochaDone) { - if (connection.closed) { - done(); - return; +export class ControllerWithInitialBreakpoints extends Controller { + private initialBreakpoints: BreakpointData[]; + + constructor(initialBreakpoints: BreakpointData[], vmConnection: VmConnection) { + super(vmConnection); + this.initialBreakpoints = initialBreakpoints; + } + + public onConnect() { + super.onConnect(); + this.vmConnection.sendInitialBreakpoints(this.initialBreakpoints); } - connection.somProc.kill(); - connection.somProc.on("exit", _code => { - connection.closed = true; - // wait until process is shut down, to make sure all ports are closed - done(); - }); } export function execSom(extraArgs: string[]): SpawnSyncReturns { @@ -66,74 +135,57 @@ export function execSom(extraArgs: string[]): SpawnSyncReturns { return spawnSync(SOM, args); } -export class HandleFirstSuspendEvent { - private firstSuspendCaptured: boolean; - public getSuspendEvent: (event: OnMessageEvent) => void; - - public readonly suspendP: Promise; - - constructor() { - this.firstSuspendCaptured = false; - this.suspendP = new Promise((resolve, _reject) => { - this.getSuspendEvent = (event: OnMessageEvent) => { - if (this.firstSuspendCaptured) { return; } - const data = JSON.parse(event.data); - if (data.type === "suspendEvent") { - this.firstSuspendCaptured = true; - resolve(data); - } - }; +export class HandleStoppedAndGetStackTrace extends ControllerWithInitialBreakpoints { + private numStopped: number; + private readonly numOps: number; + public readonly stackP: Promise; + public readonly stackPs: Promise[]; + private resolveStackP; + private readonly resolveStackPs; + public readonly stoppedActivities: number[]; + + constructor(initialBreakpoints: BreakpointData[], vmConnection: VmConnection, + numOps: number = 1) { + super(initialBreakpoints, vmConnection); + + this.numOps = numOps; + this.numStopped = 0; + this.stoppedActivities = []; + + this.stackP = new Promise((resolve, _reject) => { + this.resolveStackP = resolve; }); - } -} -export function send(socket: WebSocket, respond: Respond) { - socket.send(JSON.stringify(respond)); -} + if (numOps > 1) { + this.resolveStackPs = []; + this.stackPs = []; + for (let i = 1; i < numOps; i += 1) { + this.stackPs.push(new Promise((resolve, _reject) => { + this.resolveStackPs.push(resolve); + })); + } + } + } -export function startSomAndConnect(onMessageHandler?: OnMessageHandler, - initialBreakpoints?: BreakpointData[], extraArgs?: string[], - triggerDebugger?: boolean, testFile?: string): Promise { - if (!testFile) { testFile = "tests/pingpong.som"; } - let args = ["-G", "-t1", "-wd", testFile]; - if (triggerDebugger) { args = ["-d"].concat(args); }; - if (extraArgs) { args = args.concat(extraArgs); } - if (PRINT_CMD_LINE) { - console.log("[CMD]" + SOM + args.join(" ")); + public getStackP(idx: number) { + if (idx === 0) { + return this.stackP; + } + return this.stackPs[idx - 1]; } - const somProc = spawn(SOM, args); - const promise = new Promise((resolve, reject) => { - let connecting = false; - somProc.stderr.on("data", (data) => { - if (PRINT_SOM_OUTPUT) { - console.error(data.toString()); - } - }); + public onStoppedEvent(msg: StoppedMessage) { + if (this.numStopped >= this.numOps) { return; } + this.stoppedActivities[this.numStopped] = msg.activityId; + this.numStopped += 1; + this.vmConnection.requestStackTrace(msg.activityId); + } - somProc.stdout.on("data", (data) => { - const dataStr = data.toString(); - if (PRINT_SOM_OUTPUT) { - console.log(dataStr); - } - if (dataStr.includes("Started HTTP Server") && !connecting) { - connecting = true; - const socket = new WebSocket("ws://localhost:" + DEBUGGER_PORT); - socket.on("open", () => { - if (initialBreakpoints) { - send(socket, {action: "initialBreakpoints", - breakpoints: initialBreakpoints, debuggerProtocol: false}); - } - resolve({somProc: somProc, socket: socket, closed: false}); - }); - if (onMessageHandler) { - socket.onmessage = onMessageHandler; - } - } - if (dataStr.includes("Failed starting WebSocket and/or HTTP Server")) { - reject(new Error("SOMns failed to starting WebSocket and/or HTTP Server")); - } - }); - }); - return promise; + public onStackTrace(msg: StackTraceResponse) { + if (this.numStopped === 1) { + this.resolveStackP(msg); + return; + } + this.resolveStackPs[this.numStopped - 2](msg); + } }
name
name value