1010
1111import java .util .ArrayList ;
1212import java .util .HashMap ;
13+ import java .util .IdentityHashMap ;
1314import java .util .LinkedHashMap ;
1415import java .util .concurrent .ConcurrentHashMap ;
1516import java .util .concurrent .ConcurrentMap ;
2526public class DebugFrame implements IDebugFrame {
2627 static private AtomicLong nextId = new AtomicLong (0 );
2728
29+ /**
30+ * It's not 100% clear that our instrumentation to walk captured closure scopes will always be valid across all class loaders,
31+ * and we assume that if it fails once, we should disable it across the entire program.
32+ */
33+ static private boolean closureScopeGloballyDisabled = false ;
34+
2835 private ValTracker valTracker ;
2936 private RefTracker <CfEntityRef > refTracker ;
3037
@@ -82,6 +89,9 @@ static class FrameContext {
8289 final lucee .runtime .type .scope .Scope server ;
8390 final lucee .runtime .type .scope .Scope url ;
8491 final lucee .runtime .type .scope .Variables variables ;
92+
93+ // lazy init because it (might?) be expensive to walk scope chains eagerly every frame
94+ private ArrayList <lucee .runtime .type .scope .ClosureScope > capturedScopeChain = null ;
8595
8696 static private final ConcurrentMap <PageContext , Object > activeFrameLockByPageContext = new MapMaker ()
8797 .weakKeys ()
@@ -105,6 +115,36 @@ static class FrameContext {
105115 this .variables = getScopeOrNull (() -> pageContext .variablesScope ());
106116 }
107117
118+ public ArrayList <lucee .runtime .type .scope .ClosureScope > getCapturedScopeChain () {
119+ if (capturedScopeChain == null ) {
120+ capturedScopeChain = getCapturedScopeChain (variables );
121+ }
122+ return capturedScopeChain ;
123+ }
124+
125+ private static ArrayList <lucee .runtime .type .scope .ClosureScope > getCapturedScopeChain (lucee .runtime .type .scope .Scope variables ) {
126+ if (variables instanceof lucee .runtime .type .scope .ClosureScope ) {
127+ final var setLike_seen = new IdentityHashMap <>();
128+ final var result = new ArrayList <lucee .runtime .type .scope .ClosureScope >();
129+ var scope = variables ;
130+ while (scope instanceof lucee .runtime .type .scope .ClosureScope ) {
131+ final var captured = (lucee .runtime .type .scope .ClosureScope )scope ;
132+ if (setLike_seen .containsKey (captured )) {
133+ break ;
134+ }
135+ else {
136+ setLike_seen .put (captured , true );
137+ }
138+ result .add (captured );
139+ scope = captured .getVariables ();
140+ }
141+ return result ;
142+ }
143+ else {
144+ return new ArrayList <>();
145+ }
146+ }
147+
108148 interface SupplierOrNull <T > {
109149 T get () throws Throwable ;
110150 }
@@ -206,6 +246,25 @@ private void lazyInitScopeRefs() {
206246 checkedPutScopeRef ("server" , frameContext_ .server );
207247 checkedPutScopeRef ("url" , frameContext_ .url );
208248 checkedPutScopeRef ("variables" , frameContext_ .variables );
249+
250+ if (!closureScopeGloballyDisabled ) {
251+ final var scopeChain = frameContext_ .getCapturedScopeChain ();
252+ final int captureChainLen = scopeChain .size ();
253+ try {
254+ for (int i = 0 ; i < captureChainLen ; i ++) {
255+ // this should always succeed, there's no casting into a luceedebug shim type
256+ checkedPutScopeRef ("captured arguments " + i , scopeChain .get (i ).getArgument ());
257+ // this could potentially fail with a class cast exception
258+ checkedPutScopeRef ("captured local " + i , ((ClosureScopeLocalScopeAccessorShim )scopeChain .get (i )).getLocalScope ());
259+ }
260+ }
261+ catch (ClassCastException e ) {
262+ // We'll be left with possibly some capture scopes in the list this time around,
263+ // but all subsequent calls to this method will be guarded by this assignment.
264+ closureScopeGloballyDisabled = true ;
265+ return ;
266+ }
267+ }
209268 }
210269
211270 /**
0 commit comments