diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 0278a24cd9f..84a0ec81bd6 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -17,8 +17,27 @@ configurations { traceShadowInclude } -sourceCompatibility = JavaVersion.VERSION_1_7 -targetCompatibility = JavaVersion.VERSION_1_7 +// The special pre-check should be compiled with Java 6 to detect unsupported Java versions +// and prevent issues for users that still using them. +sourceSets { + "main_java6" { + java.srcDirs "${project.projectDir}/src/main/java6" + } +} + +compileMain_java6Java.configure { + setJavaVersion(it, 8) + sourceCompatibility = JavaVersion.VERSION_1_6 + targetCompatibility = JavaVersion.VERSION_1_6 + destinationDirectory = layout.buildDirectory.dir("classes/java/main") +} + +tasks.compileJava.dependsOn compileMain_java6Java + +dependencies { + main_java6CompileOnly 'de.thetaphi:forbiddenapis:3.8' + testImplementation sourceSets.main_java6.output +} /* * Several shadow jars are created @@ -68,7 +87,7 @@ ext.generalShadowJarConfig = { if (!projectName.equals('instrumentation')) { relocate 'org.snakeyaml.engine', 'datadog.snakeyaml.engine' relocate 'okhttp3', 'datadog.okhttp3' - relocate 'okio', 'datadog.okio' + relocate 'okio', 'datadog.okio' } if (!project.hasProperty("disableShadowRelocate") || !disableShadowRelocate) { @@ -176,7 +195,7 @@ shadowJar generalShadowJarConfig >> { attributes( "Main-Class": "datadog.trace.bootstrap.AgentBootstrap", "Agent-Class": "datadog.trace.bootstrap.AgentBootstrap", - "Premain-Class": "datadog.trace.bootstrap.AgentBootstrap", + "Premain-Class": "datadog.trace.bootstrap.AgentPreCheck", "Can-Redefine-Classes": true, "Can-Retransform-Classes": true, ) diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java index ed0db2fc50a..77bc9f8685e 100644 --- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java +++ b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java @@ -9,7 +9,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import java.io.PrintStream; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.net.URI; @@ -126,10 +125,6 @@ private static void agentmainImpl( // since tracer is presumably initialized elsewhere, still considering this complete return; } - if (lessThanJava8()) { - initTelemetry.onAbort("incompatible_runtime"); - return; - } if (isJdkTool()) { initTelemetry.onAbort("jdk_tool"); return; @@ -170,7 +165,7 @@ static boolean getConfig(String configName) { } static boolean exceptionCauseChainContains(Throwable ex, String exClassName) { - Set stack = Collections.newSetFromMap(new IdentityHashMap()); + Set stack = Collections.newSetFromMap(new IdentityHashMap<>()); Throwable t = ex; while (t != null && stack.add(t) && stack.size() <= MAX_EXCEPTION_CHAIN_LENGTH) { // 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() { return false; } - @SuppressForbidden - private static boolean lessThanJava8() { - try { - return lessThanJava8(System.getProperty("java.version"), System.err); - } catch (SecurityException e) { - // Hypothetically, we could version sniff the supported version level - // For now, just skip the check and let the JVM handle things instead - return false; - } - } - - // Reachable for testing - static boolean lessThanJava8(String version, PrintStream output) { - if (parseJavaMajorVersion(version) < 8) { - String agentRawVersion = AgentJar.tryGetAgentVersion(); - String agentVersion = agentRawVersion == null ? "This version" : "Version " + agentRawVersion; - - output.println( - "Warning: " - + agentVersion - + " of dd-java-agent is not compatible with Java " - + version - + " and will not be installed."); - output.println( - "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"); - return true; - } - return false; - } - private static boolean isJdkTool() { String moduleMain = SystemUtils.tryGetProperty("jdk.module.main"); if (null != moduleMain && !moduleMain.isEmpty() && moduleMain.charAt(0) == 'j') { @@ -263,32 +228,6 @@ private static boolean isJdkTool() { return false; } - // Reachable for testing - static int parseJavaMajorVersion(String version) { - int major = 0; - if (null == version || version.isEmpty()) { - return major; - } - int start = 0; - if (version.charAt(0) == '1' - && version.length() >= 3 - && version.charAt(1) == '.' - && Character.isDigit(version.charAt(2))) { - start = 2; - } - // Parse the major digit and be a bit lenient, allowing digits followed by any non digit - for (int i = start; i < version.length(); i++) { - char c = version.charAt(i); - if (Character.isDigit(c)) { - major *= 10; - major += Character.digit(c, 10); - } else { - break; - } - } - return major; - } - @SuppressForbidden static boolean shouldAbortDueToOtherJavaAgents() { // We don't abort if either @@ -333,9 +272,6 @@ static boolean shouldAbortDueToOtherJavaAgents() { } public static void main(final String[] args) { - if (lessThanJava8()) { - return; - } AgentJar.main(args); } diff --git a/dd-java-agent/src/main/java6/datadog/trace/bootstrap/AgentPreCheck.java b/dd-java-agent/src/main/java6/datadog/trace/bootstrap/AgentPreCheck.java new file mode 100644 index 00000000000..a2919226eb5 --- /dev/null +++ b/dd-java-agent/src/main/java6/datadog/trace/bootstrap/AgentPreCheck.java @@ -0,0 +1,194 @@ +package datadog.trace.bootstrap; + +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; + +/** Special lightweight pre-main class that skips installation on incompatible JVMs. */ +public class AgentPreCheck { + public static void premain(final String agentArgs, final Instrumentation inst) { + agentmain(agentArgs, inst); + } + + @SuppressForbidden + public static void agentmain(final String agentArgs, final Instrumentation inst) { + try { + if (compatible()) { + continueBootstrap(agentArgs, inst); + } + } catch (Throwable e) { + // If agent failed we should not fail the application. + // We don't have a log manager here, so just print. + System.err.println("ERROR: " + e.getMessage()); + } + } + + private static void reportIncompatibleJava( + String javaVersion, String javaHome, String agentVersion, PrintStream output) { + output.println( + "Warning: " + + (agentVersion == null ? "This version" : "Version " + agentVersion) + + " of dd-java-agent is not compatible with Java " + + javaVersion + + " found at '" + + javaHome + + "' and is effectively disabled."); + output.println("Please upgrade your Java version to 8+"); + } + + static void sendTelemetry(String forwarderPath, String javaVersion, String agentVersion) { + // Hardcoded payload for unsupported Java version. + String payload = + "{\"metadata\":{" + + "\"runtime_name\":\"jvm\"," + + "\"language_name\":\"jvm\"," + + "\"runtime_version\":\"" + + javaVersion + + "\"," + + "\"language_version\":\"" + + javaVersion + + "\"," + + "\"tracer_version\":\"" + + agentVersion + + "\"}," + + "\"points\":[{" + + "\"name\":\"library_entrypoint.abort\"," + + "\"tags\":[\"reason:incompatible_runtime\"]" + + "}]" + + "}"; + + ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, payload); + t.setDaemon(true); + t.start(); + } + + private static String tryGetProperty(String property) { + try { + return System.getProperty(property); + } catch (SecurityException e) { + return null; + } + } + + @SuppressForbidden + private static boolean compatible() { + String javaVersion = tryGetProperty("java.version"); + String javaHome = tryGetProperty("java.home"); + + return compatible(javaVersion, javaHome, System.err); + } + + // Reachable for testing + static boolean compatible(String javaVersion, String javaHome, PrintStream output) { + int majorJavaVersion = parseJavaMajorVersion(javaVersion); + + if (majorJavaVersion >= 8) { + return true; + } + + String agentVersion = getAgentVersion(); + + reportIncompatibleJava(javaVersion, javaHome, agentVersion, output); + + String forwarderPath = System.getenv("DD_TELEMETRY_FORWARDER_PATH"); + if (forwarderPath != null) { + sendTelemetry(forwarderPath, javaVersion, agentVersion); + } + + return false; + } + + // Reachable for testing + static int parseJavaMajorVersion(String javaVersion) { + int major = 0; + if (javaVersion == null || javaVersion.isEmpty()) { + return major; + } + + int start = 0; + if (javaVersion.charAt(0) == '1' + && javaVersion.length() >= 3 + && javaVersion.charAt(1) == '.' + && Character.isDigit(javaVersion.charAt(2))) { + start = 2; + } + + // Parse the major digit and be a bit lenient, allowing digits followed by any non digit + for (int i = start; i < javaVersion.length(); i++) { + char c = javaVersion.charAt(i); + if (Character.isDigit(c)) { + major *= 10; + major += Character.digit(c, 10); + } else { + break; + } + } + return major; + } + + private static String getAgentVersion() { + try { + // Get the resource as an InputStream + InputStream is = AgentPreCheck.class.getResourceAsStream("/dd-java-agent.version"); + if (is == null) { + return null; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final StringBuilder sb = new StringBuilder(); + for (int c = reader.read(); c != -1; c = reader.read()) { + sb.append((char) c); + } + reader.close(); + + return sb.toString().trim(); + } catch (Throwable e) { + return null; + } + } + + @SuppressForbidden + private static void continueBootstrap(final String agentArgs, final Instrumentation inst) + throws Exception { + Class clazz = Class.forName("datadog.trace.bootstrap.AgentBootstrap"); + Method agentMain = clazz.getMethod("agentmain", String.class, Instrumentation.class); + agentMain.invoke(null, agentArgs, inst); + } + + public static final class ForwarderJsonSenderThread extends Thread { + private final String forwarderPath; + private final String payload; + + public ForwarderJsonSenderThread(String forwarderPath, String payload) { + super("dd-forwarder-json-sender"); + setDaemon(true); + this.forwarderPath = forwarderPath; + this.payload = payload; + } + + @SuppressForbidden + @Override + public void run() { + ProcessBuilder builder = new ProcessBuilder(forwarderPath, "library_entrypoint"); + try { + Process process = builder.start(); + OutputStream out = null; + try { + out = process.getOutputStream(); + out.write(payload.getBytes()); + } finally { + if (out != null) { + out.close(); + } + } + } catch (Throwable e) { + System.err.println("Failed to send telemetry: " + e.getMessage()); + } + } + } +} diff --git a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentBootstrapTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentBootstrapTest.groovy index ddeb330b672..6f2cd97fb77 100644 --- a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentBootstrapTest.groovy +++ b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentBootstrapTest.groovy @@ -3,97 +3,6 @@ package datadog.trace.bootstrap import spock.lang.Specification class AgentBootstrapTest extends Specification { - def 'parse java.version strings'() { - when: - def major = AgentBootstrap.parseJavaMajorVersion(version) - - then: - major == expected - - where: - version | expected - null | 0 - '' | 0 - 'a.0.0' | 0 - '0.a.0' | 0 - '0.0.a' | 0 - '1.a.0_0' | 1 - '1.8.a_0' | 8 - '1.8.0_a' | 8 - '1.7' | 7 - '1.7.0' | 7 - '1.7.0_221' | 7 - '1.8' | 8 - '1.8.0' | 8 - '1.8.0_212' | 8 - '1.8.0_292' | 8 - '9-ea' | 9 - '9.0.4' | 9 - '9.1.2' | 9 - '10.0.2' | 10 - '11' | 11 - '11a' | 11 - '11.0.6' | 11 - '11.0.11' | 11 - '12.0.2' | 12 - '13.0.2' | 13 - '14' | 14 - '14.0.2' | 14 - '15' | 15 - '15.0.2' | 15 - '16.0.1' | 16 - '11.0.9.1+1' | 11 - '11.0.6+10' | 11 - } - - def 'log warning message when java version is less than 8'() { - setup: - def baos = new ByteArrayOutputStream() - def logStream = new PrintStream(baos) - - when: - def isLowerThan8 = AgentBootstrap.lessThanJava8(version, logStream) - logStream.flush() - def logLines = Arrays.asList(baos.toString().split('\n')) - // If the list only contains a single String and that is the empty String, then the set is empty - if (logLines.size() == 1 && logLines.contains('')) { - logLines = [] - } - - then: - isLowerThan8 == expectedLower - if (!expectedLower) { - assert logLines.isEmpty() - } else { - assert logLines.size() == 2 - assert logLines == [ - "Warning: Version ${AgentJar.getAgentVersion()} of dd-java-agent is not compatible with Java ${version} and will not be installed.", - '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' - ] - } - - where: - version | expectedLower - null | true - '' | true - 'a.0.0' | true - '0.a.0' | true - '0.0.a' | true - '1.a.0_0' | true - '1.8.a_0' | false - '1.8.0_a' | false - '1.7' | true - '1.7.0' | true - '1.7.0_221' | true - '1.8' | false - '9.0.4' | false - '10.0.2' | false - '11a' | false - '15' | false - '11.0.9.1+1' | false - - } - def 'return true when first exception in the cause chain is the specified exception'() { setup: def ex = new IOException() diff --git a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy new file mode 100644 index 00000000000..e67614813b5 --- /dev/null +++ b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy @@ -0,0 +1,165 @@ +package datadog.trace.bootstrap + +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.PosixFilePermissions + +class AgentPreCheckTest extends Specification { + def 'parse java.version of #version as #expected'() { + when: + def major = AgentPreCheck.parseJavaMajorVersion(version) + + then: + major == expected + + where: + version | expected + null | 0 + '' | 0 + 'a.0.0' | 0 + '0.a.0' | 0 + '0.0.a' | 0 + '1.a.0_0' | 1 + '1.6' | 6 + '1.6.0_45' | 6 + '1.7' | 7 + '1.7.0' | 7 + '1.7.0_221' | 7 + '1.8.a_0' | 8 + '1.8.0_a' | 8 + '1.8' | 8 + '1.8.0' | 8 + '1.8.0_212' | 8 + '1.8.0_292' | 8 + '9-ea' | 9 + '9.0.4' | 9 + '9.1.2' | 9 + '10.0.2' | 10 + '11' | 11 + '11a' | 11 + '11.0.6' | 11 + '11.0.11' | 11 + '12.0.2' | 12 + '13.0.2' | 13 + '14' | 14 + '14.0.2' | 14 + '15' | 15 + '15.0.2' | 15 + '16.0.1' | 16 + '11.0.9.1+1' | 11 + '11.0.6+10' | 11 + '17.0.15' | 17 + '21.0.7' | 21 + } + + def 'log warning message when java is not compatible'() { + setup: + def output = new ByteArrayOutputStream() + def logStream = new PrintStream(output) + + when: + boolean compatible = AgentPreCheck.compatible(javaVersion, "/Library/$javaVersion", logStream) + String log = output.toString() + def logLines = log.isEmpty() ? [] : Arrays.asList(log.split('\n')) + + then: + compatible == expectedCompatible + + if (expectedCompatible) { + assert logLines.isEmpty() + } else { + logLines.size() == 2 + def expectedLogLines = [ + "Warning: Version ${AgentJar.getAgentVersion()} of dd-java-agent is not compatible with Java $javaVersion found at '/Library/$javaVersion' and is effectively disabled.", + "Please upgrade your Java version to 8+" + ] + assert logLines == expectedLogLines + } + where: + javaVersion | expectedCompatible + null | false + '' | false + '1.6.0_45' | false + '1.7.0_221' | false + '1.8.0_212' | true + '11.0.6' | true + '17.0.15' | true + '21.0.7' | true + } + + def 'send hardcoded bootstrap telemetry for unsupported java'() { + setup: + Path path = Files.createTempFile('test-forwarder', '.sh', PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString('rwxr--r--'))) + File forwarderFile = path.toFile() + forwarderFile.deleteOnExit() + + String forwarderPath = forwarderFile.getAbsolutePath() + + File outputFile = new File(forwarderPath + '.out') + outputFile.deleteOnExit() + + def script = [ + '#!/usr/bin/env bash', + 'echo "$1 $(cat -)" >>' + outputFile.getAbsolutePath(), + '' + ] + forwarderFile << script.join('\n') + + when: + AgentPreCheck.sendTelemetry(forwarderPath, '1.6.0_45', '1.50') + + then: + // Await completion of the external process handling the payload. + new PollingConditions().within(5) { + assert outputFile.exists() + } + String payload = outputFile.text + + String expectedPayload = ''' +{ + "metadata": { + "runtime_name": "jvm", + "language_name": "jvm", + "runtime_version": "1.6.0_45", + "language_version": "1.6.0_45", + "tracer_version": "1.50" + }, + "points": [ + { + "name": "library_entrypoint.abort", + "tags": [ + "reason:incompatible_runtime" + ] + } + ] +'''.replaceAll(/\s+/, '') + + // Assert that the actual payload contains the expected data. + payload.contains(expectedPayload) + } + + def 'check #clazz compiled with Java #javaVersion'() { + setup: + def resource = clazz.getName().replace('.', '/') + '.class' + def stream = new DataInputStream(this.getClass().getClassLoader().getResourceAsStream(resource)) + + expect: + stream.withCloseable { + def magic = Integer.toUnsignedLong(it.readInt()) + def minor = (int) it.readShort() + def major = (int) it.readShort() + + assert magic == 0xCAFEBABEL + assert minor == 0 + assert major == expectedMajor + } == null + + where: + clazz | javaVersion | expectedMajor + AgentPreCheck | 6 | 50 + AgentBootstrap | 8 | 52 + } +} diff --git a/test-published-dependencies/agent-logs-on-java-7/src/test/java/StartWithAgentTest.java b/test-published-dependencies/agent-logs-on-java-7/src/test/java/StartWithAgentTest.java index b9acd13cb51..b354b5e30e0 100644 --- a/test-published-dependencies/agent-logs-on-java-7/src/test/java/StartWithAgentTest.java +++ b/test-published-dependencies/agent-logs-on-java-7/src/test/java/StartWithAgentTest.java @@ -14,8 +14,8 @@ public class StartWithAgentTest { - private static final Pattern WARNING_PATTERN = Pattern.compile("^Warning: Version [^ ]+ of dd-java-agent is not compatible with Java [^ ]+ and will not be installed\\.$"); - private static final String UPGRADE_MESSAGE = "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"; + private static final Pattern WARNING_PATTERN = Pattern.compile("^Warning: Version [^ ]+ of dd-java-agent is not compatible with Java [^ ]+ found at [^ ]+ and is effectively disabled\\.$"); + private static final String UPGRADE_MESSAGE = "Please upgrade your Java version to 8+"; @Test void ensureThatApplicationStartsWithAgentOnJava7() throws InterruptedException, IOException {