| /* | |
| * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| * | |
| */ | |
| package sun.jvm.hotspot; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.function.Consumer; | |
| import sun.jvm.hotspot.debugger.DebuggerException; | |
| import sun.jvm.hotspot.tools.JStack; | |
| import sun.jvm.hotspot.tools.JMap; | |
| import sun.jvm.hotspot.tools.JInfo; | |
| import sun.jvm.hotspot.tools.JSnap; | |
| public class SALauncher { | |
| private static boolean launcherHelp() { | |
| System.out.println(" clhsdb \tcommand line debugger"); | |
| System.out.println(" hsdb \tui debugger"); | |
| System.out.println(" debugd --help\tto get more information (deprecated)"); | |
| System.out.println(" jstack --help\tto get more information"); | |
| System.out.println(" jmap --help\tto get more information"); | |
| System.out.println(" jinfo --help\tto get more information"); | |
| System.out.println(" jsnap --help\tto get more information"); | |
| return false; | |
| } | |
| private static boolean commonHelp(String mode) { | |
| return commonHelp(mode, false); | |
| } | |
| private static boolean commonHelpWithConnect(String mode) { | |
| return commonHelp(mode, true); | |
| } | |
| private static boolean commonHelp(String mode, boolean canConnectToRemote) { | |
| // --pid <pid> | |
| // --exe <exe> | |
| // --core <core> | |
| // --connect [<id>@]<host>[:registryport] | |
| System.out.println(" --pid <pid> To attach to and operate on the given live process."); | |
| System.out.println(" --core <corefile> To operate on the given core file."); | |
| System.out.println(" --exe <executable for corefile>"); | |
| if (canConnectToRemote) { | |
| System.out.println(" --connect [<serverid>@]<host>[:registryport][/servername] To connect to a remote debug server (debugd)."); | |
| } | |
| System.out.println(); | |
| if (canConnectToRemote) { | |
| System.out.println(" The --connect option is deprecated and will be removed in a future release."); | |
| } | |
| System.out.println(" The --core and --exe options must be set together to give the core"); | |
| System.out.println(" file, and associated executable, to operate on. They can use"); | |
| System.out.println(" absolute or relative paths."); | |
| System.out.println(" The --pid option can be set to operate on a live process."); | |
| if (canConnectToRemote) { | |
| System.out.println(" --core, --pid, and --connect are mutually exclusive."); | |
| } else { | |
| System.out.println(" --core and --pid are mutually exclusive."); | |
| } | |
| System.out.println(); | |
| System.out.println(" Examples: jhsdb " + mode + " --pid 1234"); | |
| System.out.println(" or jhsdb " + mode + " --core ./core.1234 --exe ./myexe"); | |
| if (canConnectToRemote) { | |
| System.out.println(" or jhsdb " + mode + " --connect serverid@debugserver:1234/servername"); | |
| } | |
| return false; | |
| } | |
| private static boolean debugdHelp() { | |
| System.out.println("WARNING: debugd is deprecated and will be removed in a future release."); | |
| System.out.println(" --serverid <id> A unique identifier for this debugd server."); | |
| System.out.println(" --servername <name> Instance name of debugd server."); | |
| System.out.println(" --rmiport <port> Sets the port number to which the RMI connector is bound." + | |
| " If not specified a random available port is used."); | |
| System.out.println(" --registryport <port> Sets the RMI registry port." + | |
| " This option overrides the system property 'sun.jvm.hotspot.rmi.port'. If not specified," + | |
| " the system property is used. If the system property is not set, the default port 1099 is used."); | |
| System.out.println(" --disable-registry Do not start RMI registry (use already started RMI registry)"); | |
| System.out.println(" --hostname <hostname> Sets the hostname the RMI connector is bound. The value could" + | |
| " be a hostname or an IPv4/IPv6 address. This option overrides the system property" + | |
| " 'java.rmi.server.hostname'. If not specified, the system property is used. If the system" + | |
| " property is not set, a system hostname is used."); | |
| return commonHelp("debugd"); | |
| } | |
| private static boolean jinfoHelp() { | |
| // --flags -> -flags | |
| // --sysprops -> -sysprops | |
| System.out.println(" --flags To print VM flags."); | |
| System.out.println(" --sysprops To print Java System properties."); | |
| System.out.println(" <no option> To print both of the above."); | |
| return commonHelpWithConnect("jinfo"); | |
| } | |
| private static boolean jmapHelp() { | |
| // --heap -> -heap | |
| // --binaryheap -> -heap:format=b | |
| // --histo -> -histo | |
| // --clstats -> -clstats | |
| // --finalizerinfo -> -finalizerinfo | |
| System.out.println(" <no option> To print same info as Solaris pmap."); | |
| System.out.println(" --heap To print java heap summary."); | |
| System.out.println(" --binaryheap To dump java heap in hprof binary format."); | |
| System.out.println(" --dumpfile <name> The name of the dump file. Only valid with --binaryheap."); | |
| System.out.println(" --gz <1-9> The compression level for gzipped dump file. Only valid with --binaryheap."); | |
| System.out.println(" --histo To print histogram of java object heap."); | |
| System.out.println(" --clstats To print class loader statistics."); | |
| System.out.println(" --finalizerinfo To print information on objects awaiting finalization."); | |
| return commonHelpWithConnect("jmap"); | |
| } | |
| private static boolean jstackHelp() { | |
| // --locks -> -l | |
| // --mixed -> -m | |
| System.out.println(" --locks To print java.util.concurrent locks."); | |
| System.out.println(" --mixed To print both Java and native frames (mixed mode)."); | |
| return commonHelpWithConnect("jstack"); | |
| } | |
| private static boolean jsnapHelp() { | |
| System.out.println(" --all To print all performance counters."); | |
| return commonHelpWithConnect("jsnap"); | |
| } | |
| private static boolean toolHelp(String toolName) { | |
| switch (toolName) { | |
| case "jstack": | |
| return jstackHelp(); | |
| case "jinfo": | |
| return jinfoHelp(); | |
| case "jmap": | |
| return jmapHelp(); | |
| case "jsnap": | |
| return jsnapHelp(); | |
| case "debugd": | |
| return debugdHelp(); | |
| case "hsdb": | |
| case "clhsdb": | |
| return commonHelpWithConnect(toolName); | |
| default: | |
| return launcherHelp(); | |
| } | |
| } | |
| private static final String NO_REMOTE = null; | |
| private static String[] buildAttachArgs(Map<String, String> newArgMap, | |
| boolean allowEmpty) { | |
| String pid = newArgMap.remove("pid"); | |
| String exe = newArgMap.remove("exe"); | |
| String core = newArgMap.remove("core"); | |
| String connect = newArgMap.remove("connect"); | |
| if (!allowEmpty && (pid == null) && (exe == null) && (connect == NO_REMOTE)) { | |
| throw new SAGetoptException("You have to set --pid or --exe or --connect."); | |
| } | |
| List<String> newArgs = new ArrayList<>(); | |
| for (var entry : newArgMap.entrySet()) { | |
| newArgs.add(entry.getKey()); | |
| if (entry.getValue() != null) { | |
| newArgs.add(entry.getValue()); | |
| } | |
| } | |
| if (pid != null) { // Attach to live process | |
| if (exe != null) { | |
| throw new SAGetoptException("Unnecessary argument: --exe"); | |
| } else if (core != null) { | |
| throw new SAGetoptException("Unnecessary argument: --core"); | |
| } else if (connect != NO_REMOTE) { | |
| throw new SAGetoptException("Unnecessary argument: --connect"); | |
| } else if (!pid.matches("^\\d+$")) { | |
| throw new SAGetoptException("Invalid pid: " + pid); | |
| } | |
| newArgs.add(pid); | |
| } else if (exe != null) { | |
| if (connect != NO_REMOTE) { | |
| throw new SAGetoptException("Unnecessary argument: --connect"); | |
| } else if (exe.length() == 0) { | |
| throw new SAGetoptException("You have to set --exe."); | |
| } | |
| newArgs.add(exe); | |
| if ((core == null) || (core.length() == 0)) { | |
| throw new SAGetoptException("You have to set --core."); | |
| } | |
| newArgs.add(core); | |
| } else if (connect != NO_REMOTE) { | |
| System.err.println("WARNING: --connect is deprecated and will be removed in a future release."); | |
| newArgs.add(connect); | |
| } | |
| return newArgs.toArray(new String[0]); | |
| } | |
| /** | |
| * This method converts jhsdb-style options (oldArgs) to old fashioned | |
| * style. SALauncher delegates the work to the entry point of each tool. | |
| * Thus we need to convert the arguments. | |
| * For example, `jhsdb jstack --mixed` needs to be converted to `jstack -m`. | |
| * | |
| * longOptsMap holds the rule how this method should convert the args. | |
| * The key is the name of jhsdb style, the value is the name of | |
| * old fashioned style. If you want to convert mixed option in jstack, | |
| * you need to set "mixed" to the key, and to set "-m" to the value | |
| * in longOptsMap. If the option have the value, you need to add "=" to | |
| * the key like "exe=". | |
| * | |
| * You also can set the options which cannot be mapped to old fashioned | |
| * arguments. For example, `jhsdb jmap --binaryheap` cannot be mapped to | |
| * `jmap` option directly. But you set it to longOptsMap, then you can know | |
| * the user sets "binaryheap" option, and SALauncher should set | |
| * "-heap:format:b" to jmap option. | |
| * | |
| * This method returns the map of the old fashioned key/val pairs. | |
| * It can be used to build args in string array at buildAttachArgs(). | |
| */ | |
| private static Map<String, String> parseOptions(String[] oldArgs, | |
| Map<String, String> longOptsMap) { | |
| SAGetopt sg = new SAGetopt(oldArgs); | |
| String[] longOpts = longOptsMap.keySet().toArray(new String[0]); | |
| Map<String, String> newArgMap = new HashMap<>(); | |
| /* | |
| * Parse each jhsdb-style option via SAGetopt. | |
| * SAGetopt parses and validates the argument. If the user passes invalid | |
| * option, SAGetoptException will be occurred at SAGetopt::next. | |
| * Thus there is no need to validate it here. | |
| * | |
| * We can get option value via SAGetopt::get. If jhsdb-style option has | |
| * '=' at the tail, we put old fashioned option with it to newArgMap. | |
| */ | |
| String s; | |
| while ((s = sg.next(null, longOpts)) != null) { | |
| var val = longOptsMap.get(s); | |
| if (val != null) { | |
| newArgMap.put(val, null); | |
| } else { | |
| val = longOptsMap.get(s + "="); | |
| if (val != null) { | |
| newArgMap.put(val, sg.getOptarg()); | |
| } | |
| } | |
| } | |
| return newArgMap; | |
| } | |
| private static void runCLHSDB(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "connect=", "connect"); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| CLHSDB.main(buildAttachArgs(newArgMap, true)); | |
| } | |
| private static void runHSDB(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "connect=", "connect"); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| HSDB.main(buildAttachArgs(newArgMap, true)); | |
| } | |
| private static void runJSTACK(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "connect=", "connect", | |
| "mixed", "-m", | |
| "locks", "-l"); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| JStack jstack = new JStack(false, false); | |
| jstack.runWithArgs(buildAttachArgs(newArgMap, false)); | |
| } | |
| private static void runJMAP(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.ofEntries( | |
| Map.entry("exe=", "exe"), | |
| Map.entry("core=", "core"), | |
| Map.entry("pid=", "pid"), | |
| Map.entry("connect=", "connect"), | |
| Map.entry("heap", "-heap"), | |
| Map.entry("binaryheap", "binaryheap"), | |
| Map.entry("dumpfile=", "dumpfile"), | |
| Map.entry("gz=", "gz"), | |
| Map.entry("histo", "-histo"), | |
| Map.entry("clstats", "-clstats"), | |
| Map.entry("finalizerinfo", "-finalizerinfo")); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| boolean requestHeapdump = newArgMap.containsKey("binaryheap"); | |
| String dumpfile = newArgMap.get("dumpfile"); | |
| String gzLevel = newArgMap.get("gz"); | |
| String command = "-heap:format=b"; | |
| if (!requestHeapdump && (dumpfile != null)) { | |
| throw new IllegalArgumentException("Unexpected argument: dumpfile"); | |
| } | |
| if (requestHeapdump) { | |
| if (gzLevel != null) { | |
| command += ",gz=" + gzLevel; | |
| } | |
| if (dumpfile != null) { | |
| command += ",file=" + dumpfile; | |
| } | |
| newArgMap.put(command, null); | |
| } | |
| newArgMap.remove("binaryheap"); | |
| newArgMap.remove("dumpfile"); | |
| newArgMap.remove("gz"); | |
| JMap.main(buildAttachArgs(newArgMap, false)); | |
| } | |
| private static void runJINFO(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "connect=", "connect", | |
| "flags", "-flags", | |
| "sysprops", "-sysprops"); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| JInfo.main(buildAttachArgs(newArgMap, false)); | |
| } | |
| private static void runJSNAP(String[] oldArgs) { | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "connect=", "connect", | |
| "all", "-a"); | |
| Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); | |
| JSnap.main(buildAttachArgs(newArgMap, false)); | |
| } | |
| @SuppressWarnings("removal") | |
| private static void runDEBUGD(String[] args) { | |
| System.err.println("WARNING: debugd is deprecated and will be removed in a future release."); | |
| // By default SA agent classes prefer Windows process debugger | |
| // to windbg debugger. SA expects special properties to be set | |
| // to choose other debuggers. We will set those here before | |
| // attaching to SA agent. | |
| System.setProperty("sun.jvm.hotspot.debugger.useWindbgDebugger", "true"); | |
| Map<String, String> longOptsMap = Map.of("exe=", "exe", | |
| "core=", "core", | |
| "pid=", "pid", | |
| "serverid=", "serverid", | |
| "rmiport=", "rmiport", | |
| "registryport=", "registryport", | |
| "disable-registry", "disable-registry", | |
| "hostname=", "hostname", | |
| "servername=", "servername"); | |
| Map<String, String> argMap = parseOptions(args, longOptsMap); | |
| // Run the basic check for the options. If the check fails | |
| // SAGetoptException will be thrown | |
| buildAttachArgs(new HashMap<>(argMap), false); | |
| String serverID = argMap.get("serverid"); | |
| String rmiPortString = argMap.get("rmiport"); | |
| String registryPort = argMap.get("registryport"); | |
| String host = argMap.get("hostname"); | |
| String javaExecutableName = argMap.get("exe"); | |
| String coreFileName = argMap.get("core"); | |
| String pidString = argMap.get("pid"); | |
| String serverName = argMap.get("servername"); | |
| // Set RMI registry port, if specified | |
| if (registryPort != null) { | |
| try { | |
| Integer.parseInt(registryPort); | |
| } catch (NumberFormatException ex) { | |
| throw new SAGetoptException("Invalid registry port: " + registryPort); | |
| } | |
| System.setProperty("sun.jvm.hotspot.rmi.port", registryPort); | |
| } | |
| // Disable RMI registry if specified | |
| if (argMap.containsKey("disable-registry")) { | |
| System.setProperty("sun.jvm.hotspot.rmi.startRegistry", "false"); | |
| } | |
| // Set RMI connector hostname, if specified | |
| if (host != null && !host.trim().isEmpty()) { | |
| System.setProperty("java.rmi.server.hostname", host); | |
| } | |
| // Set RMI connector port, if specified | |
| int rmiPort = 0; | |
| if (rmiPortString != null) { | |
| try { | |
| rmiPort = Integer.parseInt(rmiPortString); | |
| } catch (NumberFormatException ex) { | |
| throw new SAGetoptException("Invalid RMI connector port: " + rmiPortString); | |
| } | |
| } | |
| final HotSpotAgent agent = new HotSpotAgent(); | |
| if (pidString != null) { | |
| int pid = 0; | |
| try { | |
| pid = Integer.parseInt(pidString); | |
| } catch (NumberFormatException ex) { | |
| throw new SAGetoptException("Invalid pid: " + pidString); | |
| } | |
| System.err.println("Attaching to process ID " + pid + " and starting RMI services," + | |
| " please wait..."); | |
| try { | |
| agent.startServer(pid, serverID, serverName, rmiPort); | |
| } catch (DebuggerException e) { | |
| System.err.print("Error attaching to process or starting server: "); | |
| e.printStackTrace(); | |
| System.exit(1); | |
| } catch (NumberFormatException ex) { | |
| throw new SAGetoptException("Invalid pid: " + pid); | |
| } | |
| } else if (javaExecutableName != null) { | |
| System.err.println("Attaching to core " + coreFileName + | |
| " from executable " + javaExecutableName + " and starting RMI services, please wait..."); | |
| try { | |
| agent.startServer(javaExecutableName, coreFileName, serverID, serverName, rmiPort); | |
| } catch (DebuggerException e) { | |
| System.err.print("Error attaching to core file or starting server: "); | |
| e.printStackTrace(); | |
| System.exit(1); | |
| } | |
| } | |
| // shutdown hook to clean-up the server in case of forced exit. | |
| Runtime.getRuntime().addShutdownHook(new java.lang.Thread(agent::shutdownServer)); | |
| System.err.println("Debugger attached and RMI services started." + ((rmiPortString != null) ? | |
| (" RMI connector is bound to port " + rmiPort + ".") : "")); | |
| } | |
| // Key: tool name, Value: launcher method | |
| private static Map<String, Consumer<String[]>> toolMap = | |
| Map.of("clhsdb", SALauncher::runCLHSDB, | |
| "hsdb", SALauncher::runHSDB, | |
| "jstack", SALauncher::runJSTACK, | |
| "jmap", SALauncher::runJMAP, | |
| "jinfo", SALauncher::runJINFO, | |
| "jsnap", SALauncher::runJSNAP, | |
| "debugd", SALauncher::runDEBUGD); | |
| public static void main(String[] args) { | |
| // Provide a help | |
| if (args.length == 0) { | |
| launcherHelp(); | |
| return; | |
| } | |
| // No arguments imply help for debugd, jstack, jmap, jinfo but launch clhsdb and hsdb | |
| if (args.length == 1 && !args[0].equals("clhsdb") && !args[0].equals("hsdb")) { | |
| toolHelp(args[0]); | |
| return; | |
| } | |
| for (String arg : args) { | |
| if (arg.equals("-h") || arg.equals("-help") || arg.equals("--help")) { | |
| toolHelp(args[0]); | |
| return; | |
| } | |
| } | |
| String[] oldArgs = Arrays.copyOfRange(args, 1, args.length); | |
| try { | |
| var func = toolMap.get(args[0]); | |
| if (func == null) { | |
| throw new SAGetoptException("Unknown tool: " + args[0]); | |
| } else { | |
| func.accept(oldArgs); | |
| } | |
| } catch (SAGetoptException e) { | |
| System.err.println("SA agent option related exception occurred: " + e.getMessage()); | |
| e.printStackTrace(); | |
| toolHelp(args[0]); | |
| // Exit with error status | |
| System.exit(1); | |
| } | |
| } | |
| } |