Skip to content

Commit dab12bc

Browse files
committed
LDEV-1402 Fix debugger console output and improve DAP integration
1 parent ace7329 commit dab12bc

File tree

7 files changed

+97
-101
lines changed

7 files changed

+97
-101
lines changed

core/src/main/java/lucee/runtime/PageContextImpl.java

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2830,6 +2830,11 @@ private final void execute(PageSource ps, boolean throwExcpetion, boolean onlyTo
28302830
if (fdEnabled) {
28312831
FDSignal.signal(pe, false);
28322832
}
2833+
// External debugger extension - uncaught exception
2834+
if (ConfigImpl.DEBUGGER) {
2835+
DebuggerListener debugListener = DebuggerRegistry.getListener();
2836+
debuggerNotifyException(debugListener, pe, false);
2837+
}
28332838
listener.onError(this, pe); // call Application.onError()
28342839
}
28352840
else log(false);
@@ -3433,28 +3438,10 @@ public void _setCatch(PageException pe, String name, boolean caught, boolean sto
34333438
if (fdEnabled) {
34343439
FDSignal.signal(pe, caught);
34353440
}
3436-
// External debugger (luceedebug) - frames are still intact at this point
3441+
// External debugger extension - frames are still intact at this point
34373442
if (ConfigImpl.DEBUGGER) {
34383443
DebuggerListener listener = DebuggerRegistry.getListener();
3439-
if (listener != null && listener.isClientConnected() && listener.onException( this, pe, caught )) {
3440-
// Get file/line from exception for debugger display
3441-
String file = null;
3442-
int line = 0;
3443-
if (pe instanceof PageExceptionImpl) {
3444-
PageExceptionImpl pei = (PageExceptionImpl) pe;
3445-
file = pei.getFile( getConfig() );
3446-
try {
3447-
String lineStr = pei.getLine( getConfig() );
3448-
if (lineStr != null && !lineStr.isEmpty()) {
3449-
line = Integer.parseInt( lineStr );
3450-
}
3451-
}
3452-
catch (NumberFormatException ignored) {
3453-
}
3454-
}
3455-
String label = caught ? "Caught exception: " : "Uncaught exception: ";
3456-
debuggerSuspend( file, line, label + pe.getClass().getSimpleName() );
3457-
}
3444+
debuggerNotifyException( listener, pe, caught );
34583445
}
34593446
}
34603447
// boolean outer = exception != null && exception == pe;
@@ -3588,22 +3575,13 @@ public void popDebuggerFrame() {
35883575
/**
35893576
* Get all debugger frames for the current call stack.
35903577
* Returns null if debugger is not enabled.
3578+
* Called via reflection by the debugger extension.
35913579
*/
35923580
public DebuggerFrame[] getDebuggerFrames() {
35933581
if (debuggerFrames == null) return null;
35943582
return debuggerFrames.toArray(new DebuggerFrame[0]);
35953583
}
35963584

3597-
/**
3598-
* Update the line number of the topmost debugger frame.
3599-
* Called on each CFML line when stepping/breakpoints are active.
3600-
*/
3601-
public void setDebuggerLine(int line) {
3602-
if (debuggerFrames != null && !debuggerFrames.isEmpty()) {
3603-
debuggerFrames.getLast().setLine(line);
3604-
}
3605-
}
3606-
36073585
/**
36083586
* Get the topmost debugger frame, or null if none.
36093587
*/
@@ -3612,6 +3590,29 @@ public DebuggerFrame getTopmostDebuggerFrame() {
36123590
return debuggerFrames.getLast();
36133591
}
36143592

3593+
/**
3594+
* Notify the debugger listener of an exception and suspend if requested.
3595+
*/
3596+
private void debuggerNotifyException(DebuggerListener listener, PageException pe, boolean caught) {
3597+
if (listener == null || !listener.isClientConnected() || !listener.onException(this, pe, caught)) return;
3598+
String file = null;
3599+
int line = 0;
3600+
if (pe instanceof PageExceptionImpl) {
3601+
PageExceptionImpl pei = (PageExceptionImpl) pe;
3602+
file = pei.getFile(getConfig());
3603+
try {
3604+
String lineStr = pei.getLine(getConfig());
3605+
if (lineStr != null && !lineStr.isEmpty()) {
3606+
line = Integer.parseInt(lineStr);
3607+
}
3608+
}
3609+
catch (NumberFormatException ignored) {
3610+
}
3611+
}
3612+
String label = caught ? "Caught exception: " : "Uncaught exception: ";
3613+
debuggerSuspend(file, line, label + pe.getClass().getSimpleName());
3614+
}
3615+
36153616
// Debugger suspension support
36163617
private volatile boolean debuggerSuspended = false;
36173618
private volatile String debuggerSuspendLabel = null;
@@ -3681,13 +3682,18 @@ private void debuggerSuspendImpl(String file, int line, String label) {
36813682
synchronized (debuggerSuspendLock) {
36823683
while (debuggerSuspended) {
36833684
try {
3684-
debuggerSuspendLock.wait();
3685+
debuggerSuspendLock.wait(5000);
36853686
}
36863687
catch (InterruptedException e) {
36873688
Thread.currentThread().interrupt();
36883689
LogUtil.log(this, "application", "debugger", e, Log.LEVEL_WARN);
36893690
break;
36903691
}
3692+
// Auto-resume if debugger disconnected while suspended
3693+
if (debuggerSuspended && !DebuggerRegistry.isClientConnected()) {
3694+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger disconnected while thread suspended - auto-resuming");
3695+
debuggerSuspended = false;
3696+
}
36913697
}
36923698
}
36933699
debuggerTotalSuspendedNanos += System.nanoTime() - debuggerSuspendStartNano;
@@ -3701,6 +3707,7 @@ private void debuggerSuspendImpl(String file, int line, String label) {
37013707

37023708
/**
37033709
* Resume execution after debugger suspension.
3710+
* Called via reflection by the debugger extension.
37043711
*/
37053712
public void debuggerResume() {
37063713
debuggerSuspended = false;
@@ -3711,25 +3718,20 @@ public void debuggerResume() {
37113718

37123719
/**
37133720
* Check if this PageContext is currently suspended.
3721+
* Available for debugger extensions via reflection.
37143722
*/
37153723
public boolean isDebuggerSuspended() {
37163724
return debuggerSuspended;
37173725
}
37183726

37193727
/**
37203728
* Get the label of the current suspension point, or null.
3729+
* Available for debugger extensions via reflection.
37213730
*/
37223731
public String getDebuggerSuspendLabel() {
37233732
return debuggerSuspendLabel;
37243733
}
37253734

3726-
/**
3727-
* Get total time spent suspended (for adjusting request timeouts).
3728-
*/
3729-
public long getDebuggerTotalSuspendedNanos() {
3730-
return debuggerTotalSuspendedNanos;
3731-
}
3732-
37333735
/**
37343736
* Get total time spent suspended in milliseconds, including current suspend if active.
37353737
* Used for adjusting request timeout calculations.

core/src/main/java/lucee/runtime/debug/DebuggerListener.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import lucee.runtime.PageContext;
44

55
/**
6-
* Listener interface for external debuggers (e.g., luceedebug/VS Code DAP).
6+
* Listener interface for external debugger extensions (e.g., VS Code DAP).
77
* Allows debuggers to be notified of suspend/resume events and to register breakpoints.
88
*
99
* Only one debugger listener is supported at a time.
@@ -13,7 +13,7 @@ public interface DebuggerListener {
1313
/**
1414
* Get the name of this debugger for logging purposes.
1515
*
16-
* @return A human-readable name (e.g., "luceedebug")
16+
* @return A human-readable name (e.g., "debugger")
1717
*/
1818
String getName();
1919

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,56 @@
11
package lucee.runtime.debug;
22

3+
import java.io.FilterOutputStream;
4+
import java.io.IOException;
5+
import java.io.OutputStream;
36
import java.io.PrintStream;
47

58
/**
69
* PrintStream wrapper that tees output to the DebuggerListener.
7-
* Installed on System.out/err when DEBUGGER_SECRET is set.
10+
* Installed on System.out/err when DEBUGGER_DAP_SECRET is set.
811
* Always passes through to the original stream; only notifies listener when active.
12+
*
13+
* Uses an OutputStream wrapper because PrintStream.print/println use a private
14+
* internal write path (BufferedWriter → OutputStreamWriter → OutputStream) that
15+
* bypasses the public write(byte[]) methods. Wrapping the OutputStream ensures
16+
* ALL output paths are intercepted.
917
*/
1018
public class DebuggerPrintStream extends PrintStream {
11-
private final PrintStream original;
12-
private final boolean isStdErr;
1319

1420
public DebuggerPrintStream(PrintStream original, boolean isStdErr) {
15-
super(original, true); // autoFlush=true
16-
this.original = original;
17-
this.isStdErr = isStdErr;
21+
super(new NotifyingOutputStream(original, isStdErr), true);
1822
}
1923

20-
@Override
21-
public void write(byte[] buf, int off, int len) {
22-
super.write(buf, off, len);
23-
notifyListener(new String(buf, off, len));
24-
}
25-
26-
@Override
27-
public void write(byte[] buf) {
28-
super.write(buf, 0, buf.length);
29-
notifyListener(new String(buf));
30-
}
24+
/**
25+
* OutputStream that forwards all writes to the original stream
26+
* and notifies the DebuggerListener of new output.
27+
*/
28+
private static class NotifyingOutputStream extends FilterOutputStream {
29+
private final boolean isStdErr;
3130

32-
@Override
33-
public void write(int b) {
34-
super.write(b);
35-
// Single byte - notify immediately (some libs write char-by-char)
36-
notifyListener(String.valueOf((char) b));
37-
}
31+
NotifyingOutputStream(OutputStream original, boolean isStdErr) {
32+
super(original);
33+
this.isStdErr = isStdErr;
34+
}
3835

39-
// Note: We only override write() methods to notify the listener.
40-
// Do NOT override print/println - they internally call write() and
41-
// would cause double notifications.
36+
@Override
37+
public void write(byte[] buf, int off, int len) throws IOException {
38+
out.write(buf, off, len);
39+
notifyListener(new String(buf, off, len));
40+
}
4241

43-
private void notifyListener(String text) {
44-
if (text == null || text.isEmpty()) return;
45-
DebuggerListener listener = DebuggerRegistry.getListener();
46-
if (listener != null && listener.isClientConnected()) {
47-
listener.onOutput(text, isStdErr);
42+
@Override
43+
public void write(int b) throws IOException {
44+
out.write(b);
45+
notifyListener(String.valueOf((char) b));
4846
}
49-
}
5047

51-
/**
52-
* Get the original stream (for unwrapping if needed).
53-
*/
54-
public PrintStream getOriginal() {
55-
return original;
48+
private void notifyListener(String text) {
49+
if (text == null || text.isEmpty()) return;
50+
DebuggerListener listener = DebuggerRegistry.getListener();
51+
if (listener != null && listener.isClientConnected()) {
52+
listener.onOutput(text, isStdErr);
53+
}
54+
}
5655
}
5756
}

core/src/main/java/lucee/runtime/debug/DebuggerRegistry.java

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Static registry for the debugger listener.
99
* Only one debugger listener is supported at a time.
1010
*
11-
* External debuggers (e.g., luceedebug) can access this via reflection:
11+
* External debugger extensions can access this via reflection:
1212
*
1313
* Class<?> registryClass = Class.forName("lucee.runtime.debug.DebuggerRegistry");
1414
* Method setListener = registryClass.getMethod("setListener", Class.forName("lucee.runtime.debug.DebuggerListener"), String.class);
@@ -31,18 +31,23 @@ private DebuggerRegistry() {
3131
* @return true if registration succeeded, false if secret is invalid
3232
*/
3333
public static boolean setListener(DebuggerListener l, String secret) {
34-
// Unregister always allowed
34+
String expectedSecret = ConfigImpl.DEBUGGER_SECRET;
35+
if (expectedSecret == null) {
36+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger registration rejected - LUCEE_DAP_SECRET not configured");
37+
return false;
38+
}
39+
if (!expectedSecret.equals(secret)) {
40+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger registration rejected - invalid secret");
41+
return false;
42+
}
3543
if (l == null) {
3644
listener = null;
37-
return true;
45+
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger unregistered");
3846
}
39-
// Registration requires valid secret
40-
String expectedSecret = ConfigImpl.DEBUGGER_SECRET;
41-
if (expectedSecret == null || !expectedSecret.equals(secret)) {
42-
return false;
47+
else {
48+
listener = l;
49+
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger registered [" + l.getName() + "]");
4350
}
44-
listener = l;
45-
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger registered [" + l.getName() + "]");
4651
return true;
4752
}
4853

@@ -55,15 +60,6 @@ public static DebuggerListener getListener() {
5560
return listener;
5661
}
5762

58-
/**
59-
* Check if a debugger listener is registered.
60-
*
61-
* @return true if a listener is registered
62-
*/
63-
public static boolean hasListener() {
64-
return listener != null;
65-
}
66-
6763
/**
6864
* Check if a debugger client is connected and ready to handle breakpoints.
6965
* Returns false if no listener is registered, or if the listener is registered

core/src/main/java/lucee/runtime/engine/CFMLEngineImpl.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,9 @@ else if ("warn".equals(tmp) || "warning".equals(tmp)) {
261261
public static final PrintStream CONSOLE_OUT;
262262

263263
static {
264-
// Install debugger print streams if debugger is enabled
265-
boolean debuggerEnabled = Caster.toBooleanValue(SystemUtil.getSystemPropOrEnvVar("lucee.debugger.enabled", null), false);
266-
267-
if (debuggerEnabled) {
264+
// Install debugger print streams when DAP secret is set (debugger extension expected)
265+
String dapSecret = SystemUtil.getSystemPropOrEnvVar("lucee.dap.secret", null);
266+
if (dapSecret != null && !dapSecret.trim().isEmpty()) {
268267
CONSOLE_OUT = new DebuggerPrintStream(System.out, false);
269268
CONSOLE_ERR = new DebuggerPrintStream(System.err, true);
270269
System.setOut(CONSOLE_OUT);

loader/build.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<project default="core" basedir="." name="Lucee"
33
xmlns:resolver="antlib:org.apache.maven.resolver.ant">
44

5-
<property name="version" value="7.1.0.47-SNAPSHOT"/>
5+
<property name="version" value="7.1.0.48-SNAPSHOT"/>
66

77
<taskdef uri="antlib:org.apache.maven.resolver.ant" resource="org/apache/maven/resolver/ant/antlib.xml">
88
<classpath>

loader/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>org.lucee</groupId>
55
<artifactId>lucee</artifactId>
6-
<version>7.1.0.47-SNAPSHOT</version>
6+
<version>7.1.0.48-SNAPSHOT</version>
77
<packaging>jar</packaging>
88

99
<name>Lucee Loader Build</name>

0 commit comments

Comments
 (0)