Arthas 启动过程分析

系列

arthas组成模块

arthas组成模块
arthas组成模块
  • arthas-boot.jar和as.sh模块功能类似,分别使用java和shell脚本,下载对应的jar包,并生成服务端和客户端的启动命令启动客户端和服务端。

  • arthas-core.jar是服务端程序的启动入口类,会调用virtualMachine#attach到目标进程,并加载arthas-agent.jar作为agent jar包。

  • arthas-agent.jar既可以使用premain方式(进程启动之前),也可以通过agentmain方式(进程启动之后)。arthas-agent会使用自定义的classloader(ArthasClassLoader)加载arthas-core.jar里面的Configure类和ArthasBootstrap类。 同时程序运行的时候会使用arthas-spy.jar。

  • arthas-spy.jar里面只包含Spy类,目的是为了将Spy类使用BootstrapClassLoader来加载,从而使目标进程的java应用可以访问Spy类。通过ASM修改字节码,可以将Spy类的方法ON_BEFORE_METHOD、ON_RETURN_METHOD等编织到目标类里面。Spy类你可以简单理解为类似spring aop的Advice,有前置方法,后置方法等。

  • arthas-client.jar是客户端程序,用来连接arthas-core.jar启动的服务端代码,使用telnet方式。一般由arthas-boot.jar和as.sh来负责启动。


arthas启动过程

arthas下载

arthas-packaging
  • arthas-boot或者as.sh在启动过程中会去maven仓库下载arthas-packaging的包,这个包里面包含了arthas相关的所有jar包。


https://maven.aliyun.com/repository/public/com/taobao/arthas/arthas-packaging/3.2.0/arthas-packaging-3.2.0-bin.zip

[INFO] Start download arthas from remote server: https://maven.aliyun.com/repository/public/com/taobao/arthas/arthas-packaging/3.2.0/arthas-packaging-3.2.0-bin.zip
[INFO] File size: 10.82 MB, downloaded size: 1.08 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 2.18 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 3.20 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 4.21 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 5.29 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 6.40 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 7.54 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 8.66 MB, downloading ...
[INFO] File size: 10.82 MB, downloaded size: 9.78 MB, downloading ...

解压目录
/Users/lebron374/.arthas/lib/3.2.0/arthas

目录结构
xiaozhideMacBook-Pro:arthas-packaging-3.2.0-bin lebron374$ tree -L 3
.
├── arthas-agent.jar
├── arthas-boot.jar
├── arthas-client.jar
├── arthas-core.jar
├── arthas-demo.jar
├── arthas-spy.jar
├── arthas.properties
├── as-service.bat
├── as.bat
├── as.sh
├── async-profiler
│   ├── libasyncProfiler-linux-arm.so
│   ├── libasyncProfiler-linux-x64.so
│   └── libasyncProfiler-mac-x64.so
├── install-local.sh
└── logback.xml
  • arthas-boot下载过程的日志和arthas-packaging解压后的jar包文件如上所示。


arthas启动

package com.taobao.arthas.boot;

public class ProcessUtils {
    public static void startArthasCore(long targetPid, List<String> attachArgs) {
        String javaHome = findJavaHome();
        File javaPath = findJava();
        File toolsJar = findToolsJar();

        List<String> command = new ArrayList<String>();
        command.add(javaPath.getAbsolutePath());

        if (toolsJar != null && toolsJar.exists()) {
            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();
            // 省略相关代码
        } catch (Throwable e) {
            // ignore
        }
    }
}
startArthasCore
  • arthas-boot的启动命令如上图所示,核心需要了解的是启动arthas-core的命令参数以及通过ProcessBuilder来完成启动。
  • startArthasCore图中已经列出所有相关的启动参数,可以重点关注-jar后面的那个参数,指定的就是arthas-core.jar文件,负责启动arthas-core模块。


arthas-core

pom.xml指定main入口函数

<transformers>
    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
        <mainClass>com.taobao.arthas.core.Arthas</mainClass>
        <manifestEntries>
            <Created-By>core engine team, middleware group, alibaba inc.</Created-By>
            <Implementation-Version>${project.version}</Implementation-Version>
            <Implementation-Vendor-Id>com.taobao.arthas</Implementation-Vendor-Id>
            <Specification-Version>${project.version}</Specification-Version>
            <Specification-Title>arthas-core</Specification-Title>
        </manifestEntries>
    </transformer>
