Skip to content

Commit f3138c9

Browse files
committed
DAP debug server, multi-thread debugging, and iterative interpreter debug support
- Add MSDebugServer implementing the Debug Adapter Protocol over TCP, with launch/attach modes, breakpoints, step-over/step-in/step-out, variable inspection, exception breakpoints, and watch expressions - Add multi-thread DAP support: register/unregister threads, per-thread pause states, sync and async stepping modes (sync blocks in place, async snapshots state and resumes on a new thread) - Refactor DebugContext into a full thread-aware debug state manager with per-thread StepMode, ThreadDebugState, and a thread registry for DAP - Add DaemonManager lifecycle listeners and thread-aware waitForThreads, so the debug session stays alive while background threads run - Extract spawnExecutionThread() to centralize execution thread lifecycle (run, await daemons, signal completion) in one place - Fix StackTraceManager thread affinity: remove isDebugAdopted flag so background threads (x_new_thread) get their own STM instead of sharing the main thread's, which was corrupting call depth for step-over - Fix skippingResume flag: clear unconditionally on source line change rather than requiring shouldStop=true, which blocked step-over returns - Add StackTraceFrame.getTarget() for debugger source mapping - Add Breakpoint condition/hitCount/logMessage support - Wire up cmdline interpreter (--debug flag) and lang server for DAP - Add DAPTestHarness and dual sync/async integration tests for step-over and multi-thread step-over scenarios - Add debugger dependency (lsp4j.debug) to pom.xml
1 parent d0f6e51 commit f3138c9

26 files changed

Lines changed: 2741 additions & 293 deletions

pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@
376376
<artifactId>org.eclipse.lsp4j</artifactId>
377377
<version>0.22.0</version>
378378
</dependency>
379+
<dependency>
380+
<groupId>org.eclipse.lsp4j</groupId>
381+
<artifactId>org.eclipse.lsp4j.debug</artifactId>
382+
<version>0.22.0</version>
383+
</dependency>
379384
<dependency>
380385
<groupId>io.swagger.core.v3</groupId>
381386
<artifactId>swagger-annotations</artifactId>
@@ -569,6 +574,8 @@
569574
<include>org.brotli:dec:jar:*</include>
570575
<include>org.eclipse.lsp4j:org.eclipse.lsp4j:jar:*</include>
571576
<include>org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:jar:*</include>
577+
<include>org.eclipse.lsp4j:org.eclipse.lsp4j.debug:jar:*</include>
578+
<include>org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc.debug:jar:*</include>
572579
<!-- We don't currently actually depend on these 2, but lsp4j does, so we
573580
need to shade it in. In the future, we may rip out org.json:json,
574581
and replace it with this, however, in which case we only need to
@@ -842,6 +849,24 @@
842849
<exclude>META-INF/**</exclude>
843850
</excludes>
844851
</filter>
852+
<filter>
853+
<artifact>org.eclipse.lsp4j:org.eclipse.lsp4j.debug:jar:*</artifact>
854+
<includes>
855+
<include>**</include>
856+
</includes>
857+
<excludes>
858+
<exclude>META-INF/**</exclude>
859+
</excludes>
860+
</filter>
861+
<filter>
862+
<artifact>org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc.debug:jar:*</artifact>
863+
<includes>
864+
<include>**</include>
865+
</includes>
866+
<excludes>
867+
<exclude>META-INF/**</exclude>
868+
</excludes>
869+
</filter>
845870
<filter>
846871
<artifact>com.google.code.gson:gson:jar:*</artifact>
847872
<includes>

src/main/java/com/laytonsmith/PureUtilities/DaemonManager.java

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.laytonsmith.PureUtilities;
22

3+
import java.util.ArrayList;
34
import java.util.HashSet;
5+
import java.util.List;
46
import java.util.Set;
57

68
/**
@@ -10,24 +12,79 @@
1012
*/
1113
public class DaemonManager {
1214

15+
/**
16+
* Listener interface for thread lifecycle events.
17+
*/
18+
public interface ThreadLifecycleListener {
19+
/**
20+
* Called when a thread is activated (registered as active).
21+
* @param t The thread that was activated
22+
* @param displayName A user-facing display name for the thread, or null
23+
* if no explicit name was provided (in which case the thread's Java
24+
* name should be used as a fallback).
25+
*/
26+
void onActivated(Thread t, String displayName);
27+
28+
/**
29+
* Called when a thread is deactivated (no longer active).
30+
* @param t The thread that was deactivated
31+
*/
32+
void onDeactivated(Thread t);
33+
}
34+
1335
private final Object lock = new Object();
1436
private final Set<Thread> threads = new HashSet<>();
37+
private final List<ThreadLifecycleListener> listeners = new ArrayList<>();
1538
private int count = 0;
1639

40+
/**
41+
* Adds a listener that will be notified when threads are activated or deactivated.
42+
* @param listener The listener to add
43+
*/
44+
public void addThreadLifecycleListener(ThreadLifecycleListener listener) {
45+
synchronized(lock) {
46+
listeners.add(listener);
47+
}
48+
}
49+
50+
/**
51+
* Removes a previously added lifecycle listener.
52+
* @param listener The listener to remove
53+
*/
54+
public void removeThreadLifecycleListener(ThreadLifecycleListener listener) {
55+
synchronized(lock) {
56+
listeners.remove(listener);
57+
}
58+
}
59+
1760
/**
1861
* Sets a thread to "daemon" mode, meaning it is currently active. Null may be sent, in which case the current
1962
* thread is used. You should always put a deactivateThread call for every activateThread call.
2063
*
2164
* @param t The thread to activate
2265
*/
2366
public void activateThread(Thread t) {
67+
activateThread(t, null);
68+
}
69+
70+
/**
71+
* Sets a thread to "daemon" mode with an explicit display name. Null may be sent for the thread,
72+
* in which case the current thread is used.
73+
*
74+
* @param t The thread to activate
75+
* @param displayName A user-facing name for the thread, or null to use the Java thread name
76+
*/
77+
public void activateThread(Thread t, String displayName) {
78+
Thread resolved;
79+
List<ThreadLifecycleListener> snapshot;
2480
synchronized(lock) {
25-
if(t != null) {
26-
threads.add(t);
27-
} else {
28-
threads.add(Thread.currentThread());
29-
}
81+
resolved = t != null ? t : Thread.currentThread();
82+
threads.add(resolved);
3083
++count;
84+
snapshot = new ArrayList<>(listeners);
85+
}
86+
for(ThreadLifecycleListener listener : snapshot) {
87+
listener.onActivated(resolved, displayName);
3188
}
3289
}
3390

@@ -37,14 +94,17 @@ public void activateThread(Thread t) {
3794
* @param t The thread to deactivate
3895
*/
3996
public void deactivateThread(Thread t) {
97+
Thread resolved;
98+
List<ThreadLifecycleListener> snapshot;
4099
synchronized(lock) {
41-
if(t != null) {
42-
threads.remove(t);
43-
} else {
44-
threads.remove(Thread.currentThread());
45-
}
100+
resolved = t != null ? t : Thread.currentThread();
101+
threads.remove(resolved);
46102
--count;
47103
lock.notify();
104+
snapshot = new ArrayList<>(listeners);
105+
}
106+
for(ThreadLifecycleListener listener : snapshot) {
107+
listener.onDeactivated(resolved);
48108
}
49109
}
50110

src/main/java/com/laytonsmith/core/FlowFunction.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.laytonsmith.core.StepAction.StepResult;
44
import com.laytonsmith.core.constructs.Target;
5+
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
56
import com.laytonsmith.core.natives.interfaces.Mixed;
67
import com.laytonsmith.core.environments.Environment;
78

@@ -94,4 +95,21 @@ default StepResult<S> childInterrupted(Target t, S state, StepAction.FlowControl
9495
*/
9596
default void cleanup(Target t, S state, Environment env) {
9697
}
98+
99+
/**
100+
* Returns whether the given exception would be caught by this function,
101+
* given the current per-call state. The default returns false. Override in
102+
* exception-catching functions (e.g. try/catch) to inspect the state and
103+
* determine if the exception matches a catch clause.
104+
*
105+
* <p>This is used by the debugger to determine if an exception is "uncaught"
106+
* by inspecting the eval stack without actually propagating the exception.</p>
107+
*
108+
* @param state The per-call state from the stack frame
109+
* @param exception The exception to check
110+
* @return true if this function would catch the exception in its current state
111+
*/
112+
default boolean wouldCatch(S state, ConfigRuntimeException exception) {
113+
return false;
114+
}
97115
}

src/main/java/com/laytonsmith/core/Main.java

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.laytonsmith.tools.ProfilerSummary;
5454
import com.laytonsmith.tools.SyntaxHighlighters;
5555
import com.laytonsmith.tools.UILauncher;
56+
import com.laytonsmith.tools.debugger.MSDebugServer;
5657
import com.laytonsmith.tools.docgen.DocGen;
5758
import com.laytonsmith.tools.docgen.DocGenExportTool;
5859
import com.laytonsmith.tools.docgen.DocGenTemplates;
@@ -764,6 +765,9 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce
764765
modules = modules.replaceAll("(.*)\n", "--add-opens $1=ALL-UNNAMED ");
765766
args += " " + modules;
766767
}
768+
if(JavaVersion.GetMajorVersion() >= 16) {
769+
args += "--enable-native-access=ALL-UNNAMED ";
770+
}
767771
args += "-Xrs ";
768772
StreamUtils.GetSystemOut().println(args.trim());
769773
}
@@ -1072,21 +1076,58 @@ public ArgumentParser getArgumentParser() {
10721076
.addArgument(new ArgumentBuilder()
10731077
.setDescription("The code to run")
10741078
.setUsageName("methodscript code")
1075-
.setRequiredAndDefault());
1079+
.setRequiredAndDefault())
1080+
.addArgument(new ArgumentBuilder()
1081+
.setDescription("Enable the DAP debug server.")
1082+
.asFlag()
1083+
.setName("debug"))
1084+
.addArgument(new ArgumentBuilder()
1085+
.setDescription("The port for the debug server. Defaults to "
1086+
+ MSDebugServer.DEFAULT_PORT + ".")
1087+
.setUsageName("port")
1088+
.setOptional()
1089+
.setName("debug-port")
1090+
.setArgType(ArgumentBuilder.BuilderTypeNonFlag.NUMBER))
1091+
.addArgument(new ArgumentBuilder()
1092+
.setDescription("Wait for the debugger to connect before executing.")
1093+
.asFlag()
1094+
.setName("debug-suspend"));
10761095
}
10771096

