Skip to content

Commit 820d70f

Browse files
Added special lightweight pre-main class that skips installation on incompatible JVMs. (#8855)
1 parent 97258b1 commit 820d70f

File tree

6 files changed

+385
-162
lines changed

6 files changed

+385
-162
lines changed

dd-java-agent/build.gradle

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,27 @@ configurations {
1717
traceShadowInclude
1818
}
1919

20-
sourceCompatibility = JavaVersion.VERSION_1_7
21-
targetCompatibility = JavaVersion.VERSION_1_7
20+
// The special pre-check should be compiled with Java 6 to detect unsupported Java versions
21+
// and prevent issues for users that still using them.
22+
sourceSets {
23+
"main_java6" {
24+
java.srcDirs "${project.projectDir}/src/main/java6"
25+
}
26+
}
27+
28+
compileMain_java6Java.configure {
29+
setJavaVersion(it, 8)
30+
sourceCompatibility = JavaVersion.VERSION_1_6
31+
targetCompatibility = JavaVersion.VERSION_1_6
32+
destinationDirectory = layout.buildDirectory.dir("classes/java/main")
33+
}
34+
35+
tasks.compileJava.dependsOn compileMain_java6Java
36+
37+
dependencies {
38+
main_java6CompileOnly 'de.thetaphi:forbiddenapis:3.8'
39+
testImplementation sourceSets.main_java6.output
40+
}
2241

2342
/*
2443
* Several shadow jars are created
@@ -68,7 +87,7 @@ ext.generalShadowJarConfig = {
6887
if (!projectName.equals('instrumentation')) {
6988
relocate 'org.snakeyaml.engine', 'datadog.snakeyaml.engine'
7089
relocate 'okhttp3', 'datadog.okhttp3'
71-
relocate 'okio', 'datadog.okio'
90+
relocate 'okio', 'datadog.okio'
7291
}
7392

7493
if (!project.hasProperty("disableShadowRelocate") || !disableShadowRelocate) {
@@ -176,7 +195,7 @@ shadowJar generalShadowJarConfig >> {
176195
attributes(
177196
"Main-Class": "datadog.trace.bootstrap.AgentBootstrap",
178197
"Agent-Class": "datadog.trace.bootstrap.AgentBootstrap",
179-
"Premain-Class": "datadog.trace.bootstrap.AgentBootstrap",
198+
"Premain-Class": "datadog.trace.bootstrap.AgentPreCheck",
180199
"Can-Redefine-Classes": true,
181200
"Can-Retransform-Classes": true,
182201
)

dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.io.File;
1010
import java.io.IOException;
1111
import java.io.InputStreamReader;
12-
import java.io.PrintStream;
1312
import java.lang.instrument.Instrumentation;
1413
import java.lang.reflect.Method;
1514
import java.net.URI;
@@ -126,10 +125,6 @@ private static void agentmainImpl(
126125
// since tracer is presumably initialized elsewhere, still considering this complete
127126
return;
128127
}
129-
if (lessThanJava8()) {
130-
initTelemetry.onAbort("incompatible_runtime");
131-
return;
132-
}
133128
if (isJdkTool()) {
134129
initTelemetry.onAbort("jdk_tool");
135130
return;
@@ -170,7 +165,7 @@ static boolean getConfig(String configName) {
170165
}
171166

172167
static boolean exceptionCauseChainContains(Throwable ex, String exClassName) {
173-
Set<Throwable> stack = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
168+
Set<Throwable> stack = Collections.newSetFromMap(new IdentityHashMap<>());
174169
Throwable t = ex;
175170
while (t != null && stack.add(t) && stack.size() <= MAX_EXCEPTION_CHAIN_LENGTH) {
176171
// cannot do an instanceof check since most of the agent's code is loaded by an isolated CL
@@ -193,36 +188,6 @@ private static boolean alreadyInitialized() {
193188
return false;
194189
}
195190

196-
@SuppressForbidden
197-
private static boolean lessThanJava8() {
198-
try {
199-
return lessThanJava8(System.getProperty("java.version"), System.err);
200-
} catch (SecurityException e) {
201-
// Hypothetically, we could version sniff the supported version level
202-
// For now, just skip the check and let the JVM handle things instead
203-
return false;
204-
}
205-
}
206-
207-
// Reachable for testing
208-
static boolean lessThanJava8(String version, PrintStream output) {
209-
if (parseJavaMajorVersion(version) < 8) {
210-
String agentRawVersion = AgentJar.tryGetAgentVersion();
211-
String agentVersion = agentRawVersion == null ? "This version" : "Version " + agentRawVersion;
212-
213-
output.println(
214-
"Warning: "
215-
+ agentVersion
216-
+ " of dd-java-agent is not compatible with Java "
217-
+ version
218-
+ " and will not be installed.");
219-
output.println(
220-
"Please upgrade your Java version to 8+ or use the 0.x version of dd-java-agent in your build tool or download it from https://dtdg.co/java-tracer-v0");
221-
return true;
222-
}
223-
return false;
224-
}
225-
226191
private static boolean isJdkTool() {
227192
String moduleMain = SystemUtils.tryGetProperty("jdk.module.main");
228193
if (null != moduleMain && !moduleMain.isEmpty() && moduleMain.charAt(0) == 'j') {
@@ -263,32 +228,6 @@ private static boolean isJdkTool() {
263228
return false;
264229
}
265230

266-
// Reachable for testing
267-
static int parseJavaMajorVersion(String version) {
268-
int major = 0;
269-
if (null == version || version.isEmpty()) {
270-
return major;
271-
}
272-
int start = 0;
273-
if (version.charAt(0) == '1'
274-
&& version.length() >= 3
275-
&& version.charAt(1) == '.'
276-
&& Character.isDigit(version.charAt(2))) {
277-
start = 2;
278-
}
279-
// Parse the major digit and be a bit lenient, allowing digits followed by any non digit
280-
for (int i = start; i < version.length(); i++) {
281-
char c = version.charAt(i);
282-
if (Character.isDigit(c)) {
283-
major *= 10;
284-
major += Character.digit(c, 10);
285-
} else {
286-
break;
287-
}
288-
}
289-
return major;
290-
}
291-
292231
@SuppressForbidden
293232
static boolean shouldAbortDueToOtherJavaAgents() {
294233
// We don't abort if either
@@ -333,9 +272,6 @@ static boolean shouldAbortDueToOtherJavaAgents() {
333272
}
334273

335274
public static void main(final String[] args) {
336-
if (lessThanJava8()) {
337-
return;
338-
}
339275
AgentJar.main(args);
340276
}
341277

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package datadog.trace.bootstrap;
2+
3+
import de.thetaphi.forbiddenapis.SuppressForbidden;
4+
import java.io.BufferedReader;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.io.OutputStream;
8+
import java.io.PrintStream;
9+
import java.lang.instrument.Instrumentation;
10+
import java.lang.reflect.Method;
11+
12+
/** Special lightweight pre-main class that skips installation on incompatible JVMs. */
13+
public class AgentPreCheck {
14+
public static void premain(final String agentArgs, final Instrumentation inst) {
15+
agentmain(agentArgs, inst);
16+
}
17+
18+
@SuppressForbidden
19+
public static void agentmain(final String agentArgs, final Instrumentation inst) {
20+
try {
21+
if (compatible()) {
22+
continueBootstrap(agentArgs, inst);
23+
}
24+
} catch (Throwable e) {
25+
// If agent failed we should not fail the application.
26+
// We don't have a log manager here, so just print.
27+
System.err.println("ERROR: " + e.getMessage());
28+
}
29+
}
30+
31+
private static void reportIncompatibleJava(
32+
String javaVersion, String javaHome, String agentVersion, PrintStream output) {
33+
output.println(
34+
"Warning: "
35+
+ (agentVersion == null ? "This version" : "Version " + agentVersion)
36+
+ " of dd-java-agent is not compatible with Java "
37+
+ javaVersion
38+
+ " found at '"
39+
+ javaHome
40+
+ "' and is effectively disabled.");
41+
output.println("Please upgrade your Java version to 8+");
42+
}
43+
44+
static void sendTelemetry(String forwarderPath, String javaVersion, String agentVersion) {
45+
// Hardcoded payload for unsupported Java version.
46+
String payload =
47+
"{\"metadata\":{"
48+
+ "\"runtime_name\":\"jvm\","
49+
+ "\"language_name\":\"jvm\","
50+
+ "\"runtime_version\":\""
51+
+ javaVersion
52+
+ "\","
53+
+ "\"language_version\":\""
54+
+ javaVersion
55+
+ "\","
56+
+ "\"tracer_version\":\""
57+
+ agentVersion
58+
+ "\"},"
59+
+ "\"points\":[{"
60+
+ "\"name\":\"library_entrypoint.abort\","
61+
+ "\"tags\":[\"reason:incompatible_runtime\"]"
62+
+ "}]"
63+
+ "}";
64+
65+
ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, payload);
66+
t.setDaemon(true);
67+
t.start();
68+
}
69+
70+
private static String tryGetProperty(String property) {
71+
try {
72+
return System.getProperty(property);
73+
} catch (SecurityException e) {
74+
return null;
75+
}
76+
}
77+
78+
@SuppressForbidden
79+
private static boolean compatible() {
80+
String javaVersion = tryGetProperty("java.version");
81+
String javaHome = tryGetProperty("java.home");
82+
83+
return compatible(javaVersion, javaHome, System.err);
84+
}
85+
86+
// Reachable for testing
87+
static boolean compatible(String javaVersion, String javaHome, PrintStream output) {
88+
int majorJavaVersion = parseJavaMajorVersion(javaVersion);
89+
90+
if (majorJavaVersion >= 8) {
91+
return true;
92+
}
93+
94+
String agentVersion = getAgentVersion();
95+
96+
reportIncompatibleJava(javaVersion, javaHome, agentVersion, output);
97+
98+
String forwarderPath = System.getenv("DD_TELEMETRY_FORWARDER_PATH");
99+
if (forwarderPath != null) {
100+
sendTelemetry(forwarderPath, javaVersion, agentVersion);
101+
}
102+
103+
return false;
104+
}
105+
106+
// Reachable for testing
107+
static int parseJavaMajorVersion(String javaVersion) {
108+
int major = 0;
109+
if (javaVersion == null || javaVersion.isEmpty()) {
110+
return major;
111+
}
112+
113+
int start = 0;
114+
if (javaVersion.charAt(0) == '1'
115+
&& javaVersion.length() >= 3
116+
&& javaVersion.charAt(1) == '.'
117+
&& Character.isDigit(javaVersion.charAt(2))) {
118+
start = 2;
119+
}
120+
121+
// Parse the major digit and be a bit lenient, allowing digits followed by any non digit
122+
for (int i = start; i < javaVersion.length(); i++) {
123+
char c = javaVersion.charAt(i);
124+
if (Character.isDigit(c)) {
125+
major *= 10;
126+
major += Character.digit(c, 10);
127+
} else {
128+
break;
129+
}
130+
}
131+
return major;
132+
}
133+
134+
private static String getAgentVersion() {
135+
try {
136+
// Get the resource as an InputStream
137+
InputStream is = AgentPreCheck.class.getResourceAsStream("/dd-java-agent.version");
138+
if (is == null) {
139+
return null;
140+
}
141+
142+
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
143+
final StringBuilder sb = new StringBuilder();
144+
for (int c = reader.read(); c != -1; c = reader.read()) {
145+
sb.append((char) c);
146+
}
147+
reader.close();
148+
149+
return sb.toString().trim();
150+
} catch (Throwable e) {
151+
return null;
152+
}
153+
}
154+
155+
@SuppressForbidden
156+
private static void continueBootstrap(final String agentArgs, final Instrumentation inst)
157+
throws Exception {
158+
Class<?> clazz = Class.forName("datadog.trace.bootstrap.AgentBootstrap");
159+
Method agentMain = clazz.getMethod("agentmain", String.class, Instrumentation.class);
160+
agentMain.invoke(null, agentArgs, inst);
161+
}
162+
163+
public static final class ForwarderJsonSenderThread extends Thread {
164+
private final String forwarderPath;
165+
private final String payload;
166+
167+
public ForwarderJsonSenderThread(String forwarderPath, String payload) {
168+
super("dd-forwarder-json-sender");
169+
setDaemon(true);
170+
this.forwarderPath = forwarderPath;
171+
this.payload = payload;
172+
}
173+
174+
@SuppressForbidden
175+
@Override
176+
public void run() {
177+
ProcessBuilder builder = new ProcessBuilder(forwarderPath, "library_entrypoint");
178+
try {
179+
Process process = builder.start();
180+
OutputStream out = null;
181+
try {
182+
out = process.getOutputStream();
183+
out.write(payload.getBytes());
184+
} finally {
185+
if (out != null) {
186+
out.close();
187+
}
188+
}
189+
} catch (Throwable e) {
190+
System.err.println("Failed to send telemetry: " + e.getMessage());
191+
}
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)