</transformers>
  • arthas-core的pom.xml指定mainClass=com.taobao.arthas.core.Arthas,进入arthas-core的启动入口函数。


package com.taobao.arthas.core;

/**
 * Arthas启动器
 */
public class Arthas {

    private static final String DEFAULT_TELNET_PORT = "3658";
    private static final String DEFAULT_HTTP_PORT = "8563";

    public static void main(String[] args) {
        try {
            new Arthas(args);
        } catch (Throwable t) {
        }
    }

    private Arthas(String[] args) throws Exception {
        // parse(args)解析参数并通过attachAgent启动
        attachAgent(parse(args));
    }

    private void attachAgent(Configure configure) throws Exception {

        VirtualMachineDescriptor virtualMachineDescriptor = null;
        for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
            String pid = descriptor.id();
            if (pid.equals(Long.toString(configure.getJavaPid()))) {
                virtualMachineDescriptor = descriptor;
            }
        }

        // 1.绑定pid生成VirtualMachine
        VirtualMachine virtualMachine = null;
        try {
            if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
                virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
            } else {
                virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
            }

            // 省略相关其他代码

            String arthasAgentPath = configure.getArthasAgent();
            //convert jar path to unicode string
            configure.setArthasAgent(encodeArg(arthasAgentPath));
            configure.setArthasCore(encodeArg(configure.getArthasCore()));
            // 2.VirtualMachine绑定agent对象
            virtualMachine.loadAgent(arthasAgentPath,
                    configure.getArthasCore() + ";" + configure.toString());
        } finally {
            if (null != virtualMachine) {
                virtualMachine.detach();
            }
        }
    }
}
  • arthas的启动过程中包含3个步骤:解析参数、绑定指定pid、attach指定agent。
  • parse(args)负责解析参数。
  • virtualMachine = VirtualMachine.attach("" + configure.getJavaPid())负责绑定指定pid生成VirtualMachine对象。
  • virtualMachine.loadAgent()用于指定的virtualMachine加载agent对象。



VirtualMachine-attach

VirtualMachine-loadAgent
VirtualMachine-detach
  • VirtualMachine的几个核心API的介绍,具体参考文章末尾引用的文章。


arthas-agent

    <build>
        <finalName>arthas-agent</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>com.taobao.arthas.agent3.AgentBootstrap</Premain-Class>
                                    <Agent-Class>com.taobao.arthas.agent3.AgentBootstrap</Agent-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    <Specification-Title>${project.name}</Specification-Title>
                                    <Specification-Version>${project.version}</Specification-Version>
                                    <Implementation-Title>${project.name}</Implementation-Title>
                                    <Implementation-Version>${project.version}</Implementation-Version>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • arthas-agent的pom.xml指定Premain-Class和Agent-Class,对应AgentBootstrap类。
  • Premain-Class在类文件加载且main方法执行之前修改,无法实现真正的运行时修改。
  • Agent-Class使用agent attach api附到待更新的jvm上,然后动态加载agent,agent与premain里的几乎相同,只不过这里是在jvm已经运行起来以后加载。


package com.taobao.arthas.agent3;

public class AgentBootstrap {
    private static final String RESET = "resetArthasClassLoader";
    private static final String ARTHAS_SPY_JAR = "arthas-spy.jar";
    private static final String ARTHAS_CORE_JAR = "arthas-core.jar";
    private static final String ARTHAS_BOOTSTRAP = "com.taobao.arthas.core.server.ArthasBootstrap";
    private static final String GET_INSTANCE = "getInstance";
    private static final String IS_BIND = "isBind";
    private static final String BIND = "bind";

    public static void premain(String args, Instrumentation inst) {
        main(args, inst);
    }

    public static void agentmain(String args, Instrumentation inst) {
        main(args, inst);
    }

