Skip to content

Commit 9d04ebc

Browse files
committed
LDEV-1402 add cfml DAP client test suites
1 parent e6f24f8 commit 9d04ebc

40 files changed

+4181
-188
lines changed

.github/workflows/test-dap.yml

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ test/scratch
1313
/profiling/output
1414
/test-output
1515
/profiling/test-output
16+
.vscode/settings.json
17+
.claude/
18+
*.md
19+
!README.md
20+
build-output.txt
21+
test-output.txt

.vscode/settings.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

extension/META-INF/MANIFEST.MF

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name: "Luceedebug"
44
symbolic-name: "luceedebug"
55
description: "Native CFML debugger for VS Code - no Java agent required"
66
version: "3.0.0"
7-
lucee-core-version: "7.1.0.0"
7+
lucee-core-version: "7.1.0.6"
88
start-bundles: true
99
release-type: server
10-
startup-hook: [{"class": "luceedebug.extension.ExtensionActivator", "bundle-name": "luceedebug-osgi", "bundle-version": "2.0.1.1"}]
10+
startup-hook: [{"class": "luceedebug.extension.ExtensionActivator", "bundle-name": "luceedebug-osgi", "bundle-version": "3.0.0.0"}]

luceedebug/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ tasks.jar {
8989
"Premain-Class" to "luceedebug.Agent",
9090
"Can-Redefine-Classes" to "true",
9191
"Bundle-SymbolicName" to "luceedebug-osgi",
92-
"Bundle-Version" to "2.0.1.1",
92+
"Bundle-Version" to "3.0.0.0",
9393
"Export-Package" to "luceedebug.*"
9494
)
9595
)

