diff --git a/.vscode/launch.json b/.vscode/launch.json index 494edb2..9c9622c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,5 +11,18 @@ "hostName": "localhost", "port": 9999 }, + { + "type": "cfml", + "name": "luceedebug", + "request": "attach", + "hostName": "localhost", + "port": 10000, + "pathTransforms": [ + { + "idePrefix": "c:\\Users\\anon\\dev\\luceedebug\\java-agent\\test\\docker\\", + "serverPrefix": "/var/www/" + } + ] + } ] -} \ No newline at end of file +} diff --git a/luceedebug/src/main/java/luceedebug/GlobalIDebugManagerHolder.java b/luceedebug/src/main/java/luceedebug/GlobalIDebugManagerHolder.java index 9f0853a..80cc495 100644 --- a/luceedebug/src/main/java/luceedebug/GlobalIDebugManagerHolder.java +++ b/luceedebug/src/main/java/luceedebug/GlobalIDebugManagerHolder.java @@ -11,4 +11,5 @@ public class GlobalIDebugManagerHolder { // would that happen alot in a dev environment where you want to hook up a debugger? public static ClassLoader luceeCoreLoader; public static IDebugManager debugManager; -} \ No newline at end of file + public static ILuceeVm luceeVm; +} diff --git a/luceedebug/src/main/java/luceedebug/IDebugManager.java b/luceedebug/src/main/java/luceedebug/IDebugManager.java index a961238..7fc031d 100644 --- a/luceedebug/src/main/java/luceedebug/IDebugManager.java +++ b/luceedebug/src/main/java/luceedebug/IDebugManager.java @@ -1,27 +1,29 @@ -package luceedebug; - -import java.util.ArrayList; - -/** - * We might be able to whittle this down to just {push,pop,step}, - * which is what instrumented pages need. The other methods are defined in package coreinject, - * and used only from package coreinject, so the definitely-inside-coreinject use site could - * probably cast this to the (single!) concrete implementation. - */ -public interface IDebugManager { - public interface CfStepCallback { - void call(Thread thread, int distanceToJvmFrame); - } - - void spawnWorker(Config config, String jdwpHost, int jdwpPort, String debugHost, int debugPort); - /** - * most common frame type - */ - public void pushCfFrame(lucee.runtime.PageContext pc, String filenameAbsPath, int distanceToFrame); - /** - * a "default value initialization frame" is the frame that does default function value init, - * like setting a,b,c in the following: - * `function foo(a=1,b=2,c=3) {}; foo(42);` <-- init frame will be stepped into twice, once for `b`, once for `c`; `a` is not default init'd +package luceedebug; + +import java.util.ArrayList; + +/** + * We might be able to whittle this down to just {push,pop,step}, + * which is what instrumented pages need. The other methods are defined in package coreinject, + * and used only from package coreinject, so the definitely-inside-coreinject use site could + * probably cast this to the (single!) concrete implementation. + */ +public interface IDebugManager { + public interface CfStepCallback { + void call(Thread thread, int distanceToJvmFrame); + } + + void spawnWorker(Config config, String jdwpHost, int jdwpPort, String debugHost, int debugPort); + void spawnWorkerInResponseToLuceeRestart(Config config); + + /** + * most common frame type + */ + public void pushCfFrame(lucee.runtime.PageContext pc, String filenameAbsPath, int distanceToFrame); + /** + * a "default value initialization frame" is the frame that does default function value init, + * like setting a,b,c in the following: + * `function foo(a=1,b=2,c=3) {}; foo(42);` <-- init frame will be stepped into twice, once for `b`, once for `c`; `a` is not default init'd */ public void pushCfFunctionDefaultValueInitializationFrame(lucee.runtime.PageContext pageContext, String sourceFilePath, int distanceToActualFrame); public void popCfFrame(); diff --git a/luceedebug/src/main/java/luceedebug/ILuceeVm.java b/luceedebug/src/main/java/luceedebug/ILuceeVm.java index 451d96d..e9c5a69 100644 --- a/luceedebug/src/main/java/luceedebug/ILuceeVm.java +++ b/luceedebug/src/main/java/luceedebug/ILuceeVm.java @@ -6,6 +6,8 @@ import com.sun.jdi.*; public interface ILuceeVm { + public void registerDebugManagerHooks(IDebugManager debugManager); + public void registerStepEventCallback(Consumer cb); public void registerBreakpointEventCallback(BiConsumer cb); diff --git a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java index 51ffc97..9ec0757 100644 --- a/luceedebug/src/main/java/luceedebug/LuceeTransformer.java +++ b/luceedebug/src/main/java/luceedebug/LuceeTransformer.java @@ -27,11 +27,16 @@ static public class ClassInjection { } /** - * if non-null, we are awaiting the initial class load of PageContextImpl - * When that happens, these classes will be injected into that class loader. - * Then, this should be set to null, since we don't need to hold onto them locally. + * Classes to add the lucee core class loader. */ - private ClassInjection[] pendingCoreLoaderClassInjections; + private ClassInjection[] classInjections; + + /** + * Track if we've initialized at least once. A "server restart" (as opposed to a JVM restart) + * means we get new lucee classloaders, but the jvm-wide jdwp related things remain valid, and do not need to + * be reinitialized. + */ + private boolean didInit = false; /** * this print stuff is debug related; @@ -65,7 +70,7 @@ public LuceeTransformer( int debugPort, Config config ) { - this.pendingCoreLoaderClassInjections = injections; + this.classInjections = injections; this.jdwpHost = jdwpHost; this.jdwpPort = jdwpPort; @@ -93,19 +98,30 @@ else if (className.equals("lucee/runtime/PageContextImpl")) { Method m = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); m.setAccessible(true); - for (var injection : pendingCoreLoaderClassInjections) { + for (var injection : classInjections) { // warn: reflection ... when does that become unsupported? m.invoke(GlobalIDebugManagerHolder.luceeCoreLoader, injection.name, injection.bytes, 0, injection.bytes.length); } - pendingCoreLoaderClassInjections = null; - try { final var klass = GlobalIDebugManagerHolder.luceeCoreLoader.loadClass("luceedebug.coreinject.DebugManager"); GlobalIDebugManagerHolder.debugManager = (IDebugManager)klass.getConstructor().newInstance(); System.out.println("[luceedebug] Loaded " + GlobalIDebugManagerHolder.debugManager + " with ClassLoader '" + GlobalIDebugManagerHolder.debugManager.getClass().getClassLoader() + "'"); - GlobalIDebugManagerHolder.debugManager.spawnWorker(config, jdwpHost, jdwpPort, debugHost, debugPort); + + if (didInit) { + // on a server restart (which is NOT a JVM restart), we need to reuse all the existing JDWP and DAP machinery that is already bound to particular ports. + // But, note that we did redo class injection and instrumentation, because the target classloader will have changed. + // TODO: this doesn't get flushed to output during a restart, like stdout is wired up incorrectly during this time? + System.out.println("[luceedebug] Lucee restart, reusing existing jdwp, dap server"); + + GlobalIDebugManagerHolder.debugManager.spawnWorkerInResponseToLuceeRestart(config); + } + else { + System.out.println("[luceedebug] Lucee startup, initializing jdwp, dap server"); + GlobalIDebugManagerHolder.debugManager.spawnWorker(config, jdwpHost, jdwpPort, debugHost, debugPort); + didInit = true; + } } catch (Throwable e) { e.printStackTrace(); diff --git a/luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java b/luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java index 3fb37cb..a31d2e5 100644 --- a/luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java +++ b/luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java @@ -65,23 +65,32 @@ else if (GlobalIDebugManagerHolder.luceeCoreLoader != this.getClass().getClassLo } // definitely non-null after spawnWorker + // can this be a constructor arg? private Config config_ = null; public void spawnWorker(Config config, String jdwpHost, int jdwpPort, String debugHost, int debugPort) { config_ = config; final String threadName = "luceedebug-worker"; - System.out.println("[luceedebug] attempting jdwp self connect to jdwp on " + jdwpHost + ":" + jdwpPort + "..."); + System.out.println("[luceedebug] jdwp self connect on " + jdwpHost + ":" + jdwpPort + "..."); VirtualMachine vm = jdwpSelfConnect(jdwpHost, jdwpPort); - LuceeVm luceeVm = new LuceeVm(config, vm); + GlobalIDebugManagerHolder.luceeVm = new LuceeVm(config, vm, this); new Thread(() -> { System.out.println("[luceedebug] jdwp self connect OK"); - DapServer.createForSocket(luceeVm, config, debugHost, debugPort); + DapServer.createForSocket(GlobalIDebugManagerHolder.luceeVm, config, debugHost, debugPort); }, threadName).start(); } + /** + * doesn't actually spawn anything, but the name is intended to be symmetric with the "other" spawnWorker + */ + public void spawnWorkerInResponseToLuceeRestart(Config config) { + config_ = config; + GlobalIDebugManagerHolder.luceeVm.registerDebugManagerHooks(this); + } + static private AttachingConnector getConnector() { VirtualMachineManager vmm; try { diff --git a/luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java b/luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java index 6c9d887..d6d17b8 100644 --- a/luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java +++ b/luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java @@ -325,7 +325,7 @@ private JdwpStaticCallable bootThreadWorker() { return new JdwpStaticCallable(((ClassType)refType.classObject().reflectedType()), jdwp_getThread); } - public LuceeVm(Config config, VirtualMachine vm) { + public LuceeVm(Config config, VirtualMachine vm, IDebugManager debugManager) { this.config_ = config; this.vm_ = vm; this.asyncWorker_.start(); @@ -337,8 +337,12 @@ public LuceeVm(Config config, VirtualMachine vm) { bootClassTracking(); bootThreadTracking(); + + registerDebugManagerHooks(debugManager); + } - GlobalIDebugManagerHolder.debugManager.registerCfStepHandler((thread, distanceToFrame) -> { + public void registerDebugManagerHooks(IDebugManager debugManager) { + debugManager.registerCfStepHandler((thread, distanceToFrame) -> { final var threadRef = threadMap_.getThreadRefByThreadOrFail(thread); final var done = new AtomicBoolean(false); diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index f5c87f5..03bc892 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -3,6 +3,9 @@ FROM lucee/lucee:5.3.10.120-tomcat9.0-jdk11-openjdk-2303 ENV LUCEEDEBUG_JAR /build/luceedebug.jar ENV SETENV_FILE /usr/local/tomcat/bin/setenv.sh +# configure admin password for lucee administrator UI @ http:///lucee/admin/server.cfm +RUN echo luceedebug > /opt/lucee/server/lucee-server/context/password.txt + #RUN apt-get update #RUN apt-get -y install vim diff --git a/test/docker/app1/a.cfm b/test/docker/app1/a.cfm index 783c303..606fc48 100644 --- a/test/docker/app1/a.cfm +++ b/test/docker/app1/a.cfm @@ -4,4 +4,4 @@ } writedump(foo(42)) - \ No newline at end of file + diff --git a/test/docker/build.bat b/test/docker/build.bat new file mode 100644 index 0000000..3d1c7d8 --- /dev/null +++ b/test/docker/build.bat @@ -0,0 +1,2 @@ +@echo off +docker build -t luceedebug . \ No newline at end of file