    private static synchronized void main(String args, final Instrumentation inst) {
        try {
            ps.println("Arthas server agent start...");
            // 传递的args参数分两个部分:arthasCoreJar路径和agentArgs, 分别是Agent的JAR包路径和期望传递到服务端的参数
            args = decodeArg(args);

            String arthasCoreJar;
            final String agentArgs;
            int index = args.indexOf(';');
            if (index != -1) {
                arthasCoreJar = args.substring(0, index);
                agentArgs = args.substring(index);
            } else {
                arthasCoreJar = "";
                agentArgs = args;
            }

            File arthasCoreJarFile = new File(arthasCoreJar);
            File spyJarFile = new File(arthasCoreJarFile.getParentFile(), ARTHAS_SPY_JAR);
            // 1、获取ClassLoader
            final ClassLoader agentLoader = getClassLoader(inst, spyJarFile, arthasCoreJarFile);
            // 2、初始化initSpy()
            initSpy();

            Thread bindingThread = new Thread() {
                @Override
                public void run() {
                    try {
                        // 3、执行bind动作
                        bind(inst, agentLoader, agentArgs);
                    } catch (Throwable throwable) {
                        throwable.printStackTrace(ps);
                    }
                }
            };

            bindingThread.setName("arthas-binding-thread");
            bindingThread.start();
            bindingThread.join();
        } catch (Throwable t) {
        }
    }

    private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File arthasCoreJarFile) throws Throwable {
        ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
        Class<?> spyClass = null;
        if (parent != null) {
            try {
                parent.loadClass("java.arthas.Spy");
            } catch (Throwable e) {
                // ignore
            }
        }

        // 将Spy添加到BootstrapClassLoader
        if (spyClass == null) {
            inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
        }

        // 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀
        return loadOrDefineClassLoader(arthasCoreJarFile);
    }

    // 通过反射实现bind过程的调用
    private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
        Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
        // 执行com.taobao.arthas.core.server.ArthasBootstrap#getInstance
        Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
        boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
        if (!isBind) {
            try {
                ps.println("Arthas start to bind...");
                // 执行com.taobao.arthas.core.server.ArthasBootstrap#bind
                bootstrapClass.getMethod(BIND, String.class).invoke(bootstrap, args);
                ps.println("Arthas server bind success.");
                return;
            } catch (Exception e) {
            }
        }
        ps.println("Arthas server already bind.");
    }
}
  • AgentBootstrap的premain和agentmain内部统一执行main(args, inst)。
  • AgentBootstrap#getClassLoader负责构造自定义的类加载器ArthasClassloader,尽量减少Arthas对现有工程的侵蚀。
  • AgentBootstrap#bind通过反射执行com.taobao.arthas.core.server.ArthasBootstrap#getInstance和
    com.taobao.arthas.core.server.ArthasBootstrap#bind。


arthas-core

public class ArthasBootstrap {
    public static final String ARTHAS_HOME_PROPERTY = "arthas.home";
    private static String ARTHAS_SHOME = null;
    public static final String CONFIG_NAME_PROPERTY  = "arthas.config.name";
    public static final String CONFIG_LOCATION_PROPERTY = "arthas.config.location";
    public static final String CONFIG_OVERRIDE_ALL= "arthas.config.overrideAll";
    private static ArthasBootstrap arthasBootstrap;
    private ArthasEnvironment arthasEnvironment;
    private Configure configure;
    private AtomicBoolean isBindRef = new AtomicBoolean(false);
    private Instrumentation instrumentation;
    private Thread shutdown;
    private ShellServer shellServer;
    private ExecutorService executorService;
    private TunnelClient tunnelClient;
    private File arthasOutputDir;
    private static LoggerContext loggerContext;

