Skip to content

Commit e956f4d

Browse files
committed
Merge branch '7.2' of https://github.com/lucee/Lucee into 7.2
2 parents e7ca79b + d73152e commit e956f4d

File tree

5 files changed

+95
-98
lines changed

5 files changed

+95
-98
lines changed

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

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,6 +2834,11 @@ private final void execute(PageSource ps, boolean throwExcpetion, boolean onlyTo
28342834
if (fdEnabled) {
28352835
FDSignal.signal(pe, false);
28362836
}
2837+
// External debugger extension - uncaught exception
2838+
if (debuggerFrames != null) {
2839+
DebuggerListener debugListener = DebuggerRegistry.getListener();
2840+
debuggerNotifyException(debugListener, pe, false);
2841+
}
28372842
listener.onError(this, pe); // call Application.onError()
28382843
}
28392844
else log(false);
@@ -3435,27 +3440,10 @@ public void _setCatch(PageException pe, String name, boolean caught, boolean sto
34353440
if (fdEnabled) {
34363441
FDSignal.signal(pe, caught);
34373442
}
3438-
// External debugger (luceedebug) - frames are still intact at this point
3443+
// External debugger extension - frames are still intact at this point
34393444
if (debuggerFrames != null) {
34403445
DebuggerListener listener = DebuggerRegistry.getListener();
3441-
if (listener != null && listener.isClientConnected() && listener.onException(this, pe, caught)) {
3442-
// Get file/line from exception for debugger display
3443-
String file = null;
3444-
int line = 0;
3445-
if (pe instanceof PageExceptionImpl) {
3446-
PageExceptionImpl pei = (PageExceptionImpl) pe;
3447-
file = pei.getFile(getConfig());
3448-
try {
3449-
String lineStr = pei.getLine(getConfig());
3450-
if (lineStr != null && !lineStr.isEmpty()) {
3451-
line = Integer.parseInt(lineStr);
3452-
}
3453-
}
3454-
catch (NumberFormatException ignored) {}
3455-
}
3456-
String label = caught ? "Caught exception: " : "Uncaught exception: ";
3457-
debuggerSuspend(file, line, label + pe.getClass().getSimpleName());
3458-
}
3446+
debuggerNotifyException(listener, pe, caught);
34593447
}
34603448
}
34613449
// boolean outer = exception != null && exception == pe;
@@ -3592,22 +3580,13 @@ public void popDebuggerFrame() {
35923580

35933581
/**
35943582
* Get all debugger frames for the current call stack. Returns null if debugger is not enabled.
3583+
* Called via reflection by the debugger extension.
35953584
*/
35963585
public DebuggerFrame[] getDebuggerFrames() {
35973586
if (debuggerFrames == null) return null;
35983587
return debuggerFrames.toArray(new DebuggerFrame[0]);
35993588
}
36003589

3601-
/**
3602-
* Update the line number of the topmost debugger frame. Called on each CFML line when
3603-
* stepping/breakpoints are active.
3604-
*/
3605-
public void setDebuggerLine(int line) {
3606-
if (debuggerFrames != null && !debuggerFrames.isEmpty()) {
3607-
debuggerFrames.getLast().setLine(line);
3608-
}
3609-
}
3610-
36113590
/**
36123591
* Get the topmost debugger frame, or null if none.
36133592
*/
@@ -3616,6 +3595,29 @@ public DebuggerFrame getTopmostDebuggerFrame() {
36163595
return debuggerFrames.getLast();
36173596
}
36183597

3598+
/**
3599+
* Notify the debugger listener of an exception and suspend if requested.
3600+
*/
3601+
private void debuggerNotifyException(DebuggerListener listener, PageException pe, boolean caught) {
3602+
if (listener == null || !listener.isClientConnected() || !listener.onException(this, pe, caught)) return;
3603+
String file = null;
3604+
int line = 0;
3605+
if (pe instanceof PageExceptionImpl) {
3606+
PageExceptionImpl pei = (PageExceptionImpl) pe;
3607+
file = pei.getFile(getConfig());
3608+
try {
3609+
String lineStr = pei.getLine(getConfig());
3610+
if (lineStr != null && !lineStr.isEmpty()) {
3611+
line = Integer.parseInt(lineStr);
3612+
}
3613+
}
3614+
catch (NumberFormatException ignored) {
3615+
}
3616+
}
3617+
String label = caught ? "Caught exception: " : "Uncaught exception: ";
3618+
debuggerSuspend(file, line, label + pe.getClass().getSimpleName());
3619+
}
3620+
36193621
// Debugger suspension support
36203622
private volatile boolean debuggerSuspended = false;
36213623
private volatile String debuggerSuspendLabel = null;
@@ -3687,13 +3689,18 @@ private void debuggerSuspendImpl(String file, int line, String label) {
36873689
synchronized (debuggerSuspendLock) {
36883690
while (debuggerSuspended) {
36893691
try {
3690-
debuggerSuspendLock.wait();
3692+
debuggerSuspendLock.wait(5000);
36913693
}
36923694
catch (InterruptedException e) {
36933695
Thread.currentThread().interrupt();
36943696
LogUtil.log(this, "application", "debugger", e, Log.LEVEL_WARN);
36953697
break;
36963698
}
3699+
// Auto-resume if debugger disconnected while suspended
3700+
if (debuggerSuspended && !DebuggerRegistry.isClientConnected()) {
3701+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger disconnected while thread suspended - auto-resuming");
3702+
debuggerSuspended = false;
3703+
}
36973704
}
36983705
}
36993706
debuggerTotalSuspendedNanos += System.nanoTime() - debuggerSuspendStartNano;
@@ -3707,6 +3714,7 @@ private void debuggerSuspendImpl(String file, int line, String label) {
37073714

37083715
/**
37093716
* Resume execution after debugger suspension.
3717+
* Called via reflection by the debugger extension.
37103718
*/
37113719
public void debuggerResume() {
37123720
debuggerSuspended = false;
@@ -3717,25 +3725,20 @@ public void debuggerResume() {
37173725

37183726
/**
37193727
* Check if this PageContext is currently suspended.
3728+
* Available for debugger extensions via reflection.
37203729
*/
37213730
public boolean isDebuggerSuspended() {
37223731
return debuggerSuspended;
37233732
}
37243733

37253734
/**
37263735
* Get the label of the current suspension point, or null.
3736+
* Available for debugger extensions via reflection.
37273737
*/
37283738
public String getDebuggerSuspendLabel() {
37293739
return debuggerSuspendLabel;
37303740
}
37313741

3732-
/**
3733-
* Get total time spent suspended (for adjusting request timeouts).
3734-
*/
3735-
public long getDebuggerTotalSuspendedNanos() {
3736-
return debuggerTotalSuspendedNanos;
3737-
}
3738-
37393742
/**
37403743
* Get total time spent suspended in milliseconds, including current suspend if active. Used for
37413744
* 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
/**
99
* Static registry for the debugger listener. 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"); Method
1414
* setListener = registryClass.getMethod("setListener",
@@ -32,18 +32,23 @@ private DebuggerRegistry() {
3232
* @return true if registration succeeded, false if secret is invalid
3333
*/
3434
public static boolean setListener(DebuggerListener l, String secret) {
35-
// Unregister always allowed
35+
String expectedSecret = ((ConfigPro) ThreadLocalPageContext.getConfig()).getDapSecret();
36+
if (expectedSecret == null) {
37+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger registration rejected - LUCEE_DAP_SECRET not configured");
38+
return false;
39+
}
40+
if (!expectedSecret.equals(secret)) {
41+
LogUtil.log(Log.LEVEL_WARN, "application", "Debugger registration rejected - invalid secret");
42+
return false;
43+
}
3644
if (l == null) {
3745
listener = null;
38-
return true;
46+
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger unregistered");
3947
}
40-
// Registration requires valid secret
41-
String expectedSecret = ((ConfigPro) ThreadLocalPageContext.getConfig()).getDapSecret();
42-
if (expectedSecret == null || !expectedSecret.equals(secret)) {
43-
return false;
48+
else {
49+
listener = l;
50+
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger registered [" + l.getName() + "]");
4451
}
45-
listener = l;
46-
LogUtil.log(Log.LEVEL_INFO, "application", "External debugger registered [" + l.getName() + "]");
4752
return true;
4853
}
4954

@@ -56,15 +61,6 @@ public static DebuggerListener getListener() {
5661
return listener;
5762
}
5863

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

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,9 @@ public final class CFMLEngineImpl implements CFMLEngine {
230230
public static final PrintStream CONSOLE_OUT;
231231

232232
static {
233-
// Install debugger print streams if debugger is enabled
234-
boolean debuggerEnabled = Caster.toBooleanValue(SystemUtil.getSystemPropOrEnvVar("lucee.debugger.enabled", null), false);
235-
236-
if (debuggerEnabled) {
233+
// Install debugger print streams when DAP secret is set (debugger extension expected)
234+
String dapSecret = SystemUtil.getSystemPropOrEnvVar("lucee.dap.secret", null);
235+
if (dapSecret != null && !dapSecret.trim().isEmpty()) {
237236
CONSOLE_OUT = new DebuggerPrintStream(System.out, false);
238237
CONSOLE_ERR = new DebuggerPrintStream(System.err, true);
239238
System.setOut(CONSOLE_OUT);

0 commit comments

Comments
 (0)