diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/PortUtils.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/PortUtils.java index df7830a7506..b0d51699f64 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/PortUtils.java +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/PortUtils.java @@ -1,10 +1,13 @@ package datadog.trace.agent.test.utils; +import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.IOException; +import java.lang.reflect.Field; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -111,10 +114,23 @@ private static boolean isPortOpen(String host, int port) { } } + @SuppressForbidden public static void waitForPortToOpen( final int port, final long timeout, final TimeUnit unit, final Process process) { final long startedAt = System.currentTimeMillis(); final long waitUntil = startedAt + unit.toMillis(timeout); + final long progressIntervalMillis = TimeUnit.SECONDS.toMillis(30); + long nextProgressAt = startedAt + progressIntervalMillis; + final long pid = tryGetPid(process); + + System.err.println( + "[PortUtils] Waiting up to " + + unit.toMillis(timeout) + + "ms for port " + + port + + " to open (process pid=" + + pid + + ")"); while (System.currentTimeMillis() < waitUntil) { try { @@ -138,10 +154,45 @@ public static void waitForPortToOpen( } if (isPortOpen(port)) { + System.err.println( + "[PortUtils] Port " + + port + + " opened after " + + (System.currentTimeMillis() - startedAt) + + "ms"); return; } + + long now = System.currentTimeMillis(); + if (now >= nextProgressAt) { + System.err.println( + "[PortUtils] Still waiting for port " + + port + + " (pid=" + + pid + + ") elapsed=" + + (now - startedAt) + + "ms alive=" + + process.isAlive()); + nextProgressAt = now + progressIntervalMillis; + } } + // Timeout: capture diagnostics before throwing so the next failure tells us what was hung. + System.err.println( + "[PortUtils] Timed out waiting for port " + + port + + " after " + + unit.toMillis(timeout) + + "ms; pid=" + + pid + + " alive=" + + process.isAlive()); + if (pid > 0) { + requestChildThreadDump(pid); + } + dumpTestJvmThreads(); + throw new RuntimeException( "Timed out waiting for port " + port @@ -151,6 +202,56 @@ public static void waitForPortToOpen( + System.currentTimeMillis()); } + @SuppressForbidden + private static long tryGetPid(Process process) { + try { + Field pidField = process.getClass().getDeclaredField("pid"); + pidField.setAccessible(true); + return pidField.getLong(process); + } catch (Throwable t) { + System.err.println("[PortUtils] Could not extract child pid: " + t); + return -1; + } + } + + @SuppressForbidden + private static void requestChildThreadDump(long pid) { + String osName = System.getProperty("os.name", "").toLowerCase(); + if (osName.startsWith("windows")) { + System.err.println("[PortUtils] Skipping SIGQUIT thread dump on Windows (pid=" + pid + ")"); + return; + } + try { + System.err.println( + "[PortUtils] Sending SIGQUIT (kill -3) to pid " + pid + " to trigger thread dump"); + Process kill = + new ProcessBuilder("kill", "-3", String.valueOf(pid)).redirectErrorStream(true).start(); + kill.waitFor(5, TimeUnit.SECONDS); + // HotSpot writes the dump to the child stderr (captured in the test process log). + // IBM J9 writes a javacore.* file (see -Xdump:directory, default /tmp in smoke tests). + // Give the JVM a moment to finish writing before we throw. + Thread.sleep(2000); + System.err.println("[PortUtils] SIGQUIT delivered; check child process log / javacore files"); + } catch (Throwable t) { + System.err.println("[PortUtils] Failed to send SIGQUIT to pid " + pid + ": " + t); + } + } + + @SuppressForbidden + private static void dumpTestJvmThreads() { + System.err.println("[PortUtils] === Test JVM thread dump ==="); + Map stacks = Thread.getAllStackTraces(); + for (Map.Entry entry : stacks.entrySet()) { + Thread t = entry.getKey(); + System.err.println("\"" + t.getName() + "\" id=" + t.getId() + " state=" + t.getState()); + for (StackTraceElement ste : entry.getValue()) { + System.err.println("\tat " + ste); + } + System.err.println(); + } + System.err.println("[PortUtils] === End of test JVM thread dump ==="); + } + public static void waitForPortToOpen(String host, int port, long timeout, TimeUnit unit) { waitForPort(host, port, timeout, unit, true); } diff --git a/dd-smoke-tests/gradle/src/test/groovy/datadog/smoketest/GradleLauncherSmokeTest.groovy b/dd-smoke-tests/gradle/src/test/groovy/datadog/smoketest/GradleLauncherSmokeTest.groovy index d8302d1cfe8..5c7833612cd 100644 --- a/dd-smoke-tests/gradle/src/test/groovy/datadog/smoketest/GradleLauncherSmokeTest.groovy +++ b/dd-smoke-tests/gradle/src/test/groovy/datadog/smoketest/GradleLauncherSmokeTest.groovy @@ -68,7 +68,7 @@ class GradleLauncherSmokeTest extends AbstractGradleTest { Thread.sleep(2000) // small delay for rapid retries on network issues } } - throw new AssertionError((Object) "Tried $GRADLE_WRAPPER_RETRIES times to execute gradle wrapper command and failed") + throw new AssertionError((Object) "Tried $GRADLE_WRAPPER_RETRIES times to execute gradle wrapper command and failed.") } private String whenRunningGradleLauncherWithJavaTracerInjected(String gradleDaemonCmdLineParams) {