    private ArthasBootstrap(Instrumentation instrumentation, String args) throws Throwable {
        this.instrumentation = instrumentation;

        String outputPath = System.getProperty("arthas.output.dir", "arthas-output");
        arthasOutputDir = new File(outputPath);
        arthasOutputDir.mkdirs();

        // 1. initSpy()
        initSpy();
        // 2. ArthasEnvironment
        initArthasEnvironment(args);
        // 3. init logger
        loggerContext = LogUtil.initLooger(arthasEnvironment);

        // 4. start agent server
        bind(configure);

        executorService = Executors.newCachedThreadPool(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                final Thread t = new Thread(r, "as-command-execute-daemon");
                t.setDaemon(true);
                return t;
            }
        });

        shutdown = new Thread("as-shutdown-hooker") {

            @Override
            public void run() {
                ArthasBootstrap.this.destroy();
            }
        };

        Runtime.getRuntime().addShutdownHook(shutdown);
    }

    private static void initSpy() throws ClassNotFoundException, NoSuchMethodException {
        Class<?> adviceWeaverClass = AdviceWeaver.class;
        Method onBefore = adviceWeaverClass.getMethod(AdviceWeaver.ON_BEFORE, int.class, ClassLoader.class, String.class,
                String.class, String.class, Object.class, Object[].class);
        Method onReturn = adviceWeaverClass.getMethod(AdviceWeaver.ON_RETURN, Object.class);
        Method onThrows = adviceWeaverClass.getMethod(AdviceWeaver.ON_THROWS, Throwable.class);
        Method beforeInvoke = adviceWeaverClass.getMethod(AdviceWeaver.BEFORE_INVOKE, int.class, String.class, String.class, String.class, int.class);
        Method afterInvoke = adviceWeaverClass.getMethod(AdviceWeaver.AFTER_INVOKE, int.class, String.class, String.class, String.class, int.class);
        Method throwInvoke = adviceWeaverClass.getMethod(AdviceWeaver.THROW_INVOKE, int.class, String.class, String.class, String.class, int.class);
        Spy.init(AdviceWeaver.class.getClassLoader(), onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke);
    }
    
    private void initArthasEnvironment(String args) throws IOException {
        if (arthasEnvironment == null) {
            arthasEnvironment = new ArthasEnvironment();
        }

        Map<String, String> argsMap = FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toMap(args);
        // 给配置全加上前缀
        Map<String, Object> mapWithPrefix = new HashMap<String, Object>(argsMap.size());
        for (Entry<String, String> entry : argsMap.entrySet()) {
            mapWithPrefix.put("arthas." + entry.getKey(), entry.getValue());
        }
        mapWithPrefix.put(ARTHAS_HOME_PROPERTY, arthasHome());

        MapPropertySource mapPropertySource = new MapPropertySource("args", mapWithPrefix);
        arthasEnvironment.addFirst(mapPropertySource);

        tryToLoadArthasProperties();

        configure = new Configure();
        BinderUtils.inject(arthasEnvironment, configure);
    }

    public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, String args) throws Throwable {
        if (arthasBootstrap == null) {
            arthasBootstrap = new ArthasBootstrap(instrumentation, args);
        }
        return arthasBootstrap;
    }
}
  • ArthasBootstrap#getInstance会创建ArthasBootstrap对象。
  • ArthasBootstrap对象的创建过程中会执行initSpy、initArthasEnvironment、bind三个方法,这三个方法的作用暂时还没理解,待后面理解了再进行补充。
  • ArthasBootstrap#bind(configure)负责启动Server端的监听。


package com.taobao.arthas.core.server;

public class ArthasBootstrap {

    public void bind(Configure configure) throws Throwable {

        long start = System.currentTimeMillis();

        try {
            ShellServerOptions options = new ShellServerOptions()
                            .setInstrumentation(instrumentation)
                            .setPid(PidUtils.currentLongPid())
                            .setSessionTimeout(configure.getSessionTimeout() * 1000);

            if (agentId != null) {
                Map<String, String> welcomeInfos = new HashMap<String, String>();
                welcomeInfos.put("id", agentId);
                options.setWelcomeMessage(ArthasBanner.welcome(welcomeInfos));
            }

            // 1、创建ShellServerImpl对象
            shellServer = new ShellServerImpl(options, this);
            // 2、绑定命令解析
            BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
            List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
            resolvers.add(builtinCommands);

            // 3、设置HttpTelnetTermServer
            if (configure.getTelnetPort() > 0) {
                shellServer.registerTermServer(new HttpTelnetTermServer(configure.getIp(), configure.getTelnetPort(),
                                options.getConnectionTimeout()));
            } 

            // 4、设置HttpTermServer
            if (configure.getHttpPort() > 0) {
                shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
                                options.getConnectionTimeout()));
            } 
            
            // 5、绑定命令解析resolver
            for (CommandResolver resolver : resolvers) {
                shellServer.registerCommandResolver(resolver);
            }

            // 6、启动监听listen
            shellServer.listen(new BindHandler(isBindRef));

            UserStatUtil.setStatUrl(configure.getStatUrl());
            UserStatUtil.arthasStart();

        } catch (Throwable e) {
        }
    }
}

<br>
  • 创建ShellServerImpl对象,new ShellServerImpl(options, this)。
  • 绑定命令解析, resolvers.add(builtinCommands)。
  • 设置HttpTelnetTermServer, shellServer.registerTermServer(new HttpTelnetTermServer())。
  • 设置HttpTermServer,shellServer.registerTermServer(new HttpTermServer())。
  • 绑定命令解析resolver,shellServer.registerCommandResolver(resolver)。
  • 启动监听listen,shellServer.listen()。


