Skip to content

Commit

Permalink
Review AgentLoader
Browse files Browse the repository at this point in the history
  • Loading branch information
hexiaofeng committed Jun 11, 2024
1 parent d822e0d commit 3b4a97d
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 134 deletions.
40 changes: 40 additions & 0 deletions joylive-bootstrap/joylive-bootstrap-premain/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@

<properties>
<final.name>live</final.name>
<jcommander.version>1.82</jcommander.version>
</properties>

<dependencies>
<dependency>
<groupId>com.jd.live</groupId>
<artifactId>joylive-bootstrap-api</artifactId>
</dependency>
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>${jcommander.version}</version>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -47,6 +53,40 @@
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<configuration>
<artifactSet>
<includes>
<include>com.beust:jcommander</include>
</includes>
</artifactSet>
<transformers>
<!-- This transformer will merge the contents of META-INF/services -->
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
<addHeader>false</addHeader>
</transformer>
</transformers>
<relocations>
<relocation>
<pattern>com.beust</pattern>
<shadedPattern>com.jd.live.agent.shaded.com.beust</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,119 @@
package com.jd.live.agent.bootstrap;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import com.jd.live.agent.bootstrap.option.AgentOption;
import com.jd.live.agent.bootstrap.option.OptionParser;
import com.sun.tools.attach.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.jd.live.agent.bootstrap.LivePath.LIVE_JAR;

