Skip to content

Commit 7800223

Browse files
committed
fix(agent): Support running agent on bootstrap classpath
- Update `Agent.java` to use `Agent.class.getResource()` instead of `Agent.class.getClassLoader().getResource()` when locating the agent JAR. This prevents a `NullPointerException` when the agent is loaded by the bootstrap class loader (where `getClassLoader()` returns null). - Modify `Properties.java` to automatically default `appmap.debug.disableGit` to `true` if the agent is running on the bootstrap classpath. This avoids crashes in JGit initialization, which relies on `ResourceBundle` loading that is problematic in the bootstrap context. - Add a warning log in `Agent.premain` when running on the bootstrap classpath, advising that this configuration is for troubleshooting only.
1 parent 1558b7d commit 7800223

File tree

5 files changed

+95
-10
lines changed

5 files changed

+95
-10
lines changed

agent/src/main/java/com/appland/appmap/Agent.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public static void premain(String agentArgs, Instrumentation inst) {
7474
logger.info("config: {}", AppMapConfig.get());
7575
logger.debug("System properties: {}", System.getProperties());
7676

77+
if (Agent.class.getClassLoader() == null) {
78+
logger.warn("AppMap agent is running on the bootstrap classpath. This is not a recommended configuration and should only be used for troubleshooting. Git integration will be disabled.");
79+
}
80+
7781
addAgentJars(agentArgs, inst);
7882

7983

@@ -162,12 +166,18 @@ private static void addAgentJars(String agentArgs, Instrumentation inst) {
162166
Path agentJarPath = null;
163167
try {
164168
Class<Agent> agentClass = Agent.class;
165-
URL resourceURL = agentClass.getClassLoader()
166-
.getResource(agentClass.getName().replace('.', '/') + ".class");
169+
// When the agent is loaded by the bootstrap class loader (e.g., via -Xbootclasspath/a:),
170+
// agentClass.getClassLoader() returns null, leading to a NullPointerException. To handle
171+
// this, we use Class.getResource() which correctly resolves resources even when the
172+
// class is loaded by the bootstrap class loader. The leading '/' in the resource name
173+
// is crucial for absolute path resolution when using Class.getResource().
174+
URL resourceURL = agentClass.getResource("/" + agentClass.getName().replace('.', '/') + ".class");
175+
167176
// During testing of the agent itself, classes get loaded from a directory, and will have the
168177
// protocol "file". The rest of the time (i.e. when it's actually deployed), they'll always
169-
// come from a jar file.
170-
if (resourceURL.getProtocol().equals("jar")) {
178+
// come from a jar file. We must also check that resourceURL is not null before using it,
179+
// as getResource() can return null if the resource is not found.
180+
if (resourceURL != null && resourceURL.getProtocol().equals("jar")) {
171181
String resourcePath = resourceURL.getPath();
172182
URL jarURL = new URL(resourcePath.substring(0, resourcePath.indexOf('!')));
173183
logger.debug("jarURL: {}", jarURL);

agent/src/main/java/com/appland/appmap/config/Properties.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ public class Properties {
2121
public static final String DebugClassPrefix = resolveProperty("appmap.debug.classPrefix", (String) null);
2222
public static final Boolean SaveInstrumented =
2323
resolveProperty("appmap.debug.saveInstrumented", false);
24-
public static final Boolean DisableGit = resolveProperty("appmap.debug.disableGit", false);
24+
public static final Boolean DisableGit =
25+
// Git integration (JGit) uses resource bundles, which are not reliably available
26+
// when the agent is loaded by the bootstrap class loader (i.e., when
27+
// getClassLoader() returns null). In such cases, automatically disable Git
28+
// to prevent NullPointerExceptions during initialization.
29+
resolveProperty("appmap.debug.disableGit", Properties.class.getClassLoader() == null);
2530

2631
public static final Boolean RecordingAuto = resolveProperty("appmap.recording.auto", false);
2732
public static final String RecordingName = resolveProperty("appmap.recording.name", (String) null);

agent/test/classloading/app/build.gradle

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ application {
4747
// Define the main class for the application.
4848
mainClass = 'com.appland.appmap.test.fixture.Runner'
4949
def libJar = tasks.getByPath(':lib:jar').outputs.files.singleFile
50+
51+
// Allow testing with bootstrap classpath instead of javaagent
52+
def useBootstrapClasspath = findProperty("useBootstrapClasspath") == "true"
53+
5054
applicationDefaultJvmArgs += [
51-
"-javaagent:${appmapJar}",
52-
/*"-Dsun.misc.URLClassPath.debug",*/
53-
"-DtestJars=${configurations.servlet.asPath};${libJar}",
54-
"-Djava.util.logging.config.file=${System.env.JUL_CONFIG}"
55+
"-javaagent:${appmapJar}",
56+
/*"-Dsun.misc.URLClassPath.debug",*/
57+
"-DtestJars=${configurations.servlet.asPath};${libJar}",
58+
"-Djava.util.logging.config.file=${System.env.JUL_CONFIG}"
5559
]
60+
if (useBootstrapClasspath) {
61+
applicationDefaultJvmArgs += [
62+
"-Xbootclasspath/a:${appmapJar}",
63+
]
64+
}
5665
}
5766

5867
tasks.named('test') {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.appland.appmap.test.fixture;
2+
3+
import com.appland.appmap.Agent;
4+
import com.appland.appmap.config.Properties;
5+
6+
/**
7+
* Test that verifies the agent can run when loaded on the bootstrap classpath.
8+
* This is a regression test for the fix that prevents NullPointerException when
9+
* Agent.class.getClassLoader() returns null.
10+
*/
11+
public class TestBootstrapClasspath implements TestClass {
12+
13+
@Override
14+
public int beforeTest() throws Exception {
15+
return 0;
16+
}
17+
18+
@Override
19+
public int runTest() throws Exception {
20+
// Verify that the agent is actually running on the bootstrap classpath
21+
ClassLoader agentClassLoader = Agent.class.getClassLoader();
22+
if (agentClassLoader != null) {
23+
System.err.println("ERROR: Agent is not running on bootstrap classpath");
24+
System.err.println("Agent class loader: " + agentClassLoader);
25+
return 1;
26+
}
27+
28+
// Verify that Properties class is also on bootstrap classpath
29+
ClassLoader propertiesClassLoader = Properties.class.getClassLoader();
30+
if (propertiesClassLoader != null) {
31+
System.err.println("ERROR: Properties is not running on bootstrap classpath");
32+
System.err.println("Properties class loader: " + propertiesClassLoader);
33+
return 1;
34+
}
35+
36+
// Verify that Git integration is automatically disabled when on bootstrap classpath
37+
if (!Properties.DisableGit) {
38+
System.err.println("ERROR: Git integration should be automatically disabled on bootstrap classpath");
39+
return 1;
40+
}
41+
42+
System.out.println("SUCCESS: Agent running on bootstrap classpath with Git disabled");
43+
return 0;
44+
}
45+
}

agent/test/classloading/classloading.bats

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
#!/usr/bin/env bats
2+
13
load '../helper'
24

35
setup_file() {
46
export AGENT_JAR="$(find_agent_jar)"
57

6-
cd test/classloading
8+
cd "$BATS_TEST_DIRNAME"
79
_configure_logging
810
}
911

@@ -24,3 +26,17 @@ setup_file() {
2426
assert_json_eq ".events[0].method_id" "getGreeting"
2527
assert_json_eq ".events[0].path" "lib/src/main/java/com/appland/appmap/test/fixture/helloworld/HelloWorld.java"
2628
}
29+
30+
@test "Bootstrap Classpath" {
31+
# Regression test for fix that allows running agent on bootstrap classpath.
32+
# This verifies that:
33+
# 1. Agent.class.getResource() works when getClassLoader() returns null
34+
# 2. Git integration is automatically disabled on bootstrap classpath
35+
# 3. No NullPointerException occurs during agent initialization
36+
run \
37+
gradlew ${BATS_VERSION+-q} -PappmapJar="$AGENT_JAR" -PuseBootstrapClasspath=true run --args "TestBootstrapClasspath"
38+
assert_success
39+
40+
# Verify the test confirmed running on bootstrap classpath with Git disabled
41+
assert_output --partial "SUCCESS: Agent running on bootstrap classpath with Git disabled"
42+
}

0 commit comments

Comments
 (0)