10781097
@Override
10791098
public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exception {
10801099
ClassDiscovery.getDefaultInstance().addThisJar();
10811100

1101+
boolean debug = parsedArgs.isFlagSet("debug");
1102+
int debugPort = 0;
1103+
if(parsedArgs.getNumberArgument("debug-port") != null) {
1104+
debugPort = parsedArgs.getNumberArgument("debug-port").intValue();
1105+
}
1106+
boolean debugSuspend = parsedArgs.isFlagSet("debug-suspend");
1107+
10821108
String script = parsedArgs.getStringArgument();
10831109
File file = new File("Interpreter");
10841110
Environment env = Static.GenerateStandaloneEnvironment(true,
10851111
EnumSet.of(RuntimeMode.CMDLINE, RuntimeMode.INTERPRETER));
10861112
Set<Class<? extends Environment.EnvironmentImpl>> envs = Environment.getDefaultEnvClasses();
1087-
MethodScriptCompiler.execute(script, file, true, env, envs, (s) -> {
1088-
System.out.println(s);
1089-
}, null, null);
1113+
1114+
MSDebugServer debugServer = null;
1115+
if(debug) {
1116+
int port = debugPort == 0 ? MSDebugServer.DEFAULT_PORT : debugPort;
1117+
debugServer = new MSDebugServer();
1118+
env = debugServer.startListening(port, env, debugSuspend);
1119+
debugServer.awaitConfiguration();
1120+
}
1121+
1122+
try {
1123+
MethodScriptCompiler.execute(script, file, true, env, envs, (s) -> {
1124+
System.out.println(s);
1125+
}, null, null);
1126+
} finally {
1127+
if(debugServer != null) {
1128+
debugServer.shutdown();
1129+
}
1130+
}
10901131
}
10911132
}
10921133