public class BuiltinCommandPack implements CommandResolver {

    private static List<Command> commands = new ArrayList<Command>();

    static {
        initCommands();
    }

    private static void initCommands() {
        commands.add(Command.create(HelpCommand.class));
        commands.add(Command.create(KeymapCommand.class));
        commands.add(Command.create(SearchClassCommand.class));
        commands.add(Command.create(SearchMethodCommand.class));
        commands.add(Command.create(ClassLoaderCommand.class));
        commands.add(Command.create(JadCommand.class));
        commands.add(Command.create(GetStaticCommand.class));
        commands.add(Command.create(MonitorCommand.class));
        commands.add(Command.create(StackCommand.class));
        commands.add(Command.create(ThreadCommand.class));
        commands.add(Command.create(TraceCommand.class));
        commands.add(Command.create(WatchCommand.class));
        commands.add(Command.create(TimeTunnelCommand.class));
        commands.add(Command.create(JvmCommand.class));
        commands.add(Command.create(PerfCounterCommand.class));
        // commands.add(Command.create(GroovyScriptCommand.class));
        commands.add(Command.create(OgnlCommand.class));
        commands.add(Command.create(MemoryCompilerCommand.class));
        commands.add(Command.create(RedefineCommand.class));
        commands.add(Command.create(DashboardCommand.class));
        commands.add(Command.create(DumpClassCommand.class));
        commands.add(Command.create(HeapDumpCommand.class));
        commands.add(Command.create(JulyCommand.class));
        commands.add(Command.create(ThanksCommand.class));
        commands.add(Command.create(OptionsCommand.class));
        commands.add(Command.create(ClsCommand.class));
        commands.add(Command.create(ResetCommand.class));
        commands.add(Command.create(VersionCommand.class));
        commands.add(Command.create(SessionCommand.class));
        commands.add(Command.create(SystemPropertyCommand.class));
        commands.add(Command.create(SystemEnvCommand.class));
        commands.add(Command.create(VMOptionCommand.class));
        commands.add(Command.create(LoggerCommand.class));
        commands.add(Command.create(HistoryCommand.class));
        commands.add(Command.create(CatCommand.class));
        commands.add(Command.create(EchoCommand.class));
        commands.add(Command.create(PwdCommand.class));
        commands.add(Command.create(MBeanCommand.class));
        commands.add(Command.create(GrepCommand.class));
        commands.add(Command.create(TeeCommand.class));
        commands.add(Command.create(ProfilerCommand.class));
        commands.add(Command.create(ShutdownCommand.class));
        commands.add(Command.create(StopCommand.class));
    }
}

class BuiltinCommandResolver implements CommandResolver {

    private Handler<CommandProcess> handler;

    public BuiltinCommandResolver() {
        this.handler = new NoOpHandler();
    }

    @Override
    public List<Command> commands() {
        return Arrays.asList(CommandBuilder.command("exit").processHandler(handler).build(),
                             CommandBuilder.command("quit").processHandler(handler).build(),
                            CommandBuilder.command("jobs").processHandler(handler).build(),
                             CommandBuilder.command("fg").processHandler(handler).build(),
                             CommandBuilder.command("bg").processHandler(handler).build(),
                             CommandBuilder.command("kill").processHandler(handler).build(),
                             CommandBuilder.command(PlainTextHandler.NAME).processHandler(handler).build(),
                             CommandBuilder.command(GrepHandler.NAME).processHandler(handler).build(),
                             CommandBuilder.command(WordCountHandler.NAME).processHandler(handler).build());
    }
}
  • 内部的命令解析对象BuiltinCommandResolver。
  • 自定义的命令解析对象BuiltinCommandPack。
  • 每个Command对应一个命令的实现方式。


监听过程分析

package com.taobao.arthas.core.shell.impl;

public class ShellServerImpl extends ShellServer {

    private final CopyOnWriteArrayList<CommandResolver> resolvers;
    private final InternalCommandManager commandManager;
    private final List<TermServer> termServers;
    private final long timeoutMillis;
    private final long reaperInterval;
    private String welcomeMessage;
    private ArthasBootstrap bootstrap;
    private Instrumentation instrumentation;
    private long pid;
    private boolean closed = true;
    private final Map<String, ShellImpl> sessions;
    private final Future<Void> sessionsClosed = Future.future();
    private ScheduledExecutorService scheduledExecutorService;
    private JobControllerImpl jobController = new GlobalJobControllerImpl();

