mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: support agent attacher list java processes and attach all
This commit is contained in:
@@ -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 {
|
||||
Attacher.attach(args[0]);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user