Skip to content

Commit

Permalink
handle lucee restart
Browse files Browse the repository at this point in the history
- reuse existing jdwp/luceeVM/dap
- create a fresh debug manager and instrument fresh lucee classloader
- re-register hooks in luceeVM against fresh debug manager
  • Loading branch information
softwareCobbler committed Nov 23, 2023
1 parent 6fd3c81 commit 29c439f
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 41 deletions.
15 changes: 14 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
public static ILuceeVm luceeVm;
}
50 changes: 26 additions & 24 deletions luceedebug/src/main/java/luceedebug/IDebugManager.java
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
2 changes: 2 additions & 0 deletions luceedebug/src/main/java/luceedebug/ILuceeVm.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.sun.jdi.*;

public interface ILuceeVm {
public void registerDebugManagerHooks(IDebugManager debugManager);

public void registerStepEventCallback(Consumer</*threadID*/Long> cb);
public void registerBreakpointEventCallback(BiConsumer</*threadID*/Long, /*bpID*/Integer> cb);

Expand Down
34 changes: 25 additions & 9 deletions luceedebug/src/main/java/luceedebug/LuceeTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,7 +70,7 @@ public LuceeTransformer(
int debugPort,
Config config
) {
this.pendingCoreLoaderClassInjections = injections;
this.classInjections = injections;

this.jdwpHost = jdwpHost;
this.jdwpPort = jdwpPort;
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 12 additions & 3 deletions luceedebug/src/main/java/luceedebug/coreinject/DebugManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions luceedebug/src/main/java/luceedebug/coreinject/LuceeVm.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions test/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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://<domain>/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

Expand Down
2 changes: 1 addition & 1 deletion test/docker/app1/a.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
}
writedump(foo(42))
</cfscript>
</cfscript>
2 changes: 2 additions & 0 deletions test/docker/build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
docker build -t luceedebug .

0 comments on commit 29c439f

Please sign in to comment.