    @Override
    public synchronized ShellServer registerCommandResolver(CommandResolver resolver) {
        resolvers.add(0, resolver);
        return this;
    }

    @Override
    public synchronized ShellServer registerTermServer(TermServer termServer) {
        termServers.add(termServer);
        return this;
    }

    @Override
    public ShellServer listen(final Handler<Future<Void>> listenHandler) {
      
        // 1、获取需要启动的TermServer对象
        final List<TermServer> toStart;
        synchronized (this) {
            if (!closed) {
                throw new IllegalStateException("Server listening");
            }
            toStart = termServers;
        }

        Handler<Future<TermServer>> handler = new TermServerListenHandler(this, listenHandler, toStart);
        for (TermServer termServer : toStart) {
            // TermServerTermHandler的参数this=ShellServerImpl
            termServer.termHandler(new TermServerTermHandler(this));
            termServer.listen(handler);
        }
        return this;
    }
}
  • ShellServerImpl的bind针对所有的termServers执行listen操作。
  • 以HttpTermServer为例描述termServer的listen过程。


public class HttpTermServer extends TermServer {

    private Handler<Term> termHandler;
    private NettyWebsocketTtyBootstrap bootstrap;
    private String hostIp;
    private int port;
    private long connectionTimeout;

    @Override
    public TermServer termHandler(Handler<Term> handler) {
        this.termHandler = handler;
        return this;
    }

    @Override
    public TermServer listen(Handler<Future<TermServer>> listenHandler) {
        // listenHandler 是 TermServerListenHandler
        bootstrap = new NettyWebsocketTtyBootstrap().setHost(hostIp).setPort(port);
        try {
            bootstrap.start(new Consumer<TtyConnection>() {
                @Override
                public void accept(final TtyConnection conn) {
                    // termHandler为TermServerTermHandler
                    termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
                }
            }).get(connectionTimeout, TimeUnit.MILLISECONDS);
            listenHandler.handle(Future.<TermServer>succeededFuture());
        } catch (Throwable t) {
        }
        return this;
    }
}
  • 执行NettyWebsocketTtyBootstrap的start进行监听。
  • termHandler.handle()执行的是TermServerTermHandler对象,相当于调用TermServerTermHandler#hanndle方法。


public class TermServerTermHandler implements Handler<Term> {
    private ShellServerImpl shellServer;

    public TermServerTermHandler(ShellServerImpl shellServer) {
        this.shellServer = shellServer;
    }

    @Override
    public void handle(Term term) {
        // 执行ShellServerImpl的handleTerm方法
        shellServer.handleTerm(term);
    }
}
  • TermServerTermHandler#handle会执行ShellServerImpl#handleTerm方法。


public class ShellServerImpl extends ShellServer {

    private final CopyOnWriteArrayList<CommandResolver> resolvers;
    private final InternalCommandManager commandManager;
    private final List<TermServer> termServers;
    private final long timeoutMillis;
    private final long reaperInterval;
    private String welcomeMessage;
    private ArthasBootstrap bootstrap;
    private Instrumentation instrumentation;
    private long pid;
    private boolean closed = true;
    private final Map<String, ShellImpl> sessions;
    private final Future<Void> sessionsClosed = Future.future();
    private ScheduledExecutorService scheduledExecutorService;
    private JobControllerImpl jobController = new GlobalJobControllerImpl();

    public void handleTerm(Term term) {
        ShellImpl session = createShell(term);
        session.setWelcome(welcomeMessage);
        session.closedFuture.setHandler(new SessionClosedHandler(this, session));
        session.init();
        sessions.put(session.id, session); // Put after init so the close handler on the connection is set
        // 执行ShellImpl的readline方法
        session.readline(); 
    }

    @Override
    public synchronized ShellImpl createShell(Term term) {
        // 核心的commandManager对象
        return new ShellImpl(this, term, commandManager, instrumentation, pid, jobController);
    }
}
  • ShellServerImpl#handleTerm内部执行createShell创建ShellImpl对象。
  • 通过ShellImpl#readline解析命令。


public class ShellImpl implements Shell {

    private JobControllerImpl jobController;
    final String id;
    final Future<Void> closedFuture;
    private InternalCommandManager commandManager;
    private Session session = new SessionImpl();
    private Term term;
    private String welcome;
    private Job currentForegroundJob;
    private String prompt;

