《arthas源码分析》arthas启动流程分析(一)

arthas启动

curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

选择应用的java进程:

$ java -jar arthas-boot.jar
[1]: 35542
[2]: 71560 arthas-demo.jar

Bootstrap

执行 java -jar arthas-boot.jar之后,便来到了Bootstrap#main方法中。

静态代码块
static {
    //创建 arthas lib 目录 :C:\Users\用户\.arthas\lib
    ARTHAS_LIB_DIR = new File(
            System.getProperty("user.home") + File.separator + ".arthas" + File.separator + "lib");
    try {
        ARTHAS_LIB_DIR.mkdirs();
    } catch (Throwable t) {
        //ignore
    }
    /**
     * 如果lib目录不存在,则在操作系统的缓存的临时目录,创建lib目录
     * 其中:
     * Windows的缓存目录为:C:\Users\用户\AppData\Local\Temp\
     */
    if (!ARTHAS_LIB_DIR.exists()) {
        // try to set a temp directory
        ARTHAS_LIB_DIR = new File(System.getProperty("java.io.tmpdir") + File.separator + ".arthas" + File.separator + "lib");
        try {
            ARTHAS_LIB_DIR.mkdirs();
        } catch (Throwable e) {
            // ignore
        }
    }
    //没有创建成功lib目录,则抛出异常
    if (!ARTHAS_LIB_DIR.exists()) {
        System.err.println("Can not find directory to save arthas lib. please try to set user home by -Duser.home=");
    }
}
Bootstrap#main
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException,
                ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException,
                IllegalArgumentException, InvocationTargetException {
    //当前类的Package
    Package bootstrapPackage = Bootstrap.class.getPackage();
    if (bootstrapPackage != null) {
        //arthas版本
        String arthasBootVersion = bootstrapPackage.getImplementationVersion();
        if (arthasBootVersion != null) {
            AnsiLog.info("arthas-boot version: " + arthasBootVersion);
        }
    }

    String mavenMetaData = null;

    //创建启动类
    Bootstrap bootstrap = new Bootstrap();

    //提取Bootstrap中的元信息
    CLI cli = CLIConfigurator.define(Bootstrap.class);
    //解析java -jar arthas-boot.jar 后面跟的参数
    CommandLine commandLine = cli.parse(Arrays.asList(args));

    //将命令行参数注入到Bootstrap中
    try {
        CLIConfigurator.inject(commandLine, bootstrap);
    } catch (Throwable e) {
        e.printStackTrace();
        System.out.println(usage(cli));
        System.exit(1);
    }

    //存在-v选项,设置打印所有日志内容
    if (bootstrap.isVerbose()) {
        AnsiLog.level(Level.ALL);
    }
    //存在-p选项,打印帮助信息,并退出
    if (bootstrap.isHelp()) {
        System.out.println(usage(cli));
        //正常退出
        System.exit(0);
    }

    /**
     * 设置仓库镜像,
     * 如果是北京时间,默认使用aliyun的仓库镜像,否则使用maven的中心仓库
     */
    if (bootstrap.getRepoMirror() == null || bootstrap.getRepoMirror().trim().isEmpty()) {
        bootstrap.setRepoMirror("center");
        // if timezone is +0800, default repo mirror is aliyun
        if (TimeUnit.MILLISECONDS.toHours(TimeZone.getDefault().getOffset(System.currentTimeMillis())) == 8) {
            bootstrap.setRepoMirror("aliyun");
        }
    }
    AnsiLog.debug("Repo mirror:" + bootstrap.getRepoMirror());

    //存在-version选项,列出版本信息,并退出。
    if (bootstrap.isVersions()) {
        if (mavenMetaData == null) {
            mavenMetaData = DownloadUtils.readMavenMetaData(bootstrap.getRepoMirror(), bootstrap.isuseHttp());
        }
        System.out.println(UsageRender.render(listVersions(mavenMetaData)));
        System.exit(0);
    }

    //jdk6 or jdk7使用http
    if (JavaVersionUtils.isJava6() || JavaVersionUtils.isJava7()) {
        bootstrap.setuseHttp(true);
        AnsiLog.debug("Java version is {}, only support http, set useHttp to true.",
                        JavaVersionUtils.javaVersionStr());
    }

    //判断端口号是否被占用
    // check telnet/http port
    int telnetPortPid = -1;
    int httpPortPid = -1;
    if (bootstrap.getTelnetPort() > 0) {
        //查找telnet端口号(默认3658)对应的进程id
        telnetPortPid = SocketUtils.findTcpListenProcess(bootstrap.getTelnetPort());
        if (telnetPortPid > 0) {
            //打印端口被占用日志
            AnsiLog.info("Process {} already using port {}", telnetPortPid, bootstrap.getTelnetPort());
        }
    }
    if (bootstrap.getHttpPort() > 0) {
        //查找http端口号(默认8563)对应的的进程id
        httpPortPid = SocketUtils.findTcpListenProcess(bootstrap.getHttpPort());
        if (httpPortPid > 0) {
            //打印端口被占用日志
            AnsiLog.info("Process {} already using port {}", httpPortPid, bootstrap.getHttpPort());
        }
    }

    //选择要attach的进程号
    int pid = bootstrap.getPid();
    // select pid
    if (pid < 0) {
        try {
            //列出所有java进程,并选择要attach的进程号
            pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid);
        } catch (InputMismatchException e) {
            System.out.println("Please input an integer to select pid.");
            System.exit(1);
        }
        if (pid < 0) {
            System.out.println("Please select an available pid.");
            System.exit(1);
        }
    }

    if (telnetPortPid > 0 && pid != telnetPortPid) {
        AnsiLog.error("Target process {} is not the process using port {}, you will connect to an unexpected process.",
                        pid, bootstrap.getTelnetPort());
        AnsiLog.error("1. Try to restart arthas-boot, select process {}, shutdown it first with running the 'stop' command.",
                        telnetPortPid);
        AnsiLog.error("2. Or try to use different telnet port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port -1");
        System.exit(1);
    }

    if (httpPortPid > 0 && pid != httpPortPid) {
        AnsiLog.error("Target process {} is not the process using port {}, you will connect to an unexpected process.",
                        pid, bootstrap.getHttpPort());
        AnsiLog.error("1. Try to restart arthas-boot, select process {}, shutdown it first with running the 'stop' command.",
                        httpPortPid);
        AnsiLog.error("2. Or try to use different http port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port 9999", httpPortPid);
        System.exit(1);
    }

    //创建arthas home.
    //默认的home:C:/Users/user/.arthas/lib/version/arthas
    File arthasHomeDir = null;
    //arthas home存在,则直接使用
    if (bootstrap.getArthasHome() != null) {
        verifyArthasHome(bootstrap.getArthasHome());
        arthasHomeDir = new File(bootstrap.getArthasHome());
    }
    //arthas home不存在
    if (arthasHomeDir == null && bootstrap.getUseVersion() != null) {
        // try to find from ~/.arthas/lib
        File specialVersionDir = new File(System.getProperty("user.home"), ".arthas" + File.separator + "lib"
                        + File.separator + bootstrap.getUseVersion() + File.separator + "arthas");
        //
        if (!specialVersionDir.exists()) {
            //从远程下载arthas
            DownloadUtils.downArthasPackaging(bootstrap.getRepoMirror(), bootstrap.isuseHttp(),
                            bootstrap.getUseVersion(), ARTHAS_LIB_DIR.getAbsolutePath());
        }
        verifyArthasHome(specialVersionDir.getAbsolutePath());
        arthasHomeDir = specialVersionDir;
    }


    // Try set the directory where arthas-boot.jar is located to arhtas home
    // 尝试将arthas-boot.jar所在的目录设置为arhtas home。
    if (arthasHomeDir == null) {
        CodeSource codeSource = Bootstrap.class.getProtectionDomain().getCodeSource();
        if (codeSource != null) {
            try {
                // https://stackoverflow.com/a/17870390
                File bootJarPath = new File(codeSource.getLocation().toURI().getSchemeSpecificPart());
                verifyArthasHome(bootJarPath.getParent());
                arthasHomeDir = bootJarPath.getParentFile();
            } catch (Throwable e) {
                // ignore
            }
        }
    }
    
    //尝试从远程下载arthas
    if (arthasHomeDir == null) {
        //校验lib目录是否存在
        boolean checkFile =  ARTHAS_LIB_DIR.exists() || ARTHAS_LIB_DIR.mkdirs();
        if(!checkFile){
            AnsiLog.error("cannot create directory {}: maybe permission denied", ARTHAS_LIB_DIR.getAbsolutePath());
            System.exit(1);
        }

        /**
         * <pre>
         * 1. get local latest version
         * 2. get remote latest version
         * 3. compare two version
         * </pre>
         */
        List<String> versionList = listNames(ARTHAS_LIB_DIR);
        Collections.sort(versionList);

        String localLastestVersion = null;
        if (!versionList.isEmpty()) {
            localLastestVersion = versionList.get(versionList.size() - 1);
        }

        //读取maven的元数据(maven-metadata.xml文件)
        if (mavenMetaData == null) {
            mavenMetaData = DownloadUtils.readMavenMetaData(bootstrap.getRepoMirror(), bootstrap.isuseHttp());
        }

        //获取发布的最新版本
        String remoteLastestVersion = DownloadUtils.readMavenReleaseVersion(mavenMetaData);

        //是否需要下载
        boolean needDownload = false;
        if (localLastestVersion == null) {
            if (remoteLastestVersion == null) {
                // exit
                AnsiLog.error("Can not find Arthas under local: {} and remote maven repo mirror: {}", ARTHAS_LIB_DIR,
                        bootstrap.getRepoMirror());
                AnsiLog.error(
                        "Unable to download arthas from remote server, please download the full package according to wiki: https://github.com/alibaba/arthas");
                System.exit(1);
            } else {
                needDownload = true;
            }
        } else {
            if (remoteLastestVersion != null) {
                if (localLastestVersion.compareTo(remoteLastestVersion) < 0) {
                    AnsiLog.info("local lastest version: {}, remote lastest version: {}, try to download from remote.",
                                    localLastestVersion, remoteLastestVersion);
                    needDownload = true;
                }
            }
        }
        //需要下载
        if (needDownload) {
            //尝试从远程服务器下载阿尔萨斯。
            DownloadUtils.downArthasPackaging(bootstrap.getRepoMirror(), bootstrap.isuseHttp(),
                            remoteLastestVersion, ARTHAS_LIB_DIR.getAbsolutePath());
            localLastestVersion = remoteLastestVersion;
        }

        // get the latest version
        arthasHomeDir = new File(ARTHAS_LIB_DIR, localLastestVersion + File.separator + "arthas");
    }

    verifyArthasHome(arthasHomeDir.getAbsolutePath());

    AnsiLog.info("arthas home: " + arthasHomeDir);


    //启动arthas
    if (telnetPortPid > 0 && pid == telnetPortPid) {
        //arthas已经启动。
        AnsiLog.info("The target process already listen port {}, skip attach.", bootstrap.getTelnetPort());
    } else {
        //arthas-core还没有启动,则启动arthas-core.jar
        List<String> attachArgs = new ArrayList<String>();
        attachArgs.add("-jar");
        attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
        attachArgs.add("-pid");
        attachArgs.add("" + pid);
        attachArgs.add("-target-ip");
        attachArgs.add(bootstrap.getTargetIp());
        attachArgs.add("-telnet-port");
        attachArgs.add("" + bootstrap.getTelnetPort());
        attachArgs.add("-http-port");
        attachArgs.add("" + bootstrap.getHttpPort());
        attachArgs.add("-core");
        attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
        attachArgs.add("-agent");
        attachArgs.add(new File(arthasHomeDir, "arthas-agent.jar").getAbsolutePath());
        if (bootstrap.getSessionTimeout() != null) {
            attachArgs.add("-session-timeout");
            attachArgs.add("" + bootstrap.getSessionTimeout());
        }

        if (bootstrap.getTunnelServer() != null) {
            attachArgs.add("-tunnel-server");
            attachArgs.add(bootstrap.getTunnelServer());
        }
        if (bootstrap.getAgentId() != null) {
            attachArgs.add("-agent-id");
            attachArgs.add(bootstrap.getAgentId());
        }
        if (bootstrap.getStatUrl() != null) {
            attachArgs.add("-stat-url");
            attachArgs.add(bootstrap.getStatUrl());
        }

        AnsiLog.info("Try to attach process " + pid);
        AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);

        //启动加载arthas-core.jar
        ProcessUtils.startArthasCore(pid, attachArgs);
        AnsiLog.info("Attach process {} success.", pid);
    }

    //如果只是attach java进程,到这儿就结束了,就不加载后面的telnet 客户端了。
    if (bootstrap.isAttachOnly()) {
        System.exit(0);
    }


    //查找arthas-client.jar,并反射调用TelnetConsole#main,启动telnet client
    URLClassLoader classLoader = new URLClassLoader(
                    new URL[] { new File(arthasHomeDir, "arthas-client.jar").toURI().toURL() });
    Class<?> telnetConsoleClas = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole");
    Method mainMethod = telnetConsoleClas.getMethod("main", String[].class);
    List<String> telnetArgs = new ArrayList<String>();

    if (bootstrap.getCommand() != null) {
        telnetArgs.add("-c");
        telnetArgs.add(bootstrap.getCommand());
    }
    if (bootstrap.getBatchFile() != null) {
        telnetArgs.add("-f");
        telnetArgs.add(bootstrap.getBatchFile());
    }
    if (bootstrap.getHeight() != null) {
        telnetArgs.add("--height");
        telnetArgs.add("" + bootstrap.getHeight());
    }
    if (bootstrap.getWidth() != null) {
        telnetArgs.add("--width");
        telnetArgs.add("" + bootstrap.getWidth());
    }

    // telnet port ,ip
    telnetArgs.add(bootstrap.getTargetIp());
    telnetArgs.add("" + bootstrap.getTelnetPort());

    AnsiLog.info("arthas-client connect {} {}", bootstrap.getTargetIp(), bootstrap.getTelnetPort());
    AnsiLog.debug("Start arthas-client.jar args: " + telnetArgs);

    // fix https://github.com/alibaba/arthas/issues/833
    Thread.currentThread().setContextClassLoader(classLoader);
    mainMethod.invoke(null, new Object[] { telnetArgs.toArray(new String[0]) });
}
SocketUtils#findTcpListenProcess

