diff --git a/joylive-bootstrap/joylive-bootstrap-premain/pom.xml b/joylive-bootstrap/joylive-bootstrap-premain/pom.xml index 81955ba51..ec0fff5d6 100644 --- a/joylive-bootstrap/joylive-bootstrap-premain/pom.xml +++ b/joylive-bootstrap/joylive-bootstrap-premain/pom.xml @@ -13,6 +13,7 @@ live + 1.82 @@ -20,6 +21,11 @@ com.jd.live joylive-bootstrap-api + + com.beust + jcommander + ${jcommander.version} + @@ -47,6 +53,40 @@ + + org.apache.maven.plugins + maven-shade-plugin + + + + + + com.beust:jcommander + + + + + + + + + false + + + + + com.beust + com.jd.live.agent.shaded.com.beust + + + + + + diff --git a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/AgentLoader.java b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/AgentLoader.java index 4071c286b..711ee370f 100644 --- a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/AgentLoader.java +++ b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/AgentLoader.java @@ -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 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 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; } } \ No newline at end of file diff --git a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LiveAgent.java b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LiveAgent.java index 20442f025..e10085ace 100644 --- a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LiveAgent.java +++ b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LiveAgent.java @@ -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; @@ -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; @@ -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"; @@ -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 args = createArgs(arguments); Map 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 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); @@ -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 env, Map 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. * @@ -365,7 +309,7 @@ private static Map createArgs(String args) { Map 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('='); @@ -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); diff --git a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LivePath.java b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LivePath.java new file mode 100644 index 000000000..7e01c4ca1 --- /dev/null +++ b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/LivePath.java @@ -0,0 +1,100 @@ +/* + * Copyright © ${year} ${owner} (${email}) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jd.live.agent.bootstrap; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Map; + +public abstract class LivePath { + + public static final String KEY_AGENT_PATH = "LIVE_AGENT_ROOT"; + + public static final String ARG_AGENT_PATH = "agentPath"; + + public static final String DIR_LIB = "lib"; + + public static final String DIR_LIB_SYSTEM = "system"; + + public static final String DIR_LIB_CORE = "core"; + + public static final String DIR_CONFIG = "config"; + + public static final String DIR_PLUGIN = "plugin"; + + public static final String JAR_FILE_PREFIX = "jar:file:"; + + public static final String LIVE_AGENT_PATH = "LiveAgent.path"; + + public static final String LIVE_JAR = "live.jar"; + + /** + * 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. + */ + public static File getRootPath(Map env, Map args) { + File result = null; + String root = args == null ? null : (String) args.get(LivePath.ARG_AGENT_PATH); + if (root == null || root.isEmpty()) { + root = (String) env.get(LivePath.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(LivePath.LIVE_AGENT_PATH); + if (url != null) { + String path = url.toString(); + if (path.startsWith(LivePath.JAR_FILE_PREFIX)) { + int pos = path.lastIndexOf('/'); + int end = path.lastIndexOf('/', pos - 1); + result = new File(urlDecode(path.substring(LivePath.JAR_FILE_PREFIX.length(), end))); + } + } + } + } else { + result = new File(root); + } + } else { + result = new File(root); + } + return result; + } + + /** + * 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; + } + } + +} diff --git a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/AgentOption.java b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/AgentOption.java new file mode 100644 index 000000000..a1357bd59 --- /dev/null +++ b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/AgentOption.java @@ -0,0 +1,136 @@ +/* + * Copyright © ${year} ${owner} (${email}) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jd.live.agent.bootstrap.option; + +import com.beust.jcommander.DynamicParameter; +import com.beust.jcommander.Parameter; +import com.jd.live.agent.bootstrap.LivePath; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.Map; + +/** + * The {@code AgentOption} class represents the command-line options for loading a Java agent. + * It includes options for specifying the target JVM process ID, the agent path, agent arguments, and other settings. + */ +public class AgentOption { + + @Parameter(names = {"-p", "-pid"}, description = "The target JVM process ID") + private String processId; + + @Parameter(names = {"-t", "-path"}, description = "The agent root path") + private String agentPath; + + @DynamicParameter(names = {"-a", "-arg"}, description = "The agent argument") + private Map args; + + @Parameter(names = {"-h", "-help"}, help = true, description = "The help information") + private boolean help; + + @Parameter(names = {"-i", "-interactive"}, description = "The interactive mode") + private boolean interactive = true; + + public String getProcessId() { + return processId; + } + + public void setProcessId(String processId) { + this.processId = processId; + } + + public String getAgentPath() { + return agentPath; + } + + public void setAgentPath(String agentPath) { + this.agentPath = agentPath; + } + + public Map getArgs() { + return args; + } + + public void setArgs(Map args) { + this.args = args; + } + + public void addArg(String key, String value) { + if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) { + if (args == null) { + args = new HashMap<>(); + } + args.put(key, value); + } + } + + public boolean isHelp() { + return help; + } + + public void setHelp(boolean help) { + this.help = help; + } + + public boolean isInteractive() { + return interactive; + } + + public void setInteractive(boolean interactive) { + this.interactive = interactive; + } + + /** + * Constructs the agent arguments as a single string. + * + * @return the agent arguments string + */ + public String getAgentArgs() { + StringBuilder sb = new StringBuilder(); + sb.append(LivePath.ARG_AGENT_PATH).append("=").append(agentPath); + if (args != null && !args.isEmpty()) { + args.forEach((k, v) -> sb.append(k).append("=").append(v).append(",")); + } + return sb.toString(); + } + + /** + * Validates the agent path by checking if the necessary files and directories exist. + * + * @param root the root directory to validate + * @return {@code true} if the path is valid, {@code false} otherwise + */ + public boolean isValidPath(File root) { + if (root == null || !root.exists() || !root.isDirectory()) { + return false; + } + File file = new File(root, LivePath.LIVE_JAR); + File libDir = new File(root, LivePath.DIR_LIB); + File configDir = new File(root, LivePath.DIR_CONFIG); + return file.exists() && libDir.exists() && configDir.exists() && libDir.isDirectory() && configDir.isDirectory(); + } + + /** + * Gets the current JVM process ID. + * + * @return the current process ID + */ + public static String getPid() { + String name = ManagementFactory.getRuntimeMXBean().getName(); + return name.split("@")[0]; + } +} \ No newline at end of file diff --git a/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/OptionParser.java b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/OptionParser.java new file mode 100644 index 000000000..0bf429175 --- /dev/null +++ b/joylive-bootstrap/joylive-bootstrap-premain/src/main/java/com/jd/live/agent/bootstrap/option/OptionParser.java @@ -0,0 +1,45 @@ +/* + * Copyright © ${year} ${owner} (${email}) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jd.live.agent.bootstrap.option; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.UnixStyleUsageFormatter; + +/** + * The {@code OptionParser} class provides a method to parse command-line arguments + * into an {@link AgentOption} object using the JCommander library. + */ +public class OptionParser { + + /** + * Parses the given command-line arguments into an {@link AgentOption} object. + * If the help option is specified, it prints the usage information and returns {@code null}. + * + * @param args the command-line arguments to parse + * @return the parsed {@link AgentOption} object, or {@code null} if help is requested + */ + public static AgentOption parse(String[] args) { + AgentOption option = new AgentOption(); + JCommander commander = JCommander.newBuilder().addObject(option).build(); + commander.setUsageFormatter(new UnixStyleUsageFormatter(commander)); + commander.parse(args); + if (option.isHelp()) { + commander.usage(); + return null; + } + return option; + } +} \ No newline at end of file