/**
* The {@code AgentLoader} class provides functionality to attach a Java agent to a running JVM.
* It parses command-line options, identifies the target JVM, and loads the agent into it.
*/
public class AgentLoader {

private AgentLoader() {
}

public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> vmDescriptors = VirtualMachine.list();

if (vmDescriptors.isEmpty()) {
System.out.println("No Java process found!");
return;
/**
* The main method to start the agent loader. It parses the command-line arguments,
* finds the target JVM, and loads the specified agent into it.
*
* @param args the command-line arguments
* @throws IOException if an I/O error occurs
* @throws AttachNotSupportedException if the target JVM does not support attaching
* @throws AgentLoadException if the agent cannot be loaded
* @throws AgentInitializationException if the agent initialization fails
*/
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
AgentOption option = OptionParser.parse(args);
if (option != null) {
VirtualMachineDescriptor descriptor = getVmDescriptor(option);
if (descriptor != null) {
File path = getPath(option);
if (path != null) {
option.setAgentPath(path.getAbsolutePath());
VirtualMachine vm = VirtualMachine.attach(descriptor);
// Launch Agent
vm.loadAgent(new File(path, LIVE_JAR).getPath(), option.getAgentArgs());
vm.detach();
}
}
}
}

System.out.println("Select the Java process that you want to use the agent.");
for (int i = 0; i < vmDescriptors.size(); i++) {
VirtualMachineDescriptor descriptor = vmDescriptors.get(i);
System.out.println(i + ": " + descriptor.id() + " " + descriptor.displayName());
/**
* Retrieves the path to the agent directory. If the path is not provided or is invalid,
* it prompts the user to enter a valid path interactively.
*
* @param option the agent options
* @return the valid agent directory path
* @throws IOException if an I/O error occurs while reading input
*/
private static File getPath(AgentOption option) throws IOException {
String path = option.getAgentPath();
File file = path == null || path.isEmpty() ? LivePath.getRootPath(System.getenv(), null) : new File(path);
long counter = 0;
while (file == null || !option.isValidPath(file)) {
counter++;
if (option.isInteractive()) {
System.out.print(counter == 1
? "Enter agent directory (the live.jar in this directory is used as the entry by default):"
: "Enter agent directory:");
path = new BufferedReader(new InputStreamReader(System.in)).readLine();
file = path == null || path.isEmpty() ? null : new File(path.trim());
} else {
System.out.println("The agent directory is invalid. path=" + file);
return null;
}
}
return file;
}

// Read the sequence number entered by the user
BufferedReader userInputReader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Please enter the Java program number to be used by the agent:");
int selectedProcessIndex = Integer.parseInt(userInputReader.readLine());

if (selectedProcessIndex < 0 || selectedProcessIndex >= vmDescriptors.size()) {
System.out.println("Invalid program number!");
return;
/**
* Retrieves the descriptor of the target JVM. If the process ID is not provided or is invalid,
* it prompts the user to select a valid JVM process interactively.
*
* @param option the agent options
* @return the descriptor of the target JVM
* @throws IOException if an I/O error occurs while reading input
*/
private static VirtualMachineDescriptor getVmDescriptor(AgentOption option) throws IOException {
String pid = AgentOption.getPid();
Map<String, VirtualMachineDescriptor> descriptors = VirtualMachine.list().stream()
.filter(v -> !v.id().equals(pid))
.collect(Collectors.toMap(VirtualMachineDescriptor::id, v -> v));
String jvmId = option.getProcessId();
if (!descriptors.isEmpty() && option.isInteractive()) {
long counter = 0;
while (jvmId == null || jvmId.isEmpty() || !descriptors.containsKey(jvmId)) {
counter++;
if (counter == 1) {
System.out.println("Select the java process id to be attached.");
for (VirtualMachineDescriptor vm : descriptors.values()) {
System.out.println(vm.id() + " " + vm.displayName());
}
}
System.out.print("Please enter the pid:");
// Read the jvm id entered by the user
jvmId = new BufferedReader(new InputStreamReader(System.in)).readLine();
jvmId = jvmId == null || jvmId.isEmpty() ? null : jvmId.trim();
}
}

// Connect to the selected virtual machine
VirtualMachineDescriptor selectedDescriptor = vmDescriptors.get(selectedProcessIndex);
System.out.println("The process ID you selected is:" + selectedDescriptor.id());

VirtualMachine vm = VirtualMachine.attach(selectedDescriptor);

// Obtain the agent directory
System.out.print("Enter the directory where the agent is located (the live.jar in this directory is used as the entry by default):");
String agentPath = userInputReader.readLine();

// Obtain the parameters of the incoming agent
System.out.print("Please enter the parameters passed to the agent (can be empty, the default parameter is agentPath):");
String agentArgs = "agentPath=" + agentPath + "," + userInputReader.readLine();
userInputReader.close();

try {
// Launch Agent
vm.loadAgent(agentPath + "/live.jar", agentArgs);
vm.detach();
} catch (Exception e) {
e.printStackTrace();
VirtualMachineDescriptor descriptor = jvmId == null || jvmId.isEmpty()
? null : descriptors.get(jvmId);
if (descriptor == null) {
System.out.println("The java process is not found. pid=" + jvmId);
return null;
}

return descriptor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
Expand All @@ -51,26 +49,6 @@
public class LiveAgent {
private static final Logger logger = Logger.getLogger(LiveAgent.class.getName());

public static final String KEY_AGENT_PATH = "LIVE_AGENT_ROOT";

public static final String ARG_AGENT_PATH = "agentPath";

private static final String DIR_LIB = "lib";

private static final String DIR_LIB_SYSTEM = "system";

private static final String DIR_LIB_CORE = "core";

private static final String DIR_CONFIG = "config";

private static final String BOOTSTRAP_METHOD_INSTALL = "install";

private static final String BOOTSTRAP_METHOD_EXECUTE = "execute";

private static final String BOOTSTRAP_CLASS = "com.jd.live.agent.core.bootstrap.Bootstrap";

private static final String ARG_COMMAND = "command";

private static final int STATUS_INITIAL = 0;

private static final int STATUS_SYSTEM_LIB = 1;
Expand All @@ -81,9 +59,13 @@ public class LiveAgent {

private static final int STATUS_INSTALL_FAILED = 4;

private static final String JAR_FILE_PREFIX = "jar:file:";
private static final String BOOTSTRAP_CLASS = "com.jd.live.agent.core.bootstrap.Bootstrap";

private static final String BOOTSTRAP_METHOD_INSTALL = "install";

private static final String LIVE_AGENT_PATH = "LiveAgent.path";
private static final String BOOTSTRAP_METHOD_EXECUTE = "execute";

private static final String ARG_COMMAND = "command";

private static final String BOOTSTRAP_PROPERTIES = "bootstrap.properties";

Expand Down Expand Up @@ -144,12 +126,16 @@ private static synchronized void launch(String arguments, Instrumentation instru
// Parse the arguments and environment to prepare for the agent setup.
Map<String, Object> args = createArgs(arguments);
Map<String, Object> env = createEnv();
File root = getRootPath(env, args);
File libDir = new File(root, DIR_LIB);
File configDir = new File(root, DIR_CONFIG);
File root = LivePath.getRootPath(env, args);
if (root != null) {
// Update the environment with the determined agent path.
env.put(LivePath.KEY_AGENT_PATH, root.getPath());
}
File libDir = new File(root, LivePath.DIR_LIB);
File configDir = new File(root, LivePath.DIR_CONFIG);
Map<String, Object> bootstrapConfig = createBootstrapConfig(configDir);
File[] systemLibs = getLibs(new File(libDir, DIR_LIB_SYSTEM));
File[] coreLibs = getLibs(new File(libDir, DIR_LIB_CORE));
File[] systemLibs = getLibs(new File(libDir, LivePath.DIR_LIB_SYSTEM));
File[] coreLibs = getLibs(new File(libDir, LivePath.DIR_LIB_CORE));
URL[] coreLibUrls = getUrls(coreLibs);
String command = (String) args.get(ARG_COMMAND);

Expand Down Expand Up @@ -294,48 +280,6 @@ private static void addSystemPath(Instrumentation instrumentation, File[] files)
logger.addHandler(new LogHandler());
}

/**
* Determines the root path of the agent based on the provided arguments and environment.
*
* @param env A map containing the environment configuration.
* @param args A map containing the agent's arguments.
* @return A file representing the root path of the agent.
*/
private static File getRootPath(Map<String, Object> env, Map<String, Object> args) {
File result = null;
String root = (String) args.get(ARG_AGENT_PATH);
if (root == null || root.isEmpty()) {
root = (String) env.get(KEY_AGENT_PATH);
if (root == null || root.isEmpty()) {
ProtectionDomain protectionDomain = LiveAgent.class.getProtectionDomain();
CodeSource codeSource = protectionDomain == null ? null : protectionDomain.getCodeSource();
if (codeSource != null) {
String path = urlDecode(codeSource.getLocation().getPath());
result = new File(path).getParentFile();
} else {
URL url = ClassLoader.getSystemClassLoader().getResource(LIVE_AGENT_PATH);
if (url != null) {
String path = url.toString();
if (path.startsWith(JAR_FILE_PREFIX)) {
int pos = path.lastIndexOf('/');
int end = path.lastIndexOf('/', pos - 1);
result = new File(urlDecode(path.substring(JAR_FILE_PREFIX.length(), end)));
}
}
}
} else {
result = new File(root);
}
} else {
result = new File(root);
}
if (result != null) {
// Update the environment with the determined agent path.
env.put(KEY_AGENT_PATH, result.getPath());
}
return result;
}

/**
* Creates a class loader with specified URLs, configuration path, and a configuration function.
*
Expand Down Expand Up @@ -365,7 +309,7 @@ private static Map<String, Object> createArgs(String args) {
Map<String, Object> result = new HashMap<>();
if (args != null) {
// Split the input string into parts using the semicolon as a delimiter.
String[] parts = args.trim().split(";");
String[] parts = args.trim().split("[;,]");
for (String arg : parts) {
// Find the index of the equal sign to separate key and value.
int index = arg.indexOf('=');
Expand Down Expand Up @@ -478,21 +422,6 @@ private static void close(URLClassLoader classLoader) {
}
}

/**
* Decodes a URL encoded string using UTF-8 encoding.
*
* @param value The string to be decoded.
* @return The decoded string.
*/
private static String urlDecode(String value) {
try {
return java.net.URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Returns the original value if UTF-8 encoding is not supported.
return value;
}
}

private static class LogHandler extends Handler {
final com.jd.live.agent.bootstrap.logger.Logger delegate = LoggerFactory.getLogger(LogHandler.class);

Expand Down
Loading

0 comments on commit 3b4a97d

Please sign in to comment.