Skip to content

Commit be43d6e

Browse files
authored
Merge pull request #2723 from zspitzer/LDEV-1402-native-debugger
LDEV-1402 lucee native debugger support
2 parents eca1e20 + dd58c68 commit be43d6e

24 files changed

+1066
-69
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,9 @@ public void checkTimeout() {
341341
if (pc == null) continue;
342342
long timeout = pc.getRequestTimeout();
343343
Thread th;
344-
// reached timeout
345-
if (pc.getStartTime() + timeout < System.currentTimeMillis() && Long.MAX_VALUE != timeout) {
344+
// reached timeout (adjusted for debugger suspend time)
345+
long suspendedMillis = pc.getDebuggerTotalSuspendedMillis();
346+
if (pc.getStartTime() + timeout + suspendedMillis < System.currentTimeMillis() && Long.MAX_VALUE != timeout) {
346347
Log log = ThreadLocalPageContext.getLog(pc, "requesttimeout");
347348
if (reachedConcurrentReqThreshold() && reachedMemoryThreshold() && reachedCPUThreshold()) {
348349
if (log != null) {

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

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import lucee.runtime.cache.tag.include.IncludeCacheItem;
9292
import lucee.runtime.component.ComponentLoader;
9393
import lucee.runtime.config.Config;
94+
import lucee.runtime.config.ConfigImpl;
9495
import lucee.runtime.config.ConfigPro;
9596
import lucee.runtime.config.ConfigUtil;
9697
import lucee.runtime.config.ConfigWeb;
@@ -109,8 +110,11 @@
109110
import lucee.runtime.debug.DebugEntryTemplate;
110111
import lucee.runtime.debug.Debugger;
111112
import lucee.runtime.debug.DebuggerImpl;
113+
import lucee.runtime.debug.DebuggerListener;
114+
import lucee.runtime.debug.DebuggerRegistry;
112115
import lucee.runtime.dump.DumpUtil;
113116
import lucee.runtime.dump.DumpWriter;
117+
import lucee.runtime.engine.DebuggerExecutionLog;
114118
import lucee.runtime.engine.ExecutionLog;
115119
import lucee.runtime.err.ErrorPage;
116120
import lucee.runtime.err.ErrorPageImpl;
@@ -125,6 +129,7 @@
125129
import lucee.runtime.exp.MissingIncludeException;
126130
import lucee.runtime.exp.NoLongerSupported;
127131
import lucee.runtime.exp.PageException;
132+
import lucee.runtime.exp.PageExceptionImpl;
128133
import lucee.runtime.exp.PageExceptionBox;
129134
import lucee.runtime.exp.PageRuntimeException;
130135
import lucee.runtime.exp.PageServletException;
@@ -2336,6 +2341,8 @@ public void handlePageException(PageException pe) {
23362341

23372342
public void handlePageException(final PageException pe, boolean setHeader) {
23382343
if (!Abort.isSilentAbort(pe)) {
2344+
// Note: Debugger exception notification now happens in _setCatch() where frames are still intact
2345+
23392346
// if(requestTimeoutException!=null)
23402347
// pe=Caster.toPageException(requestTimeoutException);
23412348

@@ -3421,8 +3428,34 @@ public void setCatch(PageException pe, String name, boolean caught, boolean stor
34213428
}
34223429

34233430
public void _setCatch(PageException pe, String name, boolean caught, boolean store, boolean signal) {
3424-
if (signal && fdEnabled) {
3425-
FDSignal.signal(pe, caught);
3431+
if (signal && pe != null) {
3432+
// FusionDebug support
3433+
if (fdEnabled) {
3434+
FDSignal.signal(pe, caught);
3435+
}
3436+
// External debugger (luceedebug) - frames are still intact at this point
3437+
if (ConfigImpl.DEBUGGER) {
3438+
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+
}
3458+
}
34263459
}
34273460
// boolean outer = exception != null && exception == pe;
34283461
exception = pe;
@@ -3491,6 +3524,227 @@ public void removeUDF() {
34913524
if (!udfs.isEmpty()) udfs.removeLast();
34923525
}
34933526

3527+
// ==================== Debugger Stack Frame Support ====================
3528+
3529+
/**
3530+
* Represents a captured CFML stack frame for external debugger inspection.
3531+
* Stores references to scopes at the time of function entry so debuggers can
3532+
* inspect variables in any frame, not just the current one.
3533+
*/
3534+
public static final class DebuggerFrame {
3535+
public final Local local;
3536+
public final Argument arguments;
3537+
public final Variables variables;
3538+
public final PageSource pageSource;
3539+
public final String functionName;
3540+
private volatile int line;
3541+
3542+
DebuggerFrame(Local local, Argument arguments, Variables variables, PageSource pageSource, String functionName) {
3543+
this.local = local;
3544+
this.arguments = arguments;
3545+
this.variables = variables;
3546+
this.pageSource = pageSource;
3547+
this.functionName = functionName;
3548+
this.line = 0;
3549+
}
3550+
3551+
public int getLine() { return line; }
3552+
public void setLine(int line) { this.line = line; }
3553+
public String getFile() { return pageSource != null ? pageSource.getDisplayPath() : null; }
3554+
}
3555+
3556+
private final LinkedList<DebuggerFrame> debuggerFrames = ConfigImpl.DEBUGGER ? new LinkedList<DebuggerFrame>() : null;
3557+
3558+
/**
3559+
* Push a new debugger frame onto the stack. Called on UDF entry when DEBUGGER is enabled.
3560+
*/
3561+
public void pushDebuggerFrame(Local local, Argument arguments, Variables variables, PageSource pageSource, String functionName, int startLine) {
3562+
if (debuggerFrames != null) {
3563+
debuggerFrames.add(new DebuggerFrame(local, arguments, variables, pageSource, functionName));
3564+
3565+
// Notify debugger listener of function entry (for function breakpoints)
3566+
DebuggerListener listener = DebuggerRegistry.getListener();
3567+
if (listener != null && listener.isClientConnected()) {
3568+
String file = pageSource != null ? pageSource.getDisplayPath() : null;
3569+
String componentName = (variables instanceof ComponentScope)
3570+
? ((ComponentScope) variables).getComponent().getName()
3571+
: null;
3572+
if (listener.onFunctionEntry(this, functionName, componentName, file, startLine)) {
3573+
debuggerSuspend(file, startLine, "function breakpoint: " + functionName);
3574+
}
3575+
}
3576+
}
3577+
}
3578+
3579+
/**
3580+
* Pop the topmost debugger frame. Called on UDF exit when DEBUGGER is enabled.
3581+
*/
3582+
public void popDebuggerFrame() {
3583+
if (debuggerFrames != null && !debuggerFrames.isEmpty()) {
3584+
debuggerFrames.removeLast();
3585+
}
3586+
}
3587+
3588+
/**
3589+
* Get all debugger frames for the current call stack.
3590+
* Returns null if debugger is not enabled.
3591+
*/
3592+
public DebuggerFrame[] getDebuggerFrames() {
3593+
if (debuggerFrames == null) return null;
3594+
return debuggerFrames.toArray(new DebuggerFrame[0]);
3595+
}
3596+
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+
3607+
/**
3608+
* Get the topmost debugger frame, or null if none.
3609+
*/
3610+
public DebuggerFrame getTopmostDebuggerFrame() {
3611+
if (debuggerFrames == null || debuggerFrames.isEmpty()) return null;
3612+
return debuggerFrames.getLast();
3613+
}
3614+
3615+
// Debugger suspension support
3616+
private volatile boolean debuggerSuspended = false;
3617+
private volatile String debuggerSuspendLabel = null;
3618+
private final Object debuggerSuspendLock = new Object();
3619+
private long debuggerSuspendStartNano = 0;
3620+
private long debuggerTotalSuspendedNanos = 0;
3621+
3622+
/**
3623+
* Suspend execution for debugger. Call from breakpoint() BIF or when hitting a breakpoint.
3624+
* Thread will wait until debuggerResume() is called.
3625+
* @param label Optional label to identify the breakpoint in debugger UI
3626+
*/
3627+
public void debuggerSuspend(String label) {
3628+
if (!ConfigImpl.DEBUGGER) return;
3629+
3630+
// Get current file/line for listener callback
3631+
DebuggerFrame frame = getTopmostDebuggerFrame();
3632+
String file = null;
3633+
int line = 0;
3634+
if (frame != null) {
3635+
file = frame.getFile();
3636+
line = frame.getLine();
3637+
}
3638+
else {
3639+
// Top-level code (outside functions) - try ExecutionLog's thread-local first
3640+
file = DebuggerExecutionLog.getCurrentFile();
3641+
line = DebuggerExecutionLog.getCurrentLine();
3642+
3643+
// Fall back to page source for file if thread-local not set
3644+
if (file == null) {
3645+
PageSource ps = getCurrentPageSource(null);
3646+
if (ps != null) {
3647+
Resource res = ps.getPhyscalFile();
3648+
if (res != null) {
3649+
file = res.getAbsolutePath();
3650+
}
3651+
}
3652+
}
3653+
}
3654+
3655+
debuggerSuspendImpl(file, line, label);
3656+
}
3657+
3658+
/**
3659+
* Suspend execution for debugger with explicit file and line.
3660+
* Used by DebuggerExecutionLog which already knows the current location.
3661+
* @param file Source file path
3662+
* @param line Line number
3663+
* @param label Optional label to identify the breakpoint in debugger UI
3664+
*/
3665+
public void debuggerSuspend(String file, int line, String label) {
3666+
if (!ConfigImpl.DEBUGGER) return;
3667+
debuggerSuspendImpl(file, line, label);
3668+
}
3669+
3670+
private void debuggerSuspendImpl(String file, int line, String label) {
3671+
debuggerSuspendLabel = label;
3672+
debuggerSuspended = true;
3673+
debuggerSuspendStartNano = System.nanoTime();
3674+
3675+
// Notify listener before blocking
3676+
DebuggerListener listener = DebuggerRegistry.getListener();
3677+
if (listener != null) {
3678+
listener.onSuspend(this, file, line, label);
3679+
}
3680+
3681+
synchronized (debuggerSuspendLock) {
3682+
while (debuggerSuspended) {
3683+
try {
3684+
debuggerSuspendLock.wait();
3685+
}
3686+
catch (InterruptedException e) {
3687+
Thread.currentThread().interrupt();
3688+
LogUtil.log(this, "application", "debugger", e, Log.LEVEL_WARN);
3689+
break;
3690+
}
3691+
}
3692+
}
3693+
debuggerTotalSuspendedNanos += System.nanoTime() - debuggerSuspendStartNano;
3694+
debuggerSuspendLabel = null;
3695+
3696+
// Notify listener after resuming
3697+
if (listener != null) {
3698+
listener.onResume(this);
3699+
}
3700+
}
3701+
3702+
/**
3703+
* Resume execution after debugger suspension.
3704+
*/
3705+
public void debuggerResume() {
3706+
debuggerSuspended = false;
3707+
synchronized (debuggerSuspendLock) {
3708+
debuggerSuspendLock.notify();
3709+
}
3710+
}
3711+
3712+
/**
3713+
* Check if this PageContext is currently suspended.
3714+
*/
3715+
public boolean isDebuggerSuspended() {
3716+
return debuggerSuspended;
3717+
}
3718+
3719+
/**
3720+
* Get the label of the current suspension point, or null.
3721+
*/
3722+
public String getDebuggerSuspendLabel() {
3723+
return debuggerSuspendLabel;
3724+
}
3725+
3726+
/**
3727+
* Get total time spent suspended (for adjusting request timeouts).
3728+
*/
3729+
public long getDebuggerTotalSuspendedNanos() {
3730+
return debuggerTotalSuspendedNanos;
3731+
}
3732+
3733+
/**
3734+
* Get total time spent suspended in milliseconds, including current suspend if active.
3735+
* Used for adjusting request timeout calculations.
3736+
*/
3737+
public long getDebuggerTotalSuspendedMillis() {
3738+
long total = debuggerTotalSuspendedNanos;
3739+
// If currently suspended, add the time since suspend started
3740+
if (debuggerSuspended && debuggerSuspendStartNano > 0) {
3741+
total += System.nanoTime() - debuggerSuspendStartNano;
3742+
}
3743+
return total / 1_000_000; // Convert nanos to millis
3744+
}
3745+
3746+
// ==================== End Debugger Stack Frame Support ====================
3747+
34943748
/*
34953749
* *
34963750
*

0 commit comments

Comments
 (0)