luceedebug/src/main/java/luceedebug/Agent.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,23 @@ private static Map<String, Integer> linearizedCoreInjectClasses() {
186186
result.put("luceedebug.coreinject.frame.Frame$FrameContext", 1);
187187
result.put("luceedebug.coreinject.frame.Frame$FrameContext$SupplierOrNull", 1);
188188
result.put("luceedebug.coreinject.frame.DummyFrame", 1);
189+
result.put("luceedebug.coreinject.frame.NativeDebugFrame", 1);
190+
191+
// Native debugger classes - not used in agent mode but need to be in the map
192+
result.put("luceedebug.coreinject.NativeLuceeVm", 0);
193+
result.put("luceedebug.coreinject.NativeLuceeVm$1", 0);
194+
result.put("luceedebug.coreinject.NativeLuceeVm$2", 0);
195+
result.put("luceedebug.coreinject.NativeLuceeVm$3", 0);
196+
result.put("luceedebug.coreinject.NativeDebuggerListener", 0);
197+
result.put("luceedebug.coreinject.NativeDebuggerListener$1", 0);
198+
result.put("luceedebug.coreinject.NativeDebuggerListener$CachedExecutableLines", 0);
199+
result.put("luceedebug.coreinject.NativeDebuggerListener$StepState", 0);
200+
result.put("luceedebug.coreinject.NativeDebuggerListener$SuspendLocation", 0);
201+
result.put("luceedebug.coreinject.StepMode", 0);
189202

190203
return result;
191204
}
192-
205+
193206
public static Comparator<ClassInjection> comparator() {
194207
final Map<String, Integer> ordering = linearizedCoreInjectClasses();
195208
return Comparator.comparing(injection -> {

luceedebug/src/main/java/luceedebug/DapServer.java

Lines changed: 159 additions & 50 deletions
Large diffs are not rendered by default.

luceedebug/src/main/java/luceedebug/Log.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory;
55
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
66

7+
import lucee.loader.engine.CFMLEngineFactory;
8+
79
/**
810
* Centralized logging for luceedebug.
911
* Routes all log messages through a common method that:
1012
* - Writes to System.out with [luceedebug] prefix
1113
* - Optionally sends to DAP OutputEvent when a client is connected
1214
* - Supports ANSI colors (configurable via launch.json colorLogs, default true)
1315
* - Respects log level (configurable via launch.json logLevel, default info)
16+
* - Writes errors to Lucee's exception.log when available
1417
*/
1518
public class Log {
1619
private static final String PREFIX = "[luceedebug] ";
20+
private static final String APP_NAME = "luceedebug";
1721

1822
// ANSI escape codes (for console/tomcat output)
1923
private static final String ANSI_RESET = "\u001b[0m";
@@ -117,6 +121,7 @@ public static void info(String message) {
117121

118122
/**
119123
* Log an error message. Always logged regardless of log level.
124+
* Also logs to Lucee's exception.log when available.
120125
*/
121126
public static void error(String message) {
122127
if (!consoleOutput) {
@@ -129,14 +134,17 @@ public static void error(String message) {
129134
System.out.println(consoleMsg);
130135
}
131136
sendToDap("ERROR: " + message, OutputEventArgumentsCategory.STDERR);
137+
logToLuceeException(message);
132138
}
133139

134140
/**
135141
* Log an error with exception.
142+
* Also logs to Lucee's exception.log when available.
136143
*/
137144
public static void error(String message, Throwable t) {
138145
error(message + ": " + t.getMessage());
139146
t.printStackTrace();
147+
logToLuceeException(message, t);
140148
}
141149

142150
/**
@@ -267,4 +275,40 @@ private static void sendToDap(String message, String category) {
267275
}
268276
}
269277
}
278+
279+
/**
280+
* Get Lucee's exception log if available.
281+
* Returns null if Lucee is not running or log cannot be obtained.
282+
*/
283+
private static lucee.commons.io.log.Log getLuceeExceptionLog() {
284+
try {
285+
var config = CFMLEngineFactory.getInstance().getThreadConfig();
286+
if (config != null) {
287+
return config.getLog("exception");
288+
}
289+
} catch (Throwable t) {
290+
// Lucee not available or not initialized yet - silently ignore
291+
}
292+
return null;
293+
}
294+
295+
/**
296+
* Log an error to Lucee's exception.log if available.
297+
*/
298+
private static void logToLuceeException(String message) {
299+
var luceeLog = getLuceeExceptionLog();
300+
if (luceeLog != null) {
301+
luceeLog.error(APP_NAME, message);
302+
}
303+
}
304+
305+
/**
306+
* Log an error with throwable to Lucee's exception.log if available.
307+
*/
308+
private static void logToLuceeException(String message, Throwable t) {
309+
var luceeLog = getLuceeExceptionLog();
310+
if (luceeLog != null) {
311+
luceeLog.error(APP_NAME, message, t);
312+
}
313+
}
270314
}

luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import luceedebug.coreinject.frame.DebugFrame;
3434
import luceedebug.coreinject.frame.Frame;
3535
import luceedebug.coreinject.frame.Frame.FrameContext;
36-
import luceedebug.coreinject.frame.NativeDebugFrame;
3736

3837
public class DebugManager implements IDebugManager {
3938

@@ -74,7 +73,12 @@ public void spawnWorker(Config config, String jdwpHost, int jdwpPort, String deb
7473

7574
new Thread(() -> {
7675
System.out.println("[luceedebug] jdwp self connect OK");
77-
DapServer.createForSocket(luceeVm, config, debugHost, debugPort);
76+
try {
77+
DapServer.createForSocket(luceeVm, config, debugHost, debugPort);
78+
} catch (Throwable t) {
79+
System.out.println("[luceedebug] DAP server thread failed: " + t.getMessage());
80+
t.printStackTrace();
81+
}
7882
}, threadName).start();
7983
}
8084

@@ -498,32 +502,18 @@ synchronized public IDebugEntity[] getVariables(long id, IDebugEntity.DebugEntit
498502
}
499503

500504
synchronized public IDebugFrame[] getCfStack(Thread thread) {
505+
System.out.println("[luceedebug] getCfStack: looking for thread=" + thread.getName() + " (id=" + thread.getId() + ") identity=" + System.identityHashCode(thread));
506+
System.out.println("[luceedebug] getCfStack: cfStackByThread has " + cfStackByThread.size() + " entries:");
507+
for (var entry : cfStackByThread.entrySet()) {
508+
Thread t = entry.getKey();
509+
System.out.println("[luceedebug] thread=" + t.getName() + " (id=" + t.getId() + ") identity=" + System.identityHashCode(t) + " frames=" + entry.getValue().size());
510+
}
501511
ArrayList<DebugFrame> stack = cfStackByThread.get(thread);
502512

503-
// If no instrumented frames, try native Lucee7 frames
513+
// Agent mode: only use bytecode-instrumented frames, no native fallback
504514
if (stack == null || stack.isEmpty()) {
505-
// Try our tracked PageContext first
506-
WeakReference<PageContext> pcRef = pageContextByThread.get(thread);
507-
PageContext pc = pcRef != null ? pcRef.get() : null;
508-
509-
// Fall back to ThreadLocalPageContext if we're on the same thread
510-
if (pc == null && thread == Thread.currentThread()) {
511-
pc = lucee.runtime.engine.ThreadLocalPageContext.get();
512-
}
513-
514-
if (pc != null) {
515-
// In agent mode, pass null for classloader - classes are injected into Lucee's classloader
516-
IDebugFrame[] nativeFrames = NativeDebugFrame.getNativeFrames(pc, valTracker, -1, null);
517-
if (nativeFrames != null && nativeFrames.length > 0) {
518-
return nativeFrames;
519-
}
520-
}
521-
522-
if (stack == null) {
523-
System.out.println("getCfStack called, frames was null, frames is " + cfStackByThread + ", passed thread was " + thread);
524-
System.out.println(" thread=" + thread + " this=" + this);
525-
return new Frame[0];
526-
}
515+
System.out.println("[luceedebug] getCfStack: no instrumented frames for thread " + thread);
516+
return new Frame[0];
527517
}
528518

529519
ArrayList<DebugFrame> result = new ArrayList<>();
@@ -532,9 +522,11 @@ synchronized public IDebugFrame[] getCfStack(Thread thread) {
532522
// go backwards, "most recent first"
533523
for (int i = stack.size() - 1; i >= 0; --i) {
534524
DebugFrame frame = stack.get(i);
525+
System.out.println("[luceedebug] getCfStack: frame[" + i + "] line=" + frame.getLine() + " source=" + frame.getSourceFilePath());
535526
if (frame.getLine() == 0) {
536-
// ???? should we just not push such frames on the stack?
537-
// what does this mean?
527+
// Frame line not yet set - step notification hasn't run yet
528+
// This can happen when breakpoint fires before first line executes
529+
System.out.println("[luceedebug] getCfStack: skipping frame with line=0");
538530
continue;
539531
}
540532
else {
@@ -607,14 +599,18 @@ public void clearStepRequest(Thread thread) {
607599
}
608600

609601
public void luceedebug_stepNotificationEntry_step(int lineNumber) {
610-
// Fast path: single volatile read when not stepping (99.9% of the time)
602+
Thread currentThread = Thread.currentThread();
603+
604+
// ALWAYS update the frame's line number, even when not stepping
605+
// This is required for breakpoints to work - they need to know the current line
606+
DebugFrame frame = maybeUpdateTopmostFrame(currentThread, lineNumber);
607+
608+
// Fast path: if not stepping, we're done after updating line number
611609
if (!hasAnyStepRequests) {
612610
return;
613611
}
614612

615613
final int minDistanceToLuceedebugStepNotificationEntryFrame = 0;
616-
Thread currentThread = Thread.currentThread();
617-
DebugFrame frame = maybeUpdateTopmostFrame(currentThread, lineNumber); // should be "definite update topmost frame", we 100% expect there to be a frame
618614

619615
CfStepRequest request = stepRequestByThread.get(currentThread);
620616
if (request == null) {
@@ -724,6 +720,8 @@ private DebugFrame getTopmostFrame(Thread thread) {
724720
}
725721

726722
public void pushCfFrame(PageContext pageContext, String sourceFilePath) {
723+
Thread t = Thread.currentThread();
724+
System.out.println("[luceedebug] pushCfFrame: thread=" + t.getName() + " (id=" + t.getId() + ") identity=" + System.identityHashCode(t) + " file=" + sourceFilePath);
727725
maybe_pushCfFrame_worker(pageContext, sourceFilePath);
728726
}
729727

luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -462,19 +462,6 @@ public LuceeVm(Config config, VirtualMachine vm) {
462462
while (!done.get()); // about ~8ms to queueWork + wait for work to complete
463463
});
464464

465-
// Register native breakpoint suspend callback (Lucee7+)
466-
NativeDebuggerListener.setOnNativeSuspendCallback((javaThreadId, label) -> {
467-
if (nativeBreakpointEventCallback != null) {
468-
nativeBreakpointEventCallback.accept(javaThreadId, label);
469-
}
470-
});
471-
472-
// Register native step callback (Lucee7+)
473-
NativeDebuggerListener.setOnNativeStepCallback(javaThreadId -> {
474-
if (stepEventCallback != null) {
475-
stepEventCallback.accept(javaThreadId);
476-
}
477-
});
478465
}
479466

480467
/**
@@ -741,7 +728,10 @@ public ThreadInfo[] getThreadListing() {
741728

742729
public IDebugFrame[] getStackTrace(long jdwpThreadId) {
743730
var thread = threadMap_.getThreadByJdwpIdOrFail(new JdwpThreadID(jdwpThreadId));
744-
return GlobalIDebugManagerHolder.debugManager.getCfStack(thread);
731+
System.out.println("[luceedebug] getStackTrace: jdwpThreadId=" + jdwpThreadId + " -> thread=" + thread.getName() + " (id=" + thread.getId() + ") identity=" + System.identityHashCode(thread));
732+
var frames = GlobalIDebugManagerHolder.debugManager.getCfStack(thread);
733+
System.out.println("[luceedebug] getStackTrace: returning " + frames.length + " frames");
734+
return frames;
745735
}
746736

747737
public IDebugEntity[] getScopes(long frameID) {
@@ -819,25 +809,18 @@ private BpLineAndId[] freshBpLineAndIdRecordsFromLines(RawIdePath idePath, Canon
819809
}
820810

821811
public IBreakpoint[] bindBreakpoints(RawIdePath idePath, CanonicalServerAbsPath serverPath, int[] lines, String[] exprs) {
822-
// Register native breakpoints (Lucee7+)
823-
// Clear existing native breakpoints for this file first
824-
NativeDebuggerListener.clearBreakpointsForFile(serverPath.get());
825-
for (int line : lines) {
826-
NativeDebuggerListener.addBreakpoint(serverPath.get(), line);
827-
}
828-
829-
// In native-only mode, skip JDWP breakpoint registration entirely
830-
if (NativeDebuggerListener.isNativeOnlyMode()) {
831-
// Return unbound breakpoints - native breakpoints don't have JDWP binding info
812+
if (NativeDebuggerListener.isNativeMode()) {
813+
NativeDebuggerListener.clearBreakpointsForFile(serverPath.get());
814+
for (int line : lines) {
815+
NativeDebuggerListener.addBreakpoint(serverPath.get(), line);
816+
}
832817
var lineInfo = freshBpLineAndIdRecordsFromLines(idePath, serverPath, lines, exprs);
833818
IBreakpoint[] result = new Breakpoint[lineInfo.length];
834819
for (int i = 0; i < lineInfo.length; i++) {
835-
// Mark as bound since native breakpoints are always "bound" (no class loading dependency)
836820
result[i] = Breakpoint.Bound(lineInfo[i].line, lineInfo[i].id);
837821
}
838822
return result;
839823
}
840-
841824
return __internal__bindBreakpoints(serverPath, freshBpLineAndIdRecordsFromLines(idePath, serverPath, lines, exprs));
842825
}
843826

@@ -967,14 +950,10 @@ private void clearExistingBreakpoints(CanonicalServerAbsPath absPath) {
967950
}
968951

969952
public void clearAllBreakpoints() {
970-
// Clear native breakpoints (Lucee7+)
971-
NativeDebuggerListener.clearAllBreakpoints();
972-
973-
// In native-only mode, skip JDWP operations
974-
if (NativeDebuggerListener.isNativeOnlyMode()) {
953+
if (NativeDebuggerListener.isNativeMode()) {
954+
NativeDebuggerListener.clearAllBreakpoints();
975955
return;
976956
}
977-
978957
replayableBreakpointRequestsByAbsPath_.clear();
979958
vm_.eventRequestManager().deleteAllBreakpoints();
980959
}
@@ -1016,10 +995,10 @@ private void continue_(ThreadReference threadRef) {
1016995
}
1017996

1018997
public void continueAll() {
1019-
// Resume all natively suspended threads (Lucee7+ native breakpoints)
1020-
NativeDebuggerListener.resumeAllNativeThreads();
1021-
1022-
// Resume all JDWP suspended threads
998+
if (NativeDebuggerListener.isNativeMode()) {
999+
NativeDebuggerListener.resumeAllNativeThreads();
1000+
return;
1001+
}
10231002
// avoid concurrent modification exceptions, calling continue_ mutates `suspendedThreads`
10241003
Arrays
10251004
// TODO: Set<T>.toArray(sz -> new T[sz]) is not typesafe, changing the type of Set<T>
@@ -1042,14 +1021,10 @@ public void stepIn(long jdwpThreadID) {
10421021
}
10431022

10441023
public void continue_(long threadID) {
1045-
// First, try to resume as a natively suspended thread (Lucee7+ native breakpoints)
1046-
// Native breakpoints use Java thread IDs directly
1047-
if (NativeDebuggerListener.resumeNativeThread(threadID)) {
1048-
System.out.println("[luceedebug] Resumed natively suspended thread: " + threadID);
1024+
if (NativeDebuggerListener.isNativeMode()) {
1025+
NativeDebuggerListener.resumeNativeThread(threadID);
10491026
return;
10501027
}
1051-
1052-
// Fall back to JDWP resume
10531028
continue_(new JdwpThreadID(threadID));
10541029
}
10551030

0 commit comments

Comments
 (0)