From 0ad140c609d3aafc3fa6bc99ec68a9d6a67e4cf9 Mon Sep 17 00:00:00 2001 From: David Rogers Date: Sat, 9 Dec 2023 10:15:16 -0600 Subject: [PATCH] make captured scopes available in debug output --- .../java/luceedebug/LuceeTransformer.java | 27 ++++++++- .../ClosureScopeLocalScopeAccessorShim.java | 8 +++ .../luceedebug/coreinject/DebugFrame.java | 59 +++++++++++++++++++ .../luceedebug/instrumenter/ClosureScope.java | 44 ++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 luceedebug/src/main/java/luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim.java create mode 100644 luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java diff --git a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java index 51ffc97..86ffb1c 100644 --- a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java +++ b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java @@ -5,8 +5,6 @@ import org.objectweb.asm.*; -import luceedebug.instrumenter.CfmOrCfc; - import java.security.ProtectionDomain; import java.util.ArrayList; @@ -86,6 +84,9 @@ public byte[] transform(ClassLoader loader, if (className.equals("org/apache/felix/framework/Felix")) { return instrumentFelix(classfileBuffer, loader); } + else if (className.equals("lucee/runtime/type/scope/ClosureScope")) { + return instrumentClosureScope(classfileBuffer); + } else if (className.equals("lucee/runtime/PageContextImpl")) { GlobalIDebugManagerHolder.luceeCoreLoader = loader; @@ -180,6 +181,26 @@ private byte[] instrumentPageContextImpl(final byte[] classfileBuffer) { return null; } } + + private byte[] instrumentClosureScope(final byte[] classfileBuffer) { + var classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + + try { + var instrumenter = new luceedebug.instrumenter.ClosureScope(Opcodes.ASM9, classWriter); + var classReader = new ClassReader(classfileBuffer); + + classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); + + return classWriter.toByteArray(); + } + catch (Throwable e) { + System.err.println("[luceedebug] exception during attempted classfile rewrite"); + System.err.println(e.getMessage()); + e.printStackTrace(); + System.exit(1); + return null; + } + } private byte[] instrumentCfmOrCfc(final byte[] classfileBuffer, ClassReader reader, String className) { byte[] stepInstrumentedBuffer = classfileBuffer; @@ -191,7 +212,7 @@ protected ClassLoader getClassLoader() { }; try { - var instrumenter = new CfmOrCfc(Opcodes.ASM9, classWriter, className); + var instrumenter = new luceedebug.instrumenter.CfmOrCfc(Opcodes.ASM9, classWriter, className); var classReader = new ClassReader(stepInstrumentedBuffer); classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); diff --git a/luceedebug/src/main/java/luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim.java b/luceedebug/src/main/java/luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim.java new file mode 100644 index 0000000..ec5b162 --- /dev/null +++ b/luceedebug/src/main/java/luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim.java @@ -0,0 +1,8 @@ +package luceedebug.coreinject; + +/** + * Intended to be an extension on lucee.runtime.type.scope.ClosureScope, applied during classfile rewrites during agent startup. + */ +public interface ClosureScopeLocalScopeAccessorShim { + lucee.runtime.type.scope.Scope getLocalScope(); +} diff --git a/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java b/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java index b359722..22f8e58 100644 --- a/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java +++ b/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -25,6 +26,12 @@ public class DebugFrame implements IDebugFrame { static private AtomicLong nextId = new AtomicLong(0); + /** + * It's not 100% clear that our instrumentation to walk captured closure scopes will always be valid across all class loaders, + * and we assume that if it fails once, we should disable it across the entire program. + */ + static private boolean closureScopeGloballyDisabled = false; + private ValTracker valTracker; private RefTracker refTracker; @@ -82,6 +89,9 @@ static class FrameContext { final lucee.runtime.type.scope.Scope server; final lucee.runtime.type.scope.Scope url; final lucee.runtime.type.scope.Variables variables; + + // lazy init because it (might?) be expensive to walk scope chains eagerly every frame + private ArrayList capturedScopeChain = null; static private final ConcurrentMap activeFrameLockByPageContext = new MapMaker() .weakKeys() @@ -105,6 +115,36 @@ static class FrameContext { this.variables = getScopeOrNull(() -> pageContext.variablesScope()); } + public ArrayList getCapturedScopeChain() { + if (capturedScopeChain == null) { + capturedScopeChain = getCapturedScopeChain(variables); + } + return capturedScopeChain; + } + + private static ArrayList getCapturedScopeChain(lucee.runtime.type.scope.Scope variables) { + if (variables instanceof lucee.runtime.type.scope.ClosureScope) { + final var setLike_seen = new IdentityHashMap<>(); + final var result = new ArrayList(); + var scope = variables; + while (scope instanceof lucee.runtime.type.scope.ClosureScope) { + final var captured = (lucee.runtime.type.scope.ClosureScope)scope; + if (setLike_seen.containsKey(captured)) { + break; + } + else { + setLike_seen.put(captured, true); + } + result.add(captured); + scope = captured.getVariables(); + } + return result; + } + else { + return new ArrayList<>(); + } + } + interface SupplierOrNull { T get() throws Throwable; } @@ -206,6 +246,25 @@ private void lazyInitScopeRefs() { checkedPutScopeRef("server", frameContext_.server); checkedPutScopeRef("url", frameContext_.url); checkedPutScopeRef("variables", frameContext_.variables); + + if (!closureScopeGloballyDisabled) { + final var scopeChain = frameContext_.getCapturedScopeChain(); + final int captureChainLen = scopeChain.size(); + try { + for (int i = 0; i < captureChainLen; i++) { + // this should always succeed, there's no casting into a luceedebug shim type + checkedPutScopeRef("captured arguments " + i, scopeChain.get(i).getArgument()); + // this could potentially fail with a class cast exception + checkedPutScopeRef("captured local " + i, ((ClosureScopeLocalScopeAccessorShim)scopeChain.get(i)).getLocalScope()); + } + } + catch (ClassCastException e) { + // We'll be left with possibly some capture scopes in the list this time around, + // but all subsequent calls to this method will be guarded by this assignment. + closureScopeGloballyDisabled = true; + return; + } + } } /** diff --git a/luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java b/luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java new file mode 100644 index 0000000..f732aef --- /dev/null +++ b/luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java @@ -0,0 +1,44 @@ +package luceedebug.instrumenter; + +import org.objectweb.asm.*; +import org.objectweb.asm.commons.GeneratorAdapter; + +/** + * extend lucee.runtime.type.scope.ClosureScope to implement ClosureScopeLocalScopeAccessorShim + */ + +public class ClosureScope extends ClassVisitor { + public ClosureScope(int api, ClassWriter cw) { + super(api, cw); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces + ) { + final var augmentedInterfaces = new String[interfaces.length + 1]; + for (int i = 0; i < interfaces.length; i++) { + augmentedInterfaces[i] = interfaces[i]; + } + augmentedInterfaces[interfaces.length] = "luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim"; + + super.visit(version, access, name, signature, superName, augmentedInterfaces); + } + + @Override + public void visitEnd() { + final var name = "getLocalScope"; + final var descriptor = "()Llucee/runtime/type/scope/Scope;"; + final var mv = visitMethod(org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor, null, null); + final var ga = new GeneratorAdapter(mv, org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor); + ga.loadThis(); + ga.getField(org.objectweb.asm.Type.getType("Llucee/runtime/type/scope/ClosureScope;"), "local", org.objectweb.asm.Type.getType("Llucee/runtime/type/scope/Local;")); + ga.returnValue(); + ga.endMethod(); + } +}