Fix thread safety, error handling, and code clarity in agent bootstrap #807
Fix thread safety, error handling, and code clarity in agent bootstrap #807
Conversation
8cbd216 to
f17b7c3
Compare
There was a problem hiding this comment.
Pull request overview
Refactors the standalone btrace-agent bootstrap/packaging so the agent entrypoint is a small unmasked Agent class that loads the real Main from .classdata, alongside Gradle changes to build a masked agent JAR.
Changes:
- Replaces
Main’spremain/agentmainentrypoints with a newAgententrypoint andAgentClassLoaderfor loading masked.classdata. - Simplifies/adjusts bootstrap/system classpath processing in
Main.processClasspaths. - Adds Shadow-based packaging and post-processing tasks to produce an
agentJarwith mostorg.openjdk.btrace.agentclasses renamed to.classdata.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| btrace-agent/src/main/resources/META-INF/MANIFEST.MF | Removes the static manifest resource (manifest now configured via Gradle task). |
| btrace-agent/src/main/java/org/openjdk/btrace/agent/Main.java | Drops premain/agentmain, exposes main(args, inst) and adjusts bootstrap classpath handling. |
| btrace-agent/src/main/java/org/openjdk/btrace/agent/AgentClassLoader.java | New loader that reads .classdata resources and defines classes. |
| btrace-agent/src/main/java/org/openjdk/btrace/agent/Agent.java | New agent entrypoint that appends to bootstrap search path and reflectively invokes Main.main. |
| btrace-agent/build.gradle | Adds Shadow config and tasks to build a masked agentJar and rename .class → .classdata under the agent package. |
| private static void processClasspaths(String libs) { | ||
| // Try to find JAR via Loader.class (unmasked bootstrap class) | ||
| // Main.class won't work because it's loaded from .classdata | ||
| String bootPath = null; | ||
| try { | ||
| Class<?> loaderClass = Class.forName("org.openjdk.btrace.boot.Loader"); | ||
| URL loaderResource = loaderClass.getResource("Loader.class"); | ||
| if (loaderResource != null) { | ||
| bootPath = loaderResource.toString(); | ||
| if (bootPath.startsWith("jar:file:")) { | ||
| // Extract JAR path from jar:file:/path/to/btrace.jar!/org/openjdk/btrace/boot/Loader.class | ||
| bootPath = bootPath.substring("jar:file:".length()); | ||
| int idx = bootPath.indexOf("!"); | ||
| if (idx > -1) { | ||
| bootPath = bootPath.substring(0, idx); | ||
| } | ||
| } | ||
| } | ||
| } catch (ClassNotFoundException e) { | ||
| // Fall back to Main.class if Loader not found (shouldn't happen) | ||
| URL agentJar = Main.class.getResource("Main.class"); | ||
| if (agentJar != null) { | ||
| bootPath = agentJar.toString().replace("jar:file:", ""); | ||
| int idx = bootPath.indexOf("btrace-agent.jar"); | ||
| if (idx > -1) { | ||
| bootPath = bootPath.substring(0, idx) + "btrace-boot.jar"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| String bootClassPath = argMap.get(BOOT_CLASS_PATH); | ||
| if (bootClassPath == null && bootPath != null) { | ||
| bootClassPath = bootPath; | ||
| } else if (bootClassPath != null && bootPath != null) { | ||
| if (".".equals(bootClassPath)) { | ||
| bootClassPath = bootPath; | ||
| } else { | ||
| bootClassPath = bootPath + File.pathSeparator + bootClassPath; | ||
| } | ||
| } | ||
| log.debug("Bootstrap ClassPath: {}", bootClassPath); | ||
|
|
||
| StringTokenizer tokenizer = new StringTokenizer(bootClassPath, File.pathSeparator); | ||
| try { | ||
| while (tokenizer.hasMoreTokens()) { | ||
| String path = tokenizer.nextToken(); | ||
| File f = new File(path); | ||
| if (!f.exists()) { | ||
| log.debug("BTrace bootstrap classpath resource [{}] does not exist", path); | ||
| } else { | ||
| if (f.isFile() && f.getName().toLowerCase().endsWith(".jar")) { | ||
| JarFile jf = asJarFile(f); | ||
| log.debug("Adding jar: {}", jf); | ||
| inst.appendToBootstrapClassLoaderSearch(jf); | ||
| if (bootClassPath != null) { | ||
| log.debug("Bootstrap ClassPath: {}", bootClassPath); |
There was a problem hiding this comment.
The agent-class masking/AgentClassLoader approach means Main is loaded from .classdata, but processClasspaths() still relies on addPreconfLibs(libs) which locates the agent JAR via Main.class resource. In the produced agentJar, Main.class won't exist (it becomes Main.classdata), so the preconfigured btrace-libs enhancement will silently stop working. Consider locating the jar via Agent.class/ProtectionDomain (or another unmasked .class) instead of Main.class resources.
| try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".classdata")) { | ||
| if (is != null) { | ||
| byte[] data = is.readAllBytes(); | ||
| c = defineClass(name, data, 0, data.length); | ||
| if (resolve) { |
There was a problem hiding this comment.
InputStream.readAllBytes() is a Java 9+ API. Since this project targets/runs on Java 8+ (and compiles for 8), using it here will cause a NoSuchMethodError when the agent runs on Java 8. Replace with a Java-8-compatible read loop (e.g., ByteArrayOutputStream + buffer) or Files.readAllBytes(Path) when you have a Path.
btrace-agent/build.gradle
Outdated
| outputs.dir unpackedJarDir | ||
| doLast { | ||
| fileTree(unpackedJarDir.get().asFile.path + "/org/openjdk/btrace/agent").include('**/*.class').each { File file -> | ||
| if (file.name != 'Agent.class' && !file.name.startsWith('AgentClassLoader')) { |
There was a problem hiding this comment.
renameClassFiles renames almost all org/openjdk/btrace/agent/**/*.class to .classdata. That makes classes like org.openjdk.btrace.agent.Main and PerfReaderImpl no longer loadable via Class.forName(...) from other packages/classloaders, which currently happens (e.g., btrace-instr Preprocessor and btrace-runtime PerfReader loading). Either keep a minimal set of agent classes as real .class (exclude them from renaming) or update those reflective call sites to use the masking-aware loader/entrypoint.
| if (file.name != 'Agent.class' && !file.name.startsWith('AgentClassLoader')) { | |
| if (!(file.name == 'Agent.class' | |
| || file.name.startsWith('AgentClassLoader') | |
| || file.name.startsWith('Main') | |
| || file.name.startsWith('PerfReaderImpl'))) { |
btrace-agent/build.gradle
Outdated
| task renameClassFiles(type: DefaultTask) { | ||
| dependsOn unpackJar | ||
| inputs.dir unpackedJarDir | ||
| outputs.dir unpackedJarDir | ||
| doLast { | ||
| fileTree(unpackedJarDir.get().asFile.path + "/org/openjdk/btrace/agent").include('**/*.class').each { File file -> | ||
| if (file.name != 'Agent.class' && !file.name.startsWith('AgentClassLoader')) { | ||
| file.renameTo(new File(file.parent, file.name.replace('.class', '.classdata'))) | ||
| } |
There was a problem hiding this comment.
renameClassFiles declares the same directory as both input and output and then mutates it in place. This is generally not cacheable and can lead to incorrect up-to-date/build-cache behavior. Prefer writing renamed files into a separate output directory (and have agentJar consume that), or mark the task uncacheable/avoid declaring an output if it must mutate inputs.
| } catch (Exception e) { | ||
| String msg = "BTrace agent failed to initialize: " + e.getMessage(); | ||
| System.err.println(msg); | ||
| e.printStackTrace(); | ||
| if (isPremain) { | ||
| throw new RuntimeException(msg, e); | ||
| } |
There was a problem hiding this comment.
The error message here is built from e.getMessage(), which is often null/low-signal for reflective failures (notably InvocationTargetException). This can produce unhelpful output like "...: null" even though the cause is meaningful. Consider unwrapping InvocationTargetException (or using e.toString() / root cause class + message) when building msg.
| private static final class Singleton { | ||
| private static final AgentClassLoader INSTANCE = new AgentClassLoader(); | ||
| } | ||
|
|
||
| private AgentClassLoader() { | ||
| super(Agent.class.getClassLoader() == null | ||
| ? ClassLoader.getSystemClassLoader() | ||
| : Agent.class.getClassLoader()); | ||
| } | ||
|
|
||
| public static AgentClassLoader getInstance() { | ||
| return Singleton.INSTANCE; | ||
| } | ||
|
|
||
| @Override | ||
| public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | ||
| synchronized (getClassLoadingLock(name)) { | ||
| Class<?> c = findLoadedClass(name); | ||
| if (c != null) { | ||
| return c; | ||
| } | ||
| // Try loading from .classdata resources first, then delegate to parent | ||
| try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".classdata")) { | ||
| if (is != null) { | ||
| byte[] data = is.readAllBytes(); | ||
| c = defineClass(name, data, 0, data.length); | ||
| if (resolve) { | ||
| resolveClass(c); | ||
| } | ||
| return c; | ||
| } | ||
| } catch (IOException e) { | ||
| throw new ClassNotFoundException("Failed to read classdata for " + name, e); | ||
| } | ||
| return super.loadClass(name, resolve); | ||
| } | ||
| } |
There was a problem hiding this comment.
New Java files here don't follow the repo's enforced Google Java Format (e.g., 4-space indentation). This will likely fail spotlessCheck in CI. Please run the formatter (./gradlew spotlessApply) or reformat to GJF style before merging.
| private static final class Singleton { | |
| private static final AgentClassLoader INSTANCE = new AgentClassLoader(); | |
| } | |
| private AgentClassLoader() { | |
| super(Agent.class.getClassLoader() == null | |
| ? ClassLoader.getSystemClassLoader() | |
| : Agent.class.getClassLoader()); | |
| } | |
| public static AgentClassLoader getInstance() { | |
| return Singleton.INSTANCE; | |
| } | |
| @Override | |
| public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | |
| synchronized (getClassLoadingLock(name)) { | |
| Class<?> c = findLoadedClass(name); | |
| if (c != null) { | |
| return c; | |
| } | |
| // Try loading from .classdata resources first, then delegate to parent | |
| try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".classdata")) { | |
| if (is != null) { | |
| byte[] data = is.readAllBytes(); | |
| c = defineClass(name, data, 0, data.length); | |
| if (resolve) { | |
| resolveClass(c); | |
| } | |
| return c; | |
| } | |
| } catch (IOException e) { | |
| throw new ClassNotFoundException("Failed to read classdata for " + name, e); | |
| } | |
| return super.loadClass(name, resolve); | |
| } | |
| } | |
| private static final class Singleton { | |
| private static final AgentClassLoader INSTANCE = new AgentClassLoader(); | |
| } | |
| private AgentClassLoader() { | |
| super( | |
| Agent.class.getClassLoader() == null | |
| ? ClassLoader.getSystemClassLoader() | |
| : Agent.class.getClassLoader()); | |
| } | |
| public static AgentClassLoader getInstance() { | |
| return Singleton.INSTANCE; | |
| } | |
| @Override | |
| public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | |
| synchronized (getClassLoadingLock(name)) { | |
| Class<?> c = findLoadedClass(name); | |
| if (c != null) { | |
| return c; | |
| } | |
| // Try loading from .classdata resources first, then delegate to parent | |
| try (InputStream is = | |
| getParent().getResourceAsStream(name.replace('.', '/') + ".classdata")) { | |
| if (is != null) { | |
| byte[] data = is.readAllBytes(); | |
| c = defineClass(name, data, 0, data.length); | |
| if (resolve) { | |
| resolveClass(c); | |
| } | |
| return c; | |
| } | |
| } catch (IOException e) { | |
| throw new ClassNotFoundException("Failed to read classdata for " + name, e); | |
| } | |
| return super.loadClass(name, resolve); | |
| } | |
| } |
| URL pd = Agent.class.getProtectionDomain().getCodeSource().getLocation(); | ||
| if (pd.toString().endsWith(".jar")) { | ||
| inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(pd.toURI()))); | ||
| } |
There was a problem hiding this comment.
appendToBootstrapClassLoaderSearch(new JarFile(...)) opens a JarFile that is never closed. Elsewhere in the codebase (e.g., extension loader) JarFile is closed after appending to avoid leaking file descriptors. Use try-with-resources around the JarFile, and also handle getProtectionDomain().getCodeSource() potentially being null (to avoid NPEs in non-standard classloading environments).
| public static void premain(String args, Instrumentation inst) { | ||
| main(args, inst, true); | ||
| } | ||
|
|
||
| public static void agentmain(String args, Instrumentation inst) { | ||
| main(args, inst, false); | ||
| } | ||
|
|
||
| private static void main(String args, Instrumentation inst, boolean isPremain) { | ||
| try { | ||
| // Append to bootstrap classpath before loading Main to ensure classes | ||
| // required by Main's static initializers are available | ||
| URL pd = Agent.class.getProtectionDomain().getCodeSource().getLocation(); | ||
| if (pd.toString().endsWith(".jar")) { | ||
| inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(pd.toURI()))); | ||
| } | ||
| Class<?> mainClass = AgentClassLoader.getInstance().loadClass("org.openjdk.btrace.agent.Main"); | ||
| Method mainMethod = mainClass.getMethod("main", String.class, Instrumentation.class); | ||
| mainMethod.invoke(null, args, inst); | ||
| } catch (Exception e) { | ||
| String msg = "BTrace agent failed to initialize: " + e.getMessage(); | ||
| System.err.println(msg); | ||
| e.printStackTrace(); | ||
| if (isPremain) { | ||
| throw new RuntimeException(msg, e); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
New Java file formatting here appears inconsistent with Google Java Format (Spotless-enforced), similar to AgentClassLoader.java. Please reformat (e.g., ./gradlew spotlessApply) to avoid CI failures.
| public static void premain(String args, Instrumentation inst) { | |
| main(args, inst, true); | |
| } | |
| public static void agentmain(String args, Instrumentation inst) { | |
| main(args, inst, false); | |
| } | |
| private static void main(String args, Instrumentation inst, boolean isPremain) { | |
| try { | |
| // Append to bootstrap classpath before loading Main to ensure classes | |
| // required by Main's static initializers are available | |
| URL pd = Agent.class.getProtectionDomain().getCodeSource().getLocation(); | |
| if (pd.toString().endsWith(".jar")) { | |
| inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(pd.toURI()))); | |
| } | |
| Class<?> mainClass = AgentClassLoader.getInstance().loadClass("org.openjdk.btrace.agent.Main"); | |
| Method mainMethod = mainClass.getMethod("main", String.class, Instrumentation.class); | |
| mainMethod.invoke(null, args, inst); | |
| } catch (Exception e) { | |
| String msg = "BTrace agent failed to initialize: " + e.getMessage(); | |
| System.err.println(msg); | |
| e.printStackTrace(); | |
| if (isPremain) { | |
| throw new RuntimeException(msg, e); | |
| } | |
| } | |
| } | |
| public static void premain(String args, Instrumentation inst) { | |
| main(args, inst, true); | |
| } | |
| public static void agentmain(String args, Instrumentation inst) { | |
| main(args, inst, false); | |
| } | |
| private static void main(String args, Instrumentation inst, boolean isPremain) { | |
| try { | |
| // Append to bootstrap classpath before loading Main to ensure classes | |
| // required by Main's static initializers are available | |
| URL pd = Agent.class.getProtectionDomain().getCodeSource().getLocation(); | |
| if (pd.toString().endsWith(".jar")) { | |
| inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(pd.toURI()))); | |
| } | |
| Class<?> mainClass = | |
| AgentClassLoader.getInstance().loadClass("org.openjdk.btrace.agent.Main"); | |
| Method mainMethod = | |
| mainClass.getMethod("main", String.class, Instrumentation.class); | |
| mainMethod.invoke(null, args, inst); | |
| } catch (Exception e) { | |
| String msg = "BTrace agent failed to initialize: " + e.getMessage(); | |
| System.err.println(msg); | |
| e.printStackTrace(); | |
| if (isPremain) { | |
| throw new RuntimeException(msg, e); | |
| } | |
| } | |
| } |
1b807ee to
499e25f
Compare
- Remove redundant Loader.class-based boot path auto-detection from processClasspaths() — Loader.startAgent() already appends btrace.jar to the bootstrap classpath before invoking Main - Only process BOOT_CLASS_PATH arg when explicitly set; move debug log inside null-check to avoid logging "null" - Simplify MaskedClassLoader.readEntry() to use readAllBytes() instead of manual ByteArrayOutputStream buffer management https://claude.ai/code/session_01WRpQefqudtbee9uatYakjY
499e25f to
1e0974b
Compare
Summary
Addresses code review findings in the masked agent JAR bootstrap code.
Main, soMain's static initializers can find bootstrap classesRuntimeExceptioninpremainso the JVM aborts instead of silently continuing without BTracesuper()instead of keeping a separateparentfield, eliminating confusing dual delegation pathsAgentClassLoader: OverrideloadClass()instead offindClass()for explicit delegation model; usereadAllBytes()instead of manual 4KB+65KB buffer managementDefaultTaskfor rename task, addinputs/outputsfor Gradle cacheability, replace deprecatedbuildDirwithlayout.buildDirectoryorg/openjdk/btrace/services/reference withorg/openjdk/btrace/extension/processClasspaths: Bootstrap path is now handled byAgent; remove redundantLoader.classdetection logic; move debug log inside null-checkTest plan
./gradlew :btrace-agent:agentJarAgent.classandAgentClassLoader*.classremain as.classunderorg/openjdk/btrace/agent/./gradlew testThis change is