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 2078f09ecec..0114503447c 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 @@ -216,41 +216,74 @@ private static boolean alreadyInitialized() { return false; } + /** + * Returns {@code true} if the JVM is running a JDK diagnostic/development tool rather than a user + * application, in which case the agent should abort early. + * + *

How to discover new entries when a tool is missed: + * + *

    + *
  1. Build a minimal javaagent JAR whose {@code premain} prints {@code + * System.getProperty("jdk.module.main")} and {@code System.getProperty("sun.java.command")} + * then calls {@code System.exit(0)}. + *
  2. For JDK 9+ tools, inject it via {@code JAVA_TOOL_OPTIONS}: + *
    JAVA_TOOL_OPTIONS="-javaagent:/path/to/agent.jar" $JAVA_HOME/bin/<tool>
    + * The value of {@code jdk.module.main} is the module name to add to the first switch. + *
  3. For JDK 8 tools (or non-modular IBM/OpenJ9 tools), inject the same way; the value + * of {@code sun.java.command} (up to the first space) is the main-class name to add to the + * second switch. + *
+ * + *

Native binaries (e.g. {@code jitserver}, {@code asprof}) report both properties as {@code + * null} and are automatically ignored — no switch entry needed for them. + */ static boolean isJdkTool() { String moduleMain = SystemProperties.get("jdk.module.main"); - if (null != moduleMain && !moduleMain.isEmpty() && moduleMain.charAt(0) == 'j') { - switch (moduleMain) { - case "java.base": // keytool - case "java.corba": - case "java.desktop": - case "java.rmi": - case "java.scripting": - case "java.security.jgss": - case "jdk.aot": - case "jdk.compiler": - case "jdk.dev": - case "jdk.hotspot.agent": - case "jdk.httpserver": - case "jdk.jartool": - case "jdk.javadoc": - case "jdk.jcmd": - case "jdk.jconsole": - case "jdk.jdeps": - case "jdk.jdi": - case "jdk.jfr": - case "jdk.jlink": - case "jdk.jpackage": - case "jdk.jshell": - case "jdk.jstatd": - case "jdk.jvmstat.rmi": - case "jdk.pack": - case "jdk.pack200": - case "jdk.policytool": - case "jdk.rmic": - case "jdk.scripting.nashorn.shell": - case "jdk.xml.bind": - case "jdk.xml.ws": - return true; + if (null != moduleMain && !moduleMain.isEmpty()) { + char firstChar = moduleMain.charAt(0); + if (firstChar == 'j') { + // Standard JDK 9+ module-based tools (module names start with 'java.' or 'jdk.') + switch (moduleMain) { + case "java.base": // keytool + case "java.corba": + case "java.desktop": + case "java.rmi": + case "java.scripting": + case "java.security.jgss": + case "jdk.aot": + case "jdk.compiler": + case "jdk.dev": + case "jdk.hotspot.agent": + case "jdk.httpserver": + case "jdk.jartool": + case "jdk.javadoc": + case "jdk.jcmd": + case "jdk.jconsole": + case "jdk.jdeps": + case "jdk.jdi": + case "jdk.jfr": + case "jdk.jlink": + case "jdk.jpackage": + case "jdk.jshell": + case "jdk.jstatd": + case "jdk.jvmstat.rmi": + case "jdk.pack": + case "jdk.pack200": + case "jdk.policytool": + case "jdk.rmic": + case "jdk.scripting.nashorn.shell": + case "jdk.xml.bind": + case "jdk.xml.ws": + return true; + } + } else if (firstChar == 'o') { + // OpenJ9 / Semeru 11+ module-based tools (module names start with 'openj9.') + switch (moduleMain) { + case "openj9.dtfj": // jextract, jpackcore + case "openj9.dtfjview": // jdmpview + case "openj9.traceformat": // traceformat + return true; + } } } // Handles JDK 8 tools (IBM J9 and standard JDK 8 vendors) @@ -267,9 +300,18 @@ static boolean isJdkTool() { case "com.ibm.security.krb5.internal.tools.Klist": // klist case "com.ibm.security.krb5.internal.tools.Ktab": // ktab case "com.ibm.jvm.dtfjview.DTFJView": // jdmpview + case "com.ibm.jvm.j9.dump.extract.Main": // jextract + case "com.ibm.gsk.ikeyman.Ikeyman": // ikeyman case "com.ibm.gsk.ikeyman.ikeycmd": // ikeycmd case "com.ibm.CosNaming.TransientNameServer": // tnameserv case "com.ibm.idl.toJavaPortable.Compile": // idlj + // OpenJ9 / Semeru 8 specific tool main classes (OpenJ9 reimplementation of HotSpot tools) + case "openj9.tools.attach.diagnostics.tools.Jcmd": // jcmd + case "openj9.tools.attach.diagnostics.tools.Jps": // jps + case "openj9.tools.attach.diagnostics.tools.Jstat": // jstat + case "openj9.tools.attach.diagnostics.tools.Jmap": // jmap + case "openj9.tools.attach.diagnostics.tools.Jstack": // jstack + case "com.ibm.jvm.TraceFormat": // traceformat // Standard JDK 8 tool main classes (shared by IBM J9 and Oracle/OpenJDK 8) case "sun.tools.jar.Main": // jar case "com.sun.tools.javac.Main": // javac @@ -292,6 +334,7 @@ static boolean isJdkTool() { case "com.sun.tools.internal.xjc.Driver": // xjc case "com.sun.tools.internal.jxc.SchemaGenerator": // schemagen case "com.sun.tools.script.shell.Main": // jrunscript + case "jdk.nashorn.tools.Shell": // jjs (Nashorn JS shell, JDK 8) case "sun.tools.jconsole.JConsole": // jconsole case "sun.applet.Main": // appletviewer case "com.sun.corba.se.impl.naming.cosnaming.TransientNameServer": // tnameserv @@ -310,6 +353,8 @@ static boolean isJdkTool() { case "jdk.jfr.internal.tool.Main": // jfr, backported to OpenJDK 8 in 8u262 (JEP 328 // backport, July 2020) case "sun.jvm.hotspot.jdi.SADebugServer": // jsadebugd + case "sun.jvm.hotspot.HSDB": // hsdb (HotSpot SA GUI debugger, JDK 8) + case "sun.jvm.hotspot.CLHSDB": // clhsdb (HotSpot SA command-line debugger, JDK 8) return true; } } diff --git a/dd-java-agent/src/test/java/datadog/trace/bootstrap/AgentBootstrapAbortOnJdkToolTest.java b/dd-java-agent/src/test/java/datadog/trace/bootstrap/AgentBootstrapAbortOnJdkToolTest.java new file mode 100644 index 00000000000..baa92b46ecb --- /dev/null +++ b/dd-java-agent/src/test/java/datadog/trace/bootstrap/AgentBootstrapAbortOnJdkToolTest.java @@ -0,0 +1,149 @@ +package datadog.trace.bootstrap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AgentBootstrapAbortOnJdkToolTest { + + private String savedModuleMain; + private String savedJavaCommand; + + @BeforeEach + void saveAndClearProperties() { + savedModuleMain = System.clearProperty("jdk.module.main"); + savedJavaCommand = System.clearProperty("sun.java.command"); + } + + @AfterEach + void restoreProperties() { + restoreProperty("jdk.module.main", savedModuleMain); + restoreProperty("sun.java.command", savedJavaCommand); + } + + private static void restoreProperty(String key, String previousValue) { + if (previousValue == null) { + System.clearProperty(key); + } else { + System.setProperty(key, previousValue); + } + } + + @Test + void notAJdkToolWhenNoPropertiesSet() { + assertFalse(AgentBootstrap.isJdkTool()); + } + + @Test + void notAJdkToolWhenCommandIsNotAKnownTool() { + System.setProperty("sun.java.command", "com.example.MyApplication"); + assertFalse(AgentBootstrap.isJdkTool()); + } + + @Test + void notAJdkToolWhenModuleMainIsNotAKnownTool() { + System.setProperty("jdk.module.main", "com.example.myapp"); + assertFalse(AgentBootstrap.isJdkTool()); + } + + @ParameterizedTest + @ValueSource( + strings = { + // Standard JDK 9+ module-based tools + "java.base", // keytool + "jdk.compiler", // javac + "jdk.jartool", // jar + "jdk.javadoc", // javadoc + "jdk.jcmd", // jcmd + "jdk.jconsole", // jconsole + "jdk.jshell", // jshell + "jdk.jfr", // jfr (JDK 9+) + // OpenJ9 / Semeru 11+ module-based tools + "openj9.dtfj", // jextract, jpackcore + "openj9.dtfjview", // jdmpview + "openj9.traceformat", // traceformat + }) + void isJdkToolByModuleMain(String moduleMain) { + System.setProperty("jdk.module.main", moduleMain); + assertTrue(AgentBootstrap.isJdkTool()); + } + + @ParameterizedTest + @ValueSource( + strings = { + // IBM J9 JDK 8 specific tool main classes + "com.ibm.crypto.tools.KeyTool", // keytool + "com.ibm.security.krb5.internal.tools.Kinit", // kinit + "com.ibm.security.krb5.internal.tools.Klist", // klist + "com.ibm.security.krb5.internal.tools.Ktab", // ktab + "com.ibm.jvm.dtfjview.DTFJView", // jdmpview + "com.ibm.jvm.j9.dump.extract.Main", // jextract + "com.ibm.gsk.ikeyman.Ikeyman", // ikeyman + "com.ibm.gsk.ikeyman.ikeycmd", // ikeycmd + "com.ibm.CosNaming.TransientNameServer", // tnameserv + "com.ibm.idl.toJavaPortable.Compile", // idlj + // OpenJ9 / Semeru 8 specific tool main classes (OpenJ9 reimplementation of HotSpot tools) + "openj9.tools.attach.diagnostics.tools.Jcmd", // jcmd + "openj9.tools.attach.diagnostics.tools.Jps", // jps + "openj9.tools.attach.diagnostics.tools.Jstat", // jstat + "openj9.tools.attach.diagnostics.tools.Jmap", // jmap + "openj9.tools.attach.diagnostics.tools.Jstack", // jstack + "com.ibm.jvm.TraceFormat", // traceformat + // Standard JDK 8 tool main classes (Corretto 8 / OpenJDK 8) + "sun.tools.jar.Main", // jar + "com.sun.tools.javac.Main", // javac + "com.sun.tools.javadoc.Main", // javadoc + "com.sun.tools.javap.Main", // javap + "com.sun.tools.javah.Main", // javah + "sun.security.tools.keytool.Main", // keytool + "sun.security.tools.jarsigner.Main", // jarsigner + "sun.security.tools.policytool.PolicyTool", // policytool + "com.sun.tools.example.debug.tty.TTY", // jdb + "com.sun.tools.jdeps.Main", // jdeps + "sun.rmi.rmic.Main", // rmic + "sun.rmi.registry.RegistryImpl", // rmiregistry + "sun.rmi.server.Activation", // rmid + "com.sun.tools.extcheck.Main", // extcheck + "sun.tools.serialver.SerialVer", // serialver + "sun.tools.native2ascii.Main", // native2ascii + "com.sun.tools.internal.ws.WsGen", // wsgen + "com.sun.tools.internal.ws.WsImport", // wsimport + "com.sun.tools.internal.xjc.Driver", // xjc + "com.sun.tools.internal.jxc.SchemaGenerator", // schemagen + "com.sun.tools.script.shell.Main", // jrunscript + "sun.tools.jconsole.JConsole", // jconsole + "sun.applet.Main", // appletviewer + "com.sun.corba.se.impl.naming.cosnaming.TransientNameServer", // tnameserv + "com.sun.tools.corba.se.idl.toJavaPortable.Compile", // idlj + "com.sun.corba.se.impl.activation.ORBD", // orbd + "com.sun.corba.se.impl.activation.ServerTool", // servertool + "sun.tools.jps.Jps", // jps + "sun.tools.jstack.JStack", // jstack + "sun.tools.jmap.JMap", // jmap + "sun.tools.jinfo.JInfo", // jinfo + "com.sun.tools.hat.Main", // jhat + "sun.tools.jstat.Jstat", // jstat + "sun.tools.jstatd.Jstatd", // jstatd + "sun.tools.jcmd.JCmd", // jcmd + "jdk.jfr.internal.tool.Main", // jfr (OpenJDK 8u262+ backport) + "sun.jvm.hotspot.jdi.SADebugServer", // jsadebugd + "jdk.nashorn.tools.Shell", // jjs (Nashorn JS shell, JDK 8) + "sun.jvm.hotspot.HSDB", // hsdb (HotSpot SA GUI debugger, JDK 8) + "sun.jvm.hotspot.CLHSDB", // clhsdb (HotSpot SA command-line debugger, JDK 8) + }) + void isJdkToolByCommand(String mainClass) { + System.setProperty("sun.java.command", mainClass); + assertTrue(AgentBootstrap.isJdkTool()); + } + + @Test + void isJdkToolWhenCommandIncludesArguments() { + System.setProperty("sun.java.command", "com.ibm.crypto.tools.KeyTool -list -v"); + assertTrue(AgentBootstrap.isJdkTool()); + } +} diff --git a/dd-smoke-tests/jdk-tool-abort/build.gradle b/dd-smoke-tests/jdk-tool-abort/build.gradle new file mode 100644 index 00000000000..81d57f36d80 --- /dev/null +++ b/dd-smoke-tests/jdk-tool-abort/build.gradle @@ -0,0 +1,6 @@ +apply from: "$rootDir/gradle/java.gradle" +description = 'JDK Tool Abort Smoke Tests' + +dependencies { + testImplementation project(':dd-smoke-tests') +} diff --git a/dd-smoke-tests/jdk-tool-abort/gradle.lockfile b/dd-smoke-tests/jdk-tool-abort/gradle.lockfile new file mode 100644 index 00000000000..f3b2c0bd606 --- /dev/null +++ b/dd-smoke-tests/jdk-tool-abort/gradle.lockfile @@ -0,0 +1,112 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cafe.cryptography:curve25519-elisabeth:0.1.0=testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.3=testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.3=testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=testRuntimeClasspath +com.github.javaparser:javaparser-core:3.25.6=codenarc +com.github.jnr:jffi:1.3.14=testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.19=testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.18=testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.21=testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.24=testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs:4.9.8=spotbugs +com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.13.2=spotbugs +com.google.errorprone:error_prone_annotations:2.41.0=spotbugs +com.google.guava:guava:20.0=testCompileClasspath,testRuntimeClasspath +com.google.re2j:re2j:1.7=testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath +com.thoughtworks.qdox:qdox:1.12.1=codenarc +commons-fileupload:commons-fileupload:1.5=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.20.0=spotbugs +de.thetaphi:forbiddenapis:3.10=compileClasspath,testCompileClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath +io.sqreen:libsqreen:17.3.0=testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=testCompileClasspath,testRuntimeClasspath +jaxen:jaxen:2.0.0=spotbugs +junit:junit:4.13.2=testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.3=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.3=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=testRuntimeClasspath +net.sf.saxon:Saxon-HE:12.9=spotbugs +org.apache.ant:ant-antlr:1.10.14=codenarc +org.apache.ant:ant-junit:1.10.14=codenarc +org.apache.bcel:bcel:6.11.0=spotbugs +org.apache.commons:commons-lang3:3.19.0=spotbugs +org.apache.commons:commons-text:1.14.0=spotbugs +org.apache.logging.log4j:log4j-api:2.25.2=spotbugs +org.apache.logging.log4j:log4j-core:2.25.2=spotbugs +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.codehaus.groovy:groovy-ant:3.0.23=codenarc +org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc +org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-templates:3.0.23=codenarc +org.codehaus.groovy:groovy-xml:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codenarc:CodeNarc:3.7.0=codenarc +org.dom4j:dom4j:2.2.0=spotbugs +org.gmetrics:GMetrics:2.1.0=codenarc +org.hamcrest:hamcrest-core:1.3=testRuntimeClasspath +org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=testRuntimeClasspath +org.jctools:jctools-core:4.0.6=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=testRuntimeClasspath +org.junit:junit-bom:5.14.0=spotbugs +org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=testRuntimeClasspath +org.msgpack:msgpack-core:0.8.24=testRuntimeClasspath +org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=spotbugs +org.ow2.asm:asm-commons:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-commons:9.9=spotbugs +org.ow2.asm:asm-tree:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-tree:9.9=spotbugs +org.ow2.asm:asm-util:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-util:9.9=spotbugs +org.ow2.asm:asm:9.9=spotbugs +org.ow2.asm:asm:9.9.1=testCompileClasspath,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.32=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j +org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j +org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-parser:1.2.0=testCompileClasspath,testRuntimeClasspath +org.xmlresolver:xmlresolver:5.3.3=spotbugs +empty=annotationProcessor,runtimeClasspath,spotbugsPlugins,testAnnotationProcessor diff --git a/dd-smoke-tests/jdk-tool-abort/src/test/groovy/datadog/smoketest/JdkToolAbortSmokeTest.groovy b/dd-smoke-tests/jdk-tool-abort/src/test/groovy/datadog/smoketest/JdkToolAbortSmokeTest.groovy new file mode 100644 index 00000000000..5078ca9c361 --- /dev/null +++ b/dd-smoke-tests/jdk-tool-abort/src/test/groovy/datadog/smoketest/JdkToolAbortSmokeTest.groovy @@ -0,0 +1,194 @@ +package datadog.smoketest + +import datadog.trace.agent.test.server.http.TestHttpServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer + +/** + * Smoke tests that verify the agent aborts transparently when attached to a JDK tool binary. + *

+ * Each test case discovers a real JDK tool binary from the test JVM's {@code $JAVA_HOME/bin/}, + * runs it with the agent attached via {@code -J-javaagent:}, and asserts that no {@code /info} + * request reached the test backend. The {@code /info} call is the first thing a fully-initialized + * agent makes to negotiate capabilities; its absence proves the early-abort path was taken. + */ +class JdkToolAbortSmokeTest extends Specification { + + private static final int TOOL_TIMEOUT_SECS = 15 + + /** + * Binaries excluded from testing, for one of the following reasons: + *

+ */ + private static final Set EXCLUDED_TOOLS = [ + // JVM launchers + "java", + "javaw", + "javaws", + // Long-running daemons / servers + "rmiregistry", + "rmid", + "orbd", + "servertool", + "tnameserv", + "jstatd", + // Requires an attached JVM to operate + "jsadebugd", + // GUI tools that open windows and block + "jconsole", + "appletviewer", + "jvisualvm", + "jmc", + // HotSpot SA GUI debugger (JDK 8) + "hsdb", + // HotSpot SA command-line debugger (JDK 8) + "clhsdb", + // Deprecated/removed packaging tools + "pack200", + "unpack200", + // Windows CGI adapter — not executable on POSIX systems + "java-rmi.cgi", + // Async Profiler binaries (bundled by some JVM distributions) + "asprof", + "jfrconv", + // OpenJ9/Semeru JIT Compilation Server (C++ daemon) + "jitserver", + ] as Set + + @Shared + String agentJar = System.getProperty("datadog.smoketest.agent.shadowJar.path") + + @Shared + List jdkTools = discoverJdkTools() + + @Shared + AtomicInteger infoRequestCount = new AtomicInteger() + + @Shared + @AutoCleanup + TestHttpServer server = httpServer { + handlers { + prefix("/info") { + infoRequestCount.incrementAndGet() + response.status(200).send("""{ + "version": "7.54.1", + "endpoints": ["/v0.4/traces", "/v0.5/traces", "/telemetry/proxy/"] + }""") + } + prefix("/v0.4/traces") { + response.status(200).send() + } + prefix("/v0.5/traces") { + response.status(200).send() + } + prefix("/telemetry/proxy/api/v2/apmtelemetry") { + response.status(202).send() + } + } + } + + def setupSpec() { + assert agentJar != null: "datadog.smoketest.agent.shadowJar.path not set" + assert new File(agentJar).isFile(): "Agent jar not found: $agentJar" + assert !jdkTools.isEmpty(): "No JDK tools discovered under ${System.getProperty('java.home')}" + server.start() + } + + def setup() { + infoRequestCount.set(0) + } + + def "agent does not connect to backend when attached to JDK tool '#toolName'"() { + when: "the tool is run with the agent attached via -J-javaagent" + runToolWithAgent(tool) + + then: "no /info request reached the backend — agent took the early-abort path" + infoRequestCount.get() == 0 + + where: + tool << jdkTools + toolName = tool.name + } + + /** + * Runs {@code binary} with the agent attached via {@code -J-javaagent:} and the test backend + * wired via {@code -J-Ddd.trace.agent.port=}. Closes stdin, drains stdout/stderr on a + * background thread, and waits up to {@link #TOOL_TIMEOUT_SECS} seconds. + */ + private void runToolWithAgent(File binary) { + List cmd = [ + binary.absolutePath, + "-J-javaagent:${agentJar}", + "-J-Ddd.trace.agent.port=${server.address.port}", + "-J-Ddd.agent.host=localhost", + ]*.toString() + Process proc = new ProcessBuilder(cmd) + .redirectErrorStream(true) + .start() + // Close stdin so interactive tools (e.g. jdb) do not block waiting for input. + proc.outputStream.close() + // Drain stdout/stderr on a background thread to prevent pipe buffer saturation. + Thread.start { + try { + proc.inputStream.bytes + } catch (ignored) {} + } + if (!proc.waitFor(TOOL_TIMEOUT_SECS, TimeUnit.SECONDS)) { + System.err.println("WARNING: ${binary.name} did not exit within ${TOOL_TIMEOUT_SECS}s — forcibly killed") + proc.destroyForcibly() + } + // Brief settling delay: give any in-flight loopback TCP data time to arrive at the + // server before the assertion in the then: block reads infoRequestCount. + Thread.sleep(200) + } + + /** + * Discovers executable files under {@code $JAVA_HOME/bin/}. On JDK 8, {@code java.home} + * points at the nested {@code jre/} directory inside the JDK, so the parent {@code bin/} + * is also scanned to pick up SDK tools (javac, javap, …). + * Results are sorted alphabetically and filtered against {@link #EXCLUDED_TOOLS}. + */ + private static List discoverJdkTools() { + String javaHome = System.getProperty("java.home") + File javaHomeDir = new File(javaHome) + Set scanDirs = new LinkedHashSet<>() + scanDirs.add(new File(javaHomeDir, "bin")) + // On JDK 8 java.home points to .../jdk1.8.x/jre, so scan parent bin for SDK tools + if (javaHomeDir.name == "jre") { + scanDirs.add(new File(javaHomeDir.parentFile, "bin")) + } + // Use a TreeMap so iteration order is deterministic (alphabetical by name) + Map tools = new TreeMap<>() + for (File dir : scanDirs) { + if (!dir.isDirectory()) { + continue + } + dir.listFiles()?.each { File f -> + if (!f.isFile() || !f.canExecute()) { + return + } + String name = f.name.replaceFirst(/\.exe$/, "") + if (!(name in EXCLUDED_TOOLS)) { + tools.putIfAbsent(name, f) + } + } + } + return tools.values().toList() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0bac052092a..8a5e81fd7ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -186,6 +186,7 @@ include( ":dd-smoke-tests:gradle", ":dd-smoke-tests:grpc-1.5", ":dd-smoke-tests:java9-modules", + ":dd-smoke-tests:jdk-tool-abort", ":dd-smoke-tests:jersey-2", ":dd-smoke-tests:jersey-3", ":dd-smoke-tests:jboss-modules",