Skip to content

Commit

Permalink
make captured scopes available in debug output
Browse files Browse the repository at this point in the history
  • Loading branch information
softwareCobbler committed Dec 9, 2023
1 parent 6fd3c81 commit 0ad140c
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 3 deletions.
27 changes: 24 additions & 3 deletions luceedebug/src/main/java/luceedebug/LuceeTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import org.objectweb.asm.*;

import luceedebug.instrumenter.CfmOrCfc;

import java.security.ProtectionDomain;
import java.util.ArrayList;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
59 changes: 59 additions & 0 deletions luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<CfEntityRef> refTracker;

Expand Down Expand Up @@ -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<lucee.runtime.type.scope.ClosureScope> capturedScopeChain = null;

static private final ConcurrentMap<PageContext, Object> activeFrameLockByPageContext = new MapMaker()
.weakKeys()
Expand All @@ -105,6 +115,36 @@ static class FrameContext {
this.variables = getScopeOrNull(() -> pageContext.variablesScope());
}

public ArrayList<lucee.runtime.type.scope.ClosureScope> getCapturedScopeChain() {
if (capturedScopeChain == null) {
capturedScopeChain = getCapturedScopeChain(variables);
}
return capturedScopeChain;
}

private static ArrayList<lucee.runtime.type.scope.ClosureScope> 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<lucee.runtime.type.scope.ClosureScope>();
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> {
T get() throws Throwable;
}
Expand Down Expand Up @@ -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;
}
}
}

/**
Expand Down
44 changes: 44 additions & 0 deletions luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit 0ad140c

Please sign in to comment.