/*
 * Copyright (c) 2015, 2019, 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.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");
        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>
        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 [<id>@]<host> To connect to a remote debug server (debugd).");
        }
        System.out.println();
        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("    The --connect option can be set to connect to a debug server (debugd).");
            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 debugserver");
            System.out.println("          or  jhsdb " + mode + " --connect id@debugserver");
        }
        return false;
    }

    private static boolean debugdHelp() {
        // [options] <pid> [server-id]
        // [options] <executable> <core> [server-id]
        System.out.println("    --serverid <id>         A unique identifier for this debug server.");
        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.");
        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 commonHelp(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) {
            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().
/** * 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"); 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"); 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.of("exe=", "exe", "core=", "core", "pid=", "pid", "connect=", "connect", "heap", "-heap", "binaryheap", "binaryheap", "dumpfile=", "dumpfile", "histo", "-histo", "clstats", "-clstats", "finalizerinfo", "-finalizerinfo"); Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); boolean requestHeapdump = newArgMap.containsKey("binaryheap"); String dumpfile = newArgMap.get("dumpfile"); if (!requestHeapdump && (dumpfile != null)) { throw new IllegalArgumentException("Unexpected argument: dumpfile"); } if (requestHeapdump) { if (dumpfile == null) { newArgMap.put("-heap:format=b", null); } else { newArgMap.put("-heap:format=b,file=" + dumpfile, null); } } newArgMap.remove("binaryheap"); newArgMap.remove("dumpfile"); 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)); } private static void runDEBUGD(String[] oldArgs) { // 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"); Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap); var serverid = newArgMap.remove("serverid"); List<String> newArgArray = new ArrayList<>(); newArgArray.addAll(Arrays.asList(buildAttachArgs(newArgMap, false))); // `serverid` must be located at the tail. if (serverid != null) { newArgArray.add(serverid); } // delegate to the actual SA debug server. DebugServer.main(newArgArray.toArray(new String[0])); } // 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(e.getMessage()); toolHelp(args[0]); } } }