Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions core/src/main/java/lucee/runtime/CFMLFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,9 @@ public void checkTimeout() {
if (pc == null) continue;
long timeout = pc.getRequestTimeout();
Thread th;
// reached timeout
if (pc.getStartTime() + timeout < System.currentTimeMillis() && Long.MAX_VALUE != timeout) {
// reached timeout (adjusted for debugger suspend time)
long suspendedMillis = pc.getDebuggerTotalSuspendedMillis();
if (pc.getStartTime() + timeout + suspendedMillis < System.currentTimeMillis() && Long.MAX_VALUE != timeout) {
Log log = ThreadLocalPageContext.getLog(pc, "requesttimeout");
if (reachedConcurrentReqThreshold() && reachedMemoryThreshold() && reachedCPUThreshold()) {
if (log != null) {
Expand Down
259 changes: 257 additions & 2 deletions core/src/main/java/lucee/runtime/PageContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
import lucee.runtime.cache.tag.include.IncludeCacheItem;
import lucee.runtime.component.ComponentLoader;
import lucee.runtime.config.Config;
import lucee.runtime.config.ConfigImpl;
import lucee.runtime.config.ConfigPro;
import lucee.runtime.config.ConfigUtil;
import lucee.runtime.config.ConfigWeb;
Expand All @@ -110,6 +111,8 @@
import lucee.runtime.debug.DebugEntryTemplate;
import lucee.runtime.debug.Debugger;
import lucee.runtime.debug.DebuggerImpl;
import lucee.runtime.debug.DebuggerListener;
import lucee.runtime.debug.DebuggerRegistry;
import lucee.runtime.dump.DumpUtil;
import lucee.runtime.dump.DumpWriter;
import lucee.runtime.engine.ExecutionLog;
Expand All @@ -126,6 +129,7 @@
import lucee.runtime.exp.MissingIncludeException;
import lucee.runtime.exp.NoLongerSupported;
import lucee.runtime.exp.PageException;
import lucee.runtime.exp.PageExceptionImpl;
import lucee.runtime.exp.PageExceptionBox;
import lucee.runtime.exp.PageRuntimeException;
import lucee.runtime.exp.PageServletException;
Expand Down Expand Up @@ -2344,6 +2348,8 @@ public void handlePageException(PageException pe) {

public void handlePageException(final PageException pe, boolean setHeader) {
if (!Abort.isSilentAbort(pe)) {
// Note: Debugger exception notification now happens in _setCatch() where frames are still intact

// if(requestTimeoutException!=null)
// pe=Caster.toPageException(requestTimeoutException);

Expand Down Expand Up @@ -3429,8 +3435,34 @@ public void setCatch(PageException pe, String name, boolean caught, boolean stor
}

public void _setCatch(PageException pe, String name, boolean caught, boolean store, boolean signal) {
if (signal && fdEnabled) {
FDSignal.signal(pe, caught);
if (signal && pe != null) {
// FusionDebug support
if (fdEnabled) {
FDSignal.signal(pe, caught);
}
// External debugger (luceedebug) - frames are still intact at this point
if (ConfigImpl.DEBUGGER) {
DebuggerListener listener = DebuggerRegistry.getListener();
if (listener != null && listener.isClientConnected() && listener.onException( this, pe, caught )) {
// Get file/line from exception for debugger display
String file = null;
int line = 0;
if (pe instanceof PageExceptionImpl) {
PageExceptionImpl pei = (PageExceptionImpl) pe;
file = pei.getFile( getConfig() );
try {
String lineStr = pei.getLine( getConfig() );
if (lineStr != null && !lineStr.isEmpty()) {
line = Integer.parseInt( lineStr );
}
}
catch (NumberFormatException ignored) {
}
}
String label = caught ? "Caught exception: " : "Uncaught exception: ";
debuggerSuspend( file, line, label + pe.getClass().getSimpleName() );
}
}
}
// boolean outer = exception != null && exception == pe;
exception = pe;
Expand Down Expand Up @@ -3499,6 +3531,229 @@ public void removeUDF() {
if (!udfs.isEmpty()) udfs.removeLast();
}

// ==================== Debugger Stack Frame Support ====================

/**
* Represents a captured CFML stack frame for external debugger inspection.
* Stores references to scopes at the time of function entry so debuggers can
* inspect variables in any frame, not just the current one.
*/
public static final class DebuggerFrame {
public final lucee.runtime.type.scope.Local local;
public final lucee.runtime.type.scope.Argument arguments;
public final lucee.runtime.type.scope.Variables variables;
public final PageSource pageSource;
public final String functionName;
private volatile int line;

DebuggerFrame(lucee.runtime.type.scope.Local local, lucee.runtime.type.scope.Argument arguments,
lucee.runtime.type.scope.Variables variables, PageSource pageSource, String functionName) {
this.local = local;
this.arguments = arguments;
this.variables = variables;
this.pageSource = pageSource;
this.functionName = functionName;
this.line = 0;
}

public int getLine() { return line; }
public void setLine(int line) { this.line = line; }
public String getFile() { return pageSource != null ? pageSource.getDisplayPath() : null; }
}

private final LinkedList<DebuggerFrame> debuggerFrames = ConfigImpl.DEBUGGER ? new LinkedList<DebuggerFrame>() : null;

/**
* Push a new debugger frame onto the stack. Called on UDF entry when DEBUGGER is enabled.
*/
public void pushDebuggerFrame(lucee.runtime.type.scope.Local local, lucee.runtime.type.scope.Argument arguments,
lucee.runtime.type.scope.Variables variables, PageSource pageSource, String functionName, int startLine) {
if (debuggerFrames != null) {
debuggerFrames.add(new DebuggerFrame(local, arguments, variables, pageSource, functionName));

// Notify debugger listener of function entry (for function breakpoints)
DebuggerListener listener = DebuggerRegistry.getListener();
if (listener != null && listener.isClientConnected()) {
String file = pageSource != null ? pageSource.getDisplayPath() : null;
String componentName = (variables instanceof ComponentScope)
? ((ComponentScope) variables).getComponent().getName()
: null;
if (listener.onFunctionEntry(this, functionName, componentName, file, startLine)) {
debuggerSuspend(file, startLine, "function breakpoint: " + functionName);
}
}
}
}

/**
* Pop the topmost debugger frame. Called on UDF exit when DEBUGGER is enabled.
*/
public void popDebuggerFrame() {
if (debuggerFrames != null && !debuggerFrames.isEmpty()) {
debuggerFrames.removeLast();
}
}

/**
* Get all debugger frames for the current call stack.
* Returns null if debugger is not enabled.
*/
public DebuggerFrame[] getDebuggerFrames() {
if (debuggerFrames == null) return null;
return debuggerFrames.toArray(new DebuggerFrame[0]);
}

/**
* Update the line number of the topmost debugger frame.
* Called on each CFML line when stepping/breakpoints are active.
*/
public void setDebuggerLine(int line) {
if (debuggerFrames != null && !debuggerFrames.isEmpty()) {
debuggerFrames.getLast().setLine(line);
}
}

/**
* Get the topmost debugger frame, or null if none.
*/
public DebuggerFrame getTopmostDebuggerFrame() {
if (debuggerFrames == null || debuggerFrames.isEmpty()) return null;
return debuggerFrames.getLast();
}

// Debugger suspension support
private volatile boolean debuggerSuspended = false;
private volatile String debuggerSuspendLabel = null;
private final Object debuggerSuspendLock = new Object();
private long debuggerSuspendStartNano = 0;
private long debuggerTotalSuspendedNanos = 0;

/**
* Suspend execution for debugger. Call from breakpoint() BIF or when hitting a breakpoint.
* Thread will wait until debuggerResume() is called.
* @param label Optional label to identify the breakpoint in debugger UI
*/
public void debuggerSuspend(String label) {
if (!ConfigImpl.DEBUGGER) return;

// Get current file/line for listener callback
DebuggerFrame frame = getTopmostDebuggerFrame();
String file = null;
int line = 0;
if (frame != null) {
file = frame.getFile();
line = frame.getLine();
}
else {
// Top-level code (outside functions) - try ExecutionLog's thread-local first
file = lucee.runtime.engine.DebuggerExecutionLog.getCurrentFile();
line = lucee.runtime.engine.DebuggerExecutionLog.getCurrentLine();

// Fall back to page source for file if thread-local not set
if (file == null) {
PageSource ps = getCurrentPageSource(null);
if (ps != null) {
lucee.commons.io.res.Resource res = ps.getPhyscalFile();
if (res != null) {
file = res.getAbsolutePath();
}
}
}
}

debuggerSuspendImpl(file, line, label);
}

/**
* Suspend execution for debugger with explicit file and line.
* Used by DebuggerExecutionLog which already knows the current location.
* @param file Source file path
* @param line Line number
* @param label Optional label to identify the breakpoint in debugger UI
*/
public void debuggerSuspend(String file, int line, String label) {
if (!ConfigImpl.DEBUGGER) return;
debuggerSuspendImpl(file, line, label);
}

private void debuggerSuspendImpl(String file, int line, String label) {
debuggerSuspendLabel = label;
debuggerSuspended = true;
debuggerSuspendStartNano = System.nanoTime();

// Notify listener before blocking
DebuggerListener listener = DebuggerRegistry.getListener();
if (listener != null) {
listener.onSuspend(this, file, line, label);
}

synchronized (debuggerSuspendLock) {
while (debuggerSuspended) {
try {
debuggerSuspendLock.wait();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
LogUtil.log(this, "application", "debugger", e, Log.LEVEL_WARN);
break;
}
}
}
debuggerTotalSuspendedNanos += System.nanoTime() - debuggerSuspendStartNano;
debuggerSuspendLabel = null;

// Notify listener after resuming
if (listener != null) {
listener.onResume(this);
}
}

/**
* Resume execution after debugger suspension.
*/
public void debuggerResume() {
debuggerSuspended = false;
synchronized (debuggerSuspendLock) {
debuggerSuspendLock.notify();
}
}

/**
* Check if this PageContext is currently suspended.
*/
public boolean isDebuggerSuspended() {
return debuggerSuspended;
}

/**
* Get the label of the current suspension point, or null.
*/
public String getDebuggerSuspendLabel() {
return debuggerSuspendLabel;
}

/**
* Get total time spent suspended (for adjusting request timeouts).
*/
public long getDebuggerTotalSuspendedNanos() {
return debuggerTotalSuspendedNanos;
}

/**
* Get total time spent suspended in milliseconds, including current suspend if active.
* Used for adjusting request timeout calculations.
*/
public long getDebuggerTotalSuspendedMillis() {
long total = debuggerTotalSuspendedNanos;
// If currently suspended, add the time since suspend started
if (debuggerSuspended && debuggerSuspendStartNano > 0) {
total += System.nanoTime() - debuggerSuspendStartNano;
}
return total / 1_000_000; // Convert nanos to millis
}

// ==================== End Debugger Stack Frame Support ====================

public FTPPoolImpl getFTPPool() {
if (ftpPool == null) ftpPool = new FTPPoolImpl();
return ftpPool;
Expand Down
24 changes: 12 additions & 12 deletions core/src/main/java/lucee/runtime/config/ConfigAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -1861,18 +1861,8 @@ private void _updateStartupHook(ClassDefinition cd) throws PageException {

// make sure the class exists
setClass(child, null, "", cd);

// now unload again, JDBC driver can be loaded when necessary
if (cd.isBundle()) {
Bundle bl = OSGiUtil.getBundleLoaded(cd.getName(), cd.getVersion(), null);
if (bl != null) {
try {
OSGiUtil.uninstall(bl);
}
catch (BundleException e) {
}
}
}
// Note: Unlike JDBC drivers, startup hooks are NOT lazy-loaded - they are instantiated
// immediately via resetStartups().getStartups(). Do not uninstall the bundle here.
}

private void _updateStartupHook(String component) {
Expand Down Expand Up @@ -5053,6 +5043,11 @@ else if (!StringUtil.isEmpty(cfc, true)) {
filter.add("resetStartups");
reloadNecessary = true;
logger.info("extension", "Update Startup Hook [" + cfc + "] from extension [" + rhext.getMetadata().getName() + ":" + rhext.getVersion() + "]");
}
// neither valid class nor component - log error
else {
logger.error("extension", "Startup Hook from extension [" + rhext.getMetadata().getName() + ":" + rhext.getVersion()
+ "] could not be registered: class definition [" + cd + "] is not a valid OSGi bundle (missing bundle-name/bundle-version?) and no component specified");
}
}
}
Expand Down Expand Up @@ -5183,6 +5178,11 @@ else if (!StringUtil.isEmpty(cfc, true)) {
reloadNecessary = true;
storeAndReload(false, true, reload && reloadNecessary, false);

// Trigger startup hooks if they were added/updated (LDEV-5955)
if (filter.allow("resetStartups")) {
((ConfigImpl) config).resetStartups().getStartups();
}

}
catch (Throwable t) {
ExceptionUtil.rethrowIfNecessary(t);
Expand Down
Loading