查找端口所对应的进程id

实现原理
如果是window系统:使用 netstat -ano -p TCP命令
如果是linux系统:使用 lsof -t -s TCP:LISTEN -i TCP: port 命令

public static int findTcpListenProcess(int port) {
    try {
        /**
         * window下使用 netstat -ano -p TCP命令 查看tcp连接
         * 查询回来的信息包含五部分,例如:TCP 0.0.0.0:49168 0.0.0.0:0 LISTENING 476
         */
        if (OSUtils.isWindows()) {
            String[] command = { "netstat", "-ano", "-p", "TCP" };
            List<String> lines = ExecutingCommand.runNative(command);
            for (String line : lines) {
                if (line.contains("LISTENING")) {
                    // TCP 0.0.0.0:49168 0.0.0.0:0 LISTENING 476
                    String[] strings = line.trim().split("\\s+");
                    if (strings.length == 5) {
                        if (strings[1].endsWith(":" + port)) {
                            return Integer.parseInt(strings[4]);
                        }
                    }
                }
            }
        }
        /**
         * 如果是linux系统,使用lsof -t -s TCP:LISTEN -i TCP: port 命令,查看端口号对应的进程id。
         */
        if (OSUtils.isLinux() || OSUtils.isMac()) {
            String pid = ExecutingCommand.getFirstAnswer("lsof -t -s TCP:LISTEN -i TCP:" + port);
            if (!pid.trim().isEmpty()) {
                return Integer.parseInt(pid);
            }
        }
    } catch (Throwable e) {
        // ignore
    }

    return -1;
}
ProcessUtils#select

