|
91 | 91 | import lucee.runtime.cache.tag.include.IncludeCacheItem; |
92 | 92 | import lucee.runtime.component.ComponentLoader; |
93 | 93 | import lucee.runtime.config.Config; |
| 94 | +import lucee.runtime.config.ConfigImpl; |
94 | 95 | import lucee.runtime.config.ConfigPro; |
95 | 96 | import lucee.runtime.config.ConfigUtil; |
96 | 97 | import lucee.runtime.config.ConfigWeb; |
|
109 | 110 | import lucee.runtime.debug.DebugEntryTemplate; |
110 | 111 | import lucee.runtime.debug.Debugger; |
111 | 112 | import lucee.runtime.debug.DebuggerImpl; |
| 113 | +import lucee.runtime.debug.DebuggerListener; |
| 114 | +import lucee.runtime.debug.DebuggerRegistry; |
112 | 115 | import lucee.runtime.dump.DumpUtil; |
113 | 116 | import lucee.runtime.dump.DumpWriter; |
| 117 | +import lucee.runtime.engine.DebuggerExecutionLog; |
114 | 118 | import lucee.runtime.engine.ExecutionLog; |
115 | 119 | import lucee.runtime.err.ErrorPage; |
116 | 120 | import lucee.runtime.err.ErrorPageImpl; |
|
125 | 129 | import lucee.runtime.exp.MissingIncludeException; |
126 | 130 | import lucee.runtime.exp.NoLongerSupported; |
127 | 131 | import lucee.runtime.exp.PageException; |
| 132 | +import lucee.runtime.exp.PageExceptionImpl; |
128 | 133 | import lucee.runtime.exp.PageExceptionBox; |
129 | 134 | import lucee.runtime.exp.PageRuntimeException; |
130 | 135 | import lucee.runtime.exp.PageServletException; |
@@ -2336,6 +2341,8 @@ public void handlePageException(PageException pe) { |
2336 | 2341 |
|
2337 | 2342 | public void handlePageException(final PageException pe, boolean setHeader) { |
2338 | 2343 | if (!Abort.isSilentAbort(pe)) { |
| 2344 | + // Note: Debugger exception notification now happens in _setCatch() where frames are still intact |
| 2345 | + |
2339 | 2346 | // if(requestTimeoutException!=null) |
2340 | 2347 | // pe=Caster.toPageException(requestTimeoutException); |
2341 | 2348 |
|
@@ -3421,8 +3428,34 @@ public void setCatch(PageException pe, String name, boolean caught, boolean stor |
3421 | 3428 | } |
3422 | 3429 |
|
3423 | 3430 | 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 | + } |
3426 | 3459 | } |
3427 | 3460 | // boolean outer = exception != null && exception == pe; |
3428 | 3461 | exception = pe; |
@@ -3491,6 +3524,227 @@ public void removeUDF() { |
3491 | 3524 | if (!udfs.isEmpty()) udfs.removeLast(); |
3492 | 3525 | } |
3493 | 3526 |
|
| 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 | + |
3494 | 3748 | /* |
3495 | 3749 | * * |
3496 | 3750 | * |
|
0 commit comments