    public ShellImpl(ShellServer server, Term term, InternalCommandManager commandManager,
            Instrumentation instrumentation, long pid, JobControllerImpl jobController) {
        session.put(Session.COMMAND_MANAGER, commandManager);
        session.put(Session.INSTRUMENTATION, instrumentation);
        session.put(Session.PID, pid);
        session.put(Session.SERVER, server);
        session.put(Session.TTY, term);
        this.id = UUID.randomUUID().toString();
        session.put(Session.ID, id);
        this.commandManager = commandManager;
        this.closedFuture = Future.future();
        this.term = term;
        this.jobController = jobController;

        if (term != null) {
            term.setSession(session);
        }

        this.setPrompt();
    }

    public void readline() {
        term.readline(prompt, new ShellLineHandler(this),
                new CommandManagerCompletionHandler(commandManager));
    }
}
  • ShellImpl#readline执行的TermImpl#readline。
  • TermImpl#readline的第二个参数是ShellLineHandler,ShellLineHandler()参数是ShellImpl。


public class TermImpl implements Term {

    public TermImpl(Keymap keymap, TtyConnection conn) {
        this.conn = conn;
        readline = new Readline(keymap);
        readline.setHistory(FileUtils.loadCommandHistory(new File(Constants.CMD_HISTORY_FILE)));
        for (Function function : readlineFunctions) {
            readline.addFunction(function);
        }

        echoHandler = new DefaultTermStdinHandler(this);
        conn.setStdinHandler(echoHandler);
        conn.setEventHandler(new EventHandler(this));
    }


    public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) {

        inReadline = true;

        readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session));
    }
}
  • TermImpl#readline执行readline.readline。
  • lineHandler指的是ShellLineHandler,执行ShellLineHandler#handle方法。


public class ShellLineHandler implements Handler<String> {

    private ShellImpl shell;
    private Term term;

    public ShellLineHandler(ShellImpl shell) {
        this.shell = shell;
        this.term = shell.term();
    }

    @Override
    public void handle(String line) {
        if (line == null) {
            // EOF
            handleExit();
            return;
        }

        List<CliToken> tokens = CliTokens.tokenize(line);
        CliToken first = TokenUtils.findFirstTextToken(tokens);
        if (first == null) {
            // For now do like this
            shell.readline();
            return;
        }

        String name = first.value();
        if (name.equals("exit") || name.equals("logout") || name.equals("q") || name.equals("quit")) {
            handleExit();
            return;
        } else if (name.equals("jobs")) {
            handleJobs();
            return;
        } else if (name.equals("fg")) {
            handleForeground(tokens);
            return;
        } else if (name.equals("bg")) {
            handleBackground(tokens);
            return;
        } else if (name.equals("kill")) {
            handleKill(tokens);
            return;
        }

        Job job = createJob(tokens);
        if (job != null) {
            job.run();
        }
    }

    private Job createJob(List<CliToken> tokens) {
        Job job;
        try {
            job = shell.createJob(tokens);
        } catch (Exception e) {
            term.echo(e.getMessage() + "\n");
            shell.readline();
            return null;
        }
        return job;
    }

    private void handleJobs() {
        for (Job job : shell.jobController().jobs()) {
            String statusLine = shell.statusLine(job, job.status());
            term.write(statusLine);
        }
        shell.readline();
    }
}
  • 核心关注ShellLineHandler#createJob,最终会调用ShellImpl#createJob。


public class ShellImpl implements Shell {
    public synchronized Job createJob(List<CliToken> args) {
        Job job = jobController.createJob(commandManager, args, this);
        return job;
    }
}

public class JobControllerImpl implements JobController {

    @Override
    public Job createJob(InternalCommandManager commandManager, List<CliToken> tokens, ShellImpl shell) {
        int jobId = idGenerator.incrementAndGet();
        StringBuilder line = new StringBuilder();
        for (CliToken arg : tokens) {
            line.append(arg.raw());
        }
        boolean runInBackground = runInBackground(tokens);
        Process process = createProcess(tokens, commandManager, jobId, shell.term());
        process.setJobId(jobId);
        JobImpl job = new JobImpl(jobId, this, process, line.toString(), runInBackground, shell);
        jobs.put(jobId, job);
        return job;
    }
}
  • JobControllerImpl#createJob负责根据命令生成对应的任务执行。


参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271