Arthas 原始碼分析(三):命令執行過程
Arthas原始碼第三篇之命令執行過程
ofollow,noindex" target="_blank">工具化|Arthas
這一篇主要聊下輸入命令,到最後響應的過程, 順帶著熟悉整個專案結構。(以下會邊貼程式碼邊解釋,篇幅可能比較長)
接著上一篇ArthasBootstrap bind方法. 在方法內部會發現下面一行程式碼:
程式碼塊
shellServer.listen(new BindHandler(isBindRef));
這行程式碼主要是監聽命令作用,具體到listen方法內部看看:
程式碼塊
@Override public ShellServer listen(final Handler<Future<Void>> listenHandler) { final List<TermServer> toStart; synchronized (this) { if (!closed) { throw new IllegalStateException("Server listening"); } toStart = termServers; } final AtomicInteger count = new AtomicInteger(toStart.size()); if (count.get() == 0) { setClosed(false); listenHandler.handle(Future.<Void>succeededFuture()); return this; } Handler<Future<TermServer>> handler = new TermServerListenHandler(this, listenHandler, toStart); for (TermServer termServer : toStart) { termServer.termHandler(new TermServerTermHandler(this)); termServer.listen(handler); } return this; }
可以看到TermServerTermHandler被例項化, 賦值給TermServer物件,然後TermServer監聽(下面telnet監聽為例).
程式碼塊
@Override public TermServer listen(Handler<Future<TermServer>> listenHandler) { // TODO: charset and inputrc from options bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port); try { bootstrap.start(new Consumer<TtyConnection>() { @Override public void accept(final TtyConnection conn) { termHandler.handle(new TermImpl(Helper.loadKeymap(), conn)); } }).get(connectionTimeout, TimeUnit.MILLISECONDS); listenHandler.handle(Future.<TermServer>succeededFuture()); } catch (Throwable t) { logger.error(null, "Error listening to port " + port, t); listenHandler.handle(Future.<TermServer>failedFuture(t)); } return this; }
- 方法內部啟動telnet埠監聽。
- Helper.loadKeymap()這個類方法主要是在專案目錄inputrc檔案里加載對應的快捷鍵以及對應的處理類name標識,返回個對映物件,對命令列介面快捷鍵指示處理需要。
- new TermImpl 看看這個類內部例項做了哪些事情
程式碼塊
private static final List<Function> readlineFunctions = Helper.loadServices(Function.class.getClassLoader(), Function.class); ... 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)); }
- TermImpl內部首先可以看到對Function類通過spi進行了所有Function的載入,Function就是剛才快捷鍵對應的處理類,下面隨便看看一個類,快捷鍵向上看歷史命令。
public class HistorySearchBackward implements Function { @Override public String name() { return "history-search-backward"; } @Override public void apply(Readline.Interaction interaction) { LineBuffer buf = interaction.buffer().copy(); int cursor = buf.getCursor(); List<int[]> history = interaction.history(); int curr = interaction.getHistoryIndex(); int searchStart = curr + 1; for (int i = searchStart; i < history.size(); ++i) { int[] line = history.get(i); if (LineBufferUtils.equals(buf, line)) { continue; } if (LineBufferUtils.matchBeforeCursor(buf, line)) { interaction.refresh(new LineBuffer().insert(line).setCursor(cursor)); interaction.setHistoryIndex(i); break; } } interaction.resume(); } }
- 上面可以看到readline的欄位history,通過本地history檔案加載出來。我們執行的歷史命令都會儲存到history檔案中。 可以猜測history命令怎麼查詢所有歷史命令,就是這樣拿出來的。
- 接著例項化DefaultTermStdinHandler,EventHandler以及對應的賦值,然後結合term框架,對相應的快捷鍵進行處理,這裡就不多說,感興趣自行去看。下面會重點說明help的整個過程。
- 回到上面termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));這一行程式碼。回顧一下最上面termServer listen的時候, termServer.termHandler(new TermServerTermHandler(this)); 例項化了TermServerTermHandler。所以這裡執行了TermServerTermHandler.handle方法
程式碼塊
public class TermServerTermHandler implements Handler<Term> { private ShellServerImpl shellServer; public TermServerTermHandler(ShellServerImpl shellServer) { this.shellServer = shellServer; } @Override public void handle(Term term) { shellServer.handleTerm(term); }
- 然後調到ShellServerImpl.handleTerm方法
程式碼塊
public void handleTerm(Term term) { synchronized (this) { // That might happen with multiple ser if (closed) { term.close(); return; } } 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 session.readline(); // Now readline }
- 這裡就初始化了session物件,然後設定了對應的歡迎語,所以你attach成功後,看到了圖形介面,wiki,version等。
- 這裡注意的是ShellImpl構造把命令列表以及內建命令快取到session記憶體。
- session.readline(); 然後就是等待使用者命令輸入了,如圖中$。 這裡利用了term框架封裝好的readline方法庫,同時根據對應ShellLineHandler來回調處理相應的命令。
程式碼塊
public void readline() { term.readline(Constants.DEFAULT_PROMPT, new ShellLineHandler(this), new CommandManagerCompletionHandler(commandManager)); } ... public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) { if (conn.getStdinHandler() != echoHandler) { throw new IllegalStateException(); } if (inReadline) { throw new IllegalStateException(); } inReadline = true; readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session)); } ... public class RequestHandler implements Consumer<String> { private TermImpl term; private final Handler<String> lineHandler; public RequestHandler(TermImpl term, Handler<String> lineHandler) { this.term = term; this.lineHandler = lineHandler; } @Override public void accept(String line) { term.setInReadline(false); lineHandler.handle(line); } }
- 下面我們就輸入help來看下專案整個處理過程.
- help輸入來到上面readline方法。最終回撥到ShellLineHandler.handle方法,ShellLineHandler handle方法關鍵步驟處理如下:
程式碼塊
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("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(); }
- 這裡做了前置的檔案檢查以及解析,help命令順利到了createJob這一步,一層層封裝點進去, 這裡主要是遍歷前面載入到記憶體的命令,如果找不到,command not found。
- 然後同時建立例項化CommandProcess, 這裡要注意的是找到command對應的processHandler賦值給ProcessImpl屬性了,這裡就埋下伏筆,為後面路由找到HelpCommand。
- 篇幅有限,其他的點這裡就不解釋下去了。
程式碼塊
@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; } ... private Process createProcess(List<CliToken> line, InternalCommandManager commandManager, int jobId, Term term) { try { ListIterator<CliToken> tokens = line.listIterator(); while (tokens.hasNext()) { CliToken token = tokens.next(); if (token.isText()) { Command command = commandManager.getCommand(token.value()); if (command != null) { return createCommandProcess(command, tokens, jobId, term); } else { throw new IllegalArgumentException(token.value() + ": command not found"); } } } throw new IllegalArgumentException(); } catch (Exception e) { throw new RuntimeException(e); } } ... public Command getCommand(String commandName) { Command command = null; for (CommandResolver resolver : resolvers) { // 內建命令在ShellLineHandler裡提前處理了,所以這裡不需要再查詢內建命令 if (resolver instanceof BuiltinCommandPack) { command = getCommand(resolver, commandName); if (command != null) { break; } } } return command; }
- 然後建立完job, 繼續回到ShellLineHandler job那塊程式碼,上面程式碼可以看出 job.run(); 對job啟動。這裡比較重要的是剛才建立的Process物件,這裡呼叫run方法
程式碼塊
public Job run(boolean foreground) { ... process.setTty(shell.term()); process.setSession(shell.session()); process.run(foreground); if (!foreground && foregroundUpdatedHandler != null) { foregroundUpdatedHandler.handle(null); } if (foreground) { shell.setForegroundJob(this); } else { shell.setForegroundJob(null); } return this; }
- 然後接著以下關鍵程式碼, 關鍵一步就是最好兩行ArthasBootstrap.getInstance().execute(task); 執行這個task,ProcessHandler執行process。
程式碼塊
@Override public synchronized void run(boolean fg) { ... CommandLine cl = null; try { if (commandContext.cli() != null) { if (commandContext.cli().parse(args2, false).isAskingForHelp()) { UsageMessageFormatter formatter = new StyledUsageFormatter(Color.green); formatter.setWidth(tty.width()); StringBuilder usage = new StringBuilder(); commandContext.cli().usage(usage, formatter); usage.append('\n'); tty.write(usage.toString()); terminate(); return; } cl = commandContext.cli().parse(args2); } } catch (CLIException e) { tty.write(e.getMessage() + "\n"); terminate(); return; } process = new CommandProcessImpl(args2, tty, cl); if (cacheLocation() != null) { process.echoTips("job id: " + this.jobId + "\n"); process.echoTips("cache location: " + cacheLocation() + "\n"); } Runnable task = new CommandProcessTask(process); ArthasBootstrap.getInstance().execute(task); } ... private class CommandProcessTask implements Runnable { private CommandProcess process; public CommandProcessTask(CommandProcess process) { this.process = process; } @Override public void run() { try { handler.handle(process); } catch (Throwable t) { logger.error(null, "Error during processing the command:", t); process.write("Error during processing the command: " + t.getMessage() + "\n"); terminate(1, null); } } }
- Handler的實現有很多,可以發現ProcessHandler. 這裡實現了handle,不過processHandler是在AnnotatedCommandImpl類裡面的。(AnnotatedCommandImpl類的話,在之前初始化命令的時候,就已經例項化了,Command.create(HelpCommand.class))
- process裡面instance.process(process)這一步,結合前面埋下的伏筆路由找到對應的HelpCommand的process
程式碼塊
private class ProcessHandler implements Handler<CommandProcess> { @Override public void handle(CommandProcess process) { process(process); } } ... private void process(CommandProcess process) { AnnotatedCommand instance; try { instance = clazz.newInstance(); } catch (Exception e) { process.end(); return; } CLIConfigurator.inject(process.commandLine(), instance); instance.process(process); UserStatUtil.arthasUsageSuccess(name(), process.args()); }
- HelpCommand裡面首先查詢session裡面的內建命令快取(還記得前面內建命令快取嗎?), 然後render出對應的help表格介面
程式碼塊
@Override public void process(CommandProcess process) { List<Command> commands = allCommands(process.session()); Command targetCmd = findCommand(commands); String message; if (targetCmd == null) { message = RenderUtil.render(mainHelp(commands), process.width()); } else { message = commandHelp(targetCmd, process.width()); } process.write(message); process.end(); }
最後
- 整個原始碼過程中還涉及到很多東西,一篇文章不能全部道來,只能把一部分流程寫出來,希望有點幫忙。
- 上面只是我閱讀原始碼過程中,順帶記錄下來的,有任何理解不對的地方可以指出來。