1、使用java 的 jps -l 命令查找所有java 进程,并列出。
2、用户选择要attach的目标进程。

public static int select(boolean v, int telnetPortPid) throws InputMismatchException {
    //jps命令查找到的所有java进程,其中key:进程号,value:java进程
    Map<Integer, String> processMap = listProcessByJps(v);

    // Put the port that is already listening at the first
    //把已经监听的端口放在第一个
    if (telnetPortPid > 0 && processMap.containsKey(telnetPortPid)) {
        String telnetPortProcess = processMap.get(telnetPortPid);
        processMap.remove(telnetPortPid);
        Map<Integer, String> newProcessMap = new LinkedHashMap<Integer, String>();
        newProcessMap.put(telnetPortPid, telnetPortProcess);
        newProcessMap.putAll(processMap);
        processMap = newProcessMap;
    }

    //没有找到Java进程
    if (processMap.isEmpty()) {
        AnsiLog.info("Can not find java process. Try to pass <pid> in command line.");
        return -1;
    }

    /**
     * 打印找到的所有进程,例如:
     * [1]: 12744 org.jetbrains.jps.cmdline.Launcher
     * [2]: 1672
     */
    AnsiLog.info("Found existing java process, please choose one and hit RETURN.");
    // print list
    int count = 1;
    for (String process : processMap.values()) {
        if (count == 1) {
            System.out.println("* [" + count + "]: " + process);
        } else {
            System.out.println("  [" + count + "]: " + process);
        }
        count++;
    }

    //创建扫描器,等待接受用户输入数字
    // read choice
    String line = new Scanner(System.in).nextLine();
    if (line.trim().isEmpty()) {
        // get the first process id
        return processMap.keySet().iterator().next();
    }

    //用户键入的数字
    int choice = new Scanner(line).nextInt();

    if (choice <= 0 || choice > processMap.size()) {
        return -1;
    }

    //查找数字编号对应的进程id,并返回
    Iterator<Integer> idIter = processMap.keySet().iterator();
    for (int i = 1; i <= choice; ++i) {
        if (i == choice) {
            return idIter.next();
        }
        idIter.next();
    }

    return -1;
}
ProcessUtils#startArthasCore
public static void startArthasCore(int targetPid, List<String> attachArgs) {
    // find java/java.exe, then try to find tools.jar
    String javaHome = findJavaHome();

    // find java/java.exe
    File javaPath = findJava();
    if (javaPath == null) {
        throw new IllegalArgumentException(
                        "Can not find java/java.exe executable file under java home: " + javaHome);
    }

    //find tools.jar
    File toolsJar = findToolsJar();

    if (JavaVersionUtils.isLessThanJava9()) {
        if (toolsJar == null || !toolsJar.exists()) {
            throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome);
        }
    }

    //将java.exe路径放入command集合
    List<String> command = new ArrayList<String>();
    command.add(javaPath.getAbsolutePath());

    //将tools.jar路径放入command集合
    if (toolsJar != null && toolsJar.exists()) {
        //在tools.jar中查找相关类。
        command.add("-Xbootclasspath/a:" + toolsJar.getAbsolutePath());
    }

    command.addAll(attachArgs);
    // "${JAVA_HOME}"/bin/java \
    // ${opts} \
    // -jar "${arthas_lib_dir}/arthas-core.jar" \
    // -pid ${TARGET_PID} \
    // -target-ip ${TARGET_IP} \
    // -telnet-port ${TELNET_PORT} \
    // -http-port ${HTTP_PORT} \
    // -core "${arthas_lib_dir}/arthas-core.jar" \
    // -agent "${arthas_lib_dir}/arthas-agent.jar"

    //创建一个子进程
    ProcessBuilder pb = new ProcessBuilder(command);
    try {
        //启动子进程
        final Process proc = pb.start();

        //开启一个线程,接受子进程的输入流
        Thread redirectStdout = new Thread(new Runnable() {
            @Override
            public void run() {
                InputStream inputStream = proc.getInputStream();
                try {
                    IOUtils.copy(inputStream, System.out);
                } catch (IOException e) {
                    IOUtils.close(inputStream);
                }

            }
        });

        //开启一个线程,接受子进程的错误流
        Thread redirectStderr = new Thread(new Runnable() {
            @Override
            public void run() {
                InputStream inputStream = proc.getErrorStream();
                try {
                    IOUtils.copy(inputStream, System.err);
                } catch (IOException e) {
                    IOUtils.close(inputStream);
                }

            }
        });

        //启动线程,并加入main线程
        redirectStdout.start();
        redirectStderr.start();
        redirectStdout.join();
        redirectStderr.join();

        //获取子进程的退出值,如果值不等于0,退出子进程。
        //exitValue 表示子进程System.exit()中的值
        int exitValue = proc.exitValue();
        if (exitValue != 0) {
            AnsiLog.error("attach fail, targetPid: " + targetPid);
            System.exit(1);
        }
    } catch (Throwable e) {
        // ignore
    }

}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值