@@ -1218,15 +1259,44 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce
12181259
//We actually can't use the parsedArgs, because there may be cmdline switches in
12191260
//the arguments that we want to ignore here, but otherwise pass through. parsedArgs
12201261
//will prevent us from seeing those, however.
1221-
List<String> allArgs = parsedArgs.getRawArguments();
1262+
List<String> allArgs = new ArrayList<>(parsedArgs.getRawArguments());
12221263
if(allArgs.isEmpty()) {
12231264
StreamUtils.GetSystemErr().println("Usage: path/to/file.ms [arg1 arg2]");
12241265
System.exit(1);
12251266
}
1267+
1268+
boolean debug = false;
1269+
int debugPort = 0;
1270+
boolean debugSuspend = false;
1271+
String debugThreadingMode = null;
1272+
for(java.util.Iterator<String> it = allArgs.iterator(); it.hasNext();) {
1273+
String arg = it.next();
1274+
if("--debug".equals(arg)) {
1275+
debug = true;
1276+
it.remove();
1277+
} else if("--debug-port".equals(arg)) {
1278+
it.remove();
1279+
if(it.hasNext()) {
1280+
debugPort = Integer.parseInt(it.next());
1281+
it.remove();
1282+
}
1283+
} else if("--debug-suspend".equals(arg)) {
1284+
debugSuspend = true;
1285+
it.remove();
1286+
} else if("--debug-threading-mode".equals(arg)) {
1287+
it.remove();
1288+
if(it.hasNext()) {
1289+
debugThreadingMode = it.next();
1290+
it.remove();
1291+
}
1292+
}
1293+
}
1294+
12261295
String fileName = allArgs.get(0);
12271296
allArgs.remove(0);
12281297
try {
1229-
Interpreter.startWithTTY(fileName, allArgs);
1298+
Interpreter.startWithTTY(fileName, allArgs, true,
1299+
debug, debugPort, debugSuspend, debugThreadingMode);
12301300
} catch (Profiles.InvalidProfileException ex) {
12311301
StreamUtils.GetSystemErr().println("Invalid profile file at " + MethodScriptFileLocations.getDefault()
12321302
.getProfilesFile()

src/main/java/com/laytonsmith/core/Procedure.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.laytonsmith.core.constructs.CVoid;
1111
import com.laytonsmith.core.constructs.Construct;
1212
import com.laytonsmith.core.constructs.IVariable;
13+
import com.laytonsmith.core.constructs.IVariableList;
1314
import com.laytonsmith.core.constructs.InstanceofUtil;
1415
import com.laytonsmith.core.constructs.Target;
1516
import com.laytonsmith.core.environments.Environment;
@@ -288,25 +289,27 @@ private Environment prepareEnvironment(List<Mixed> args, Environment oldEnv, Tar
288289
}
289290
oldEnv.getEnv(GlobalEnv.class).setCloneVars(prev);
290291

292+
IVariableList varList = env.getEnv(GlobalEnv.class).GetVarList();
291293
CArray arguments = new CArray(Target.UNKNOWN, this.varIndex.size());
294+
IVariable lastParam = this.varIndex.isEmpty() ? null : this.varIndex.get(this.varIndex.size() - 1);
292295

293296
int varInd;
294297
CArray vararg = null;
295298
for(varInd = 0; varInd < args.size(); varInd++) {
296299
Mixed c = args.get(varInd);
297300
arguments.push(c, callTarget);
298301
if(this.varIndex.size() > varInd
299-
|| (!this.varIndex.isEmpty()
300-
&& this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType())) {
302+
|| (lastParam != null
303+
&& lastParam.getDefinedType().isVariadicType())) {
301304
IVariable var;
302305
if(varInd < this.varIndex.size() - 1
303-
|| !this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType()) {
306+
|| !lastParam.getDefinedType().isVariadicType()) {
304307
var = this.varIndex.get(varInd);
305308
} else {
306-
var = this.varIndex.get(this.varIndex.size() - 1);
309+
var = lastParam;
307310
if(vararg == null) {
308311
vararg = new CArray(callTarget);
309-
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE,
312+
varList.set(new IVariable(CArray.TYPE,
310313
var.getVariableName(), vararg, c.getTarget()));
311314
}
312315
}
@@ -330,7 +333,7 @@ private Environment prepareEnvironment(List<Mixed> args, Environment oldEnv, Tar
330333
}
331334

332335
if(InstanceofUtil.isInstanceof(c.typeof(env), var.getDefinedType(), env)) {
333-
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(var.getDefinedType(),
336+
varList.set(new IVariable(var.getDefinedType(),
334337
var.getVariableName(), c, c.getTarget()));
335338
continue;
336339
} else {
@@ -344,11 +347,11 @@ private Environment prepareEnvironment(List<Mixed> args, Environment oldEnv, Tar
344347
while(varInd < this.varIndex.size()) {
345348
String varName = this.varIndex.get(varInd++).getVariableName();
346349
Mixed defVal = this.originals.get(varName);
347-
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(Auto.TYPE, varName, defVal, defVal.getTarget()));
350+
varList.set(new IVariable(Auto.TYPE, varName, defVal, defVal.getTarget()));
348351
arguments.push(defVal, callTarget);
349352
}
350353

351-
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, callTarget));
354+
varList.set(new IVariable(CArray.TYPE, "@arguments", arguments, callTarget));
352355
return env;
353356
}
354357

@@ -471,7 +474,7 @@ private StepAction startBody(Environment callerEnv) {
471474
procEnv = prepareEnvironment(evaluatedArgs, callerEnv, callTarget);
472475
StackTraceManager stManager = procEnv.getEnv(GlobalEnv.class).GetStackTraceManager();
473476
stManager.addStackTraceFrame(
474-
new StackTraceFrame("proc " + name, getTarget()));
477+
new StackTraceFrame("proc " + name, getTarget(), callTarget));
475478
return new StepAction.Evaluate(tree, procEnv);
476479
}
477480

0 commit comments

Comments
 (0)