From 9b8a3ac7b07466a01ce7884d97e836c64e10def4 Mon Sep 17 00:00:00 2001 From: David Rogers Date: Fri, 23 Feb 2024 22:02:44 -0600 Subject: [PATCH] improve inspection of component scope --- .../java/luceedebug/LuceeTransformer.java | 31 +++++++ .../luceedebug/coreinject/CfEntityRef.java | 91 ++++++++++++------- .../ComponentScopeMarkerTraitShim.java | 21 +++++ .../luceedebug/coreinject/DebugFrame.java | 14 ++- .../instrumenter/ComponentImpl.java | 45 +++++++++ 5 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 luceedebug/src/main/java/luceedebug/coreinject/ComponentScopeMarkerTraitShim.java create mode 100644 luceedebug/src/main/java/luceedebug/instrumenter/ComponentImpl.java diff --git a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java index 86ffb1c..5394988 100644 --- a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java +++ b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java @@ -87,6 +87,12 @@ public byte[] transform(ClassLoader loader, else if (className.equals("lucee/runtime/type/scope/ClosureScope")) { return instrumentClosureScope(classfileBuffer); } + else if (className.equals("lucee/runtime/ComponentImpl")) { + if (loader == null) { + throw new RuntimeException("instrumention ComponentImpl but core loader not seen yet"); + } + return instrumentComponentImpl(classfileBuffer, loader); + } else if (className.equals("lucee/runtime/PageContextImpl")) { GlobalIDebugManagerHolder.luceeCoreLoader = loader; @@ -201,6 +207,31 @@ private byte[] instrumentClosureScope(final byte[] classfileBuffer) { return null; } } + + private byte[] instrumentComponentImpl(final byte[] classfileBuffer, ClassLoader loader) { + var classWriter = new ClassWriter(/*ClassWriter.COMPUTE_FRAMES |*/ ClassWriter.COMPUTE_MAXS) { + @Override + protected ClassLoader getClassLoader() { + return loader; + } + }; + + try { + var instrumenter = new luceedebug.instrumenter.ComponentImpl(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; diff --git a/luceedebug/src/main/java/luceedebug/coreinject/CfEntityRef.java b/luceedebug/src/main/java/luceedebug/coreinject/CfEntityRef.java index 3d0429f..feb2dc2 100644 --- a/luceedebug/src/main/java/luceedebug/coreinject/CfEntityRef.java +++ b/luceedebug/src/main/java/luceedebug/coreinject/CfEntityRef.java @@ -13,48 +13,59 @@ // CfValue class CfEntityRef implements ICfEntityRef { - private final ValTracker valTracker; + private final DebugFrame frame; public final Object obj; public final long id; - public CfEntityRef(ValTracker valTracker, Object obj) { - this.valTracker = valTracker; + public CfEntityRef(DebugFrame frame, Object obj) { + this.frame = frame; this.obj = obj; - this.id = valTracker.idempotentRegisterObject(obj).id; + this.id = frame.valTracker.idempotentRegisterObject(obj).id; } public long getID() { return id; } + static class MarkerTrait { + static class Scope { + public final Map scopelike; + Scope(Map scopelike) { + this.scopelike = scopelike; + } + } + } + /** * @maybeNull_which --> null means "any type" */ + public static IDebugEntity[] getAsDebugEntity(DebugFrame frame, Object obj, IDebugEntity.DebugEntityType maybeNull_which) { + return getAsDebugEntity(frame.valTracker, obj, maybeNull_which); + } + public static IDebugEntity[] getAsDebugEntity(ValTracker valTracker, Object obj, IDebugEntity.DebugEntityType maybeNull_which) { final boolean namedOK = maybeNull_which == null || maybeNull_which == IDebugEntity.DebugEntityType.NAMED; final boolean indexedOK = maybeNull_which == null || maybeNull_which == IDebugEntity.DebugEntityType.INDEXED; - if (obj instanceof Map && namedOK) { - /* - if (cfEntity.wrapped instanceof Component) { - if (this.flags.contains(Flags.isScope) || this.flags.contains(Flags.isIterableThisRef)) { - @SuppressWarnings("unchecked") - var m = (Map)cfEntity.wrapped; - return getAsMaplike(m); - } - else { - return new IDebugEntity[] { - maybeNull_asValue("this", cfEntity.wrapped, true, true), - maybeNull_asValue("variables", ((Component)cfEntity.wrapped).getComponentScope()), - maybeNull_asValue("static", ((Component)cfEntity.wrapped).staticScope()) - }; - } - } - */ + if (obj instanceof MarkerTrait.Scope && namedOK) { @SuppressWarnings("unchecked") - var m = (Map)obj; + var m = (Map)(((MarkerTrait.Scope)obj).scopelike); return getAsMaplike(valTracker, m); } + else if (obj instanceof Map && namedOK) { + if (obj instanceof Component) { + return new IDebugEntity[] { + maybeNull_asValue(valTracker, "this", obj, true, true), + maybeNull_asValue(valTracker, "variables", ((Component)obj).getComponentScope()), + maybeNull_asValue(valTracker, "static", ((Component)obj).staticScope()) + }; + } + else { + @SuppressWarnings("unchecked") + var m = (Map)obj; + return getAsMaplike(valTracker, m); + } + } else if (obj instanceof Array && indexedOK) { return getAsCfArray(valTracker, (Array)obj); } @@ -74,18 +85,18 @@ private static IDebugEntity[] getAsMaplike(ValTracker valTracker, Map entry : entries) { - IDebugEntity val = maybeNull_asValue(valTracker, entry.getKey(), entry.getValue(), skipNoisyComponentFunctions); + IDebugEntity val = maybeNull_asValue(valTracker, entry.getKey(), entry.getValue(), skipNoisyComponentFunctions, false); if (val != null) { results.add(val); } } - { - DebugEntity val = new DebugEntity(); - val.name = "__scopeID"; - val.value = "" + valTracker.idempotentRegisterObject(map).id; - results.add(val); - } + // { + // DebugEntity val = new DebugEntity(); + // val.name = "__luceedebugValueID"; + // val.value = "" + valTracker.idempotentRegisterObject(map).id; + // results.add(val); + // } results.sort(xscopeByName); @@ -107,7 +118,7 @@ private static IDebugEntity[] getAsCfArray(ValTracker valTracker, Array array) { } public IDebugEntity maybeNull_asValue(String name) { - return maybeNull_asValue(valTracker, name, obj, true); + return maybeNull_asValue(frame.valTracker, name, obj, true, false); } /** @@ -117,14 +128,20 @@ public IDebugEntity maybeNull_asValue(String name) { * Maybe such things should be optionally included as per some configuration. */ private static IDebugEntity maybeNull_asValue(ValTracker valTracker, String name, Object obj) { - return maybeNull_asValue(valTracker, name, obj, true); + return maybeNull_asValue(valTracker, name, obj, true, false); } /** * @markDiscoveredComponentsAsIterableThisRef if true, a Component will be marked as if it were any normal Map. This drives discovery of variables; * showing the "top level" of a component we want to show its "inner scopes" (this, variables, and static) */ - private static IDebugEntity maybeNull_asValue(ValTracker valTracker, String name, Object obj, boolean skipNoisyComponentFunctions) { + private static IDebugEntity maybeNull_asValue( + ValTracker valTracker, + String name, + Object obj, + boolean skipNoisyComponentFunctions, + boolean treatDiscoveredComponentsAsScopes + ) { DebugEntity val = new DebugEntity(); val.name = name; @@ -194,12 +211,20 @@ else if (obj instanceof lucee.runtime.type.QueryImpl) { else if (obj instanceof Map) { if (obj instanceof Component) { val.value = "cfc<" + ((Component)obj).getName() + ">"; + if (treatDiscoveredComponentsAsScopes) { + var v = new MarkerTrait.Scope((Component)obj); + ((ComponentScopeMarkerTraitShim)obj).__luceedebug__pinComponentScopeMarkerTrait(v); + val.variablesReference = valTracker.idempotentRegisterObject(v).id; + } + else { + val.variablesReference = valTracker.idempotentRegisterObject(obj).id; + } } else { int len = ((Map)obj).size(); val.value = "{} (" + len + " members)"; + val.variablesReference = valTracker.idempotentRegisterObject(obj).id; } - val.variablesReference = valTracker.idempotentRegisterObject(obj).id; } else { try { diff --git a/luceedebug/src/main/java/luceedebug/coreinject/ComponentScopeMarkerTraitShim.java b/luceedebug/src/main/java/luceedebug/coreinject/ComponentScopeMarkerTraitShim.java new file mode 100644 index 0000000..04328ff --- /dev/null +++ b/luceedebug/src/main/java/luceedebug/coreinject/ComponentScopeMarkerTraitShim.java @@ -0,0 +1,21 @@ +package luceedebug.coreinject; + +/** + * Intended to be an extension on lucee.runtime.ComponentImpl + * We need to disambiguate between a component meaning "container of a this/variables/static scope" and "literally a this scope" + * Because we want to show something like the following to the IDE: + * + * someObj : cfc + * - variables -> {...} + * - this -> {...} + * - static -> {...} + * + * But, `someObj` literally is `this` in the above; so we need to know when to show the nested scope listing, and when to show + * the contents of the `this` scope. + * + * There is only a setter because we just use the setter to pin the disambiguating-wrapper object onto something with a + * reasonable lifetime to prevent it from being GC'd. + */ +public interface ComponentScopeMarkerTraitShim { + void __luceedebug__pinComponentScopeMarkerTrait(Object obj); +} diff --git a/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java b/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java index 3783038..bb9a158 100644 --- a/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java +++ b/luceedebug/src/main/java/luceedebug/coreinject/DebugFrame.java @@ -17,6 +17,7 @@ import com.google.common.collect.MapMaker; import luceedebug.*; +import luceedebug.coreinject.CfEntityRef.MarkerTrait; public class DebugFrame implements IDebugFrame { static private AtomicLong nextId = new AtomicLong(0); @@ -27,7 +28,7 @@ public class DebugFrame implements IDebugFrame { */ static private boolean closureScopeGloballyDisabled = false; - private ValTracker valTracker; + public final ValTracker valTracker; final private FrameContext frameContext_; final private String sourceFilePath; @@ -57,6 +58,9 @@ public class DebugFrame implements IDebugFrame { // these should be made gc'able when this frame is collected // We might want to place these results somewhere that is kept alive for the whole request? private ArrayList refsToKeepAlive_ = new ArrayList<>(); + void pin(Object obj) { + refsToKeepAlive_.add(obj); + } // hold strong refs to scopes, because PageContext will swap them out as frames change (variables, local, this) // (application, server and etc. maybe could be held as globals) @@ -242,7 +246,9 @@ private static String tryGetFrameName(PageContext pageContext) { private void checkedPutScopeRef(String name, Map scope) { if (scope != null && !(scope instanceof LocalNotSupportedScope)) { - scopes_.put(name, new CfEntityRef(valTracker, scope)); + var v = new MarkerTrait.Scope(scope); + pin(v); + scopes_.put(name, new CfEntityRef(this, v)); } } @@ -315,7 +321,7 @@ public IDebugEntity[] getScopes() { } CfEntityRef trackEvalResult(Object obj) { - refsToKeepAlive_.add(obj); - return new CfEntityRef(valTracker, obj); + pin(obj); + return new CfEntityRef(this, obj); } } diff --git a/luceedebug/src/main/java/luceedebug/instrumenter/ComponentImpl.java b/luceedebug/src/main/java/luceedebug/instrumenter/ComponentImpl.java new file mode 100644 index 0000000..04bb2ef --- /dev/null +++ b/luceedebug/src/main/java/luceedebug/instrumenter/ComponentImpl.java @@ -0,0 +1,45 @@ +package luceedebug.instrumenter; + +import org.objectweb.asm.*; +import org.objectweb.asm.commons.GeneratorAdapter; + +public class ComponentImpl extends ClassVisitor { + public ComponentImpl(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/ComponentScopeMarkerTraitShim"; + + super.visit(version, access, name, signature, superName, augmentedInterfaces); + } + + @Override + public void visitEnd() { + final var fieldName = "__luceedebug__pinned_componentScopeMarkerTrait"; + visitField(org.objectweb.asm.Opcodes.ACC_PUBLIC, fieldName, "Ljava/lang/Object;", null, null); + + final var name = "__luceedebug__pinComponentScopeMarkerTrait"; + final var descriptor = "(Ljava/lang/Object;)V"; + 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.loadArg(0); + ga.putField(org.objectweb.asm.Type.getType("Llucee/runtime/ComponentImpl;"), fieldName, org.objectweb.asm.Type.getType("Ljava/lang/Object;")); + ga.visitInsn(org.objectweb.asm.Opcodes.RETURN); + ga.endMethod(); + } +}