1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.command; 18 19 import com.android.ddmlib.Log.LogLevel; 20 import com.android.tradefed.clearcut.ClearcutClient; 21 import com.android.tradefed.clearcut.TerminateClearcutClient; 22 import com.android.tradefed.config.ArgsOptionParser; 23 import com.android.tradefed.config.ConfigurationException; 24 import com.android.tradefed.config.ConfigurationFactory; 25 import com.android.tradefed.config.GlobalConfiguration; 26 import com.android.tradefed.config.IConfigurationFactory; 27 import com.android.tradefed.config.Option; 28 import com.android.tradefed.device.IDeviceManager; 29 import com.android.tradefed.log.ConsoleReaderOutputStream; 30 import com.android.tradefed.log.LogRegistry; 31 import com.android.tradefed.log.LogUtil.CLog; 32 import com.android.tradefed.util.ArrayUtil; 33 import com.android.tradefed.util.ConfigCompletor; 34 import com.android.tradefed.util.FileUtil; 35 import com.android.tradefed.util.QuotationAwareTokenizer; 36 import com.android.tradefed.util.RegexTrie; 37 import com.android.tradefed.util.RunUtil; 38 import com.android.tradefed.util.StreamUtil; 39 import com.android.tradefed.util.TimeUtil; 40 import com.android.tradefed.util.VersionParser; 41 import com.android.tradefed.util.ZipUtil; 42 import com.android.tradefed.util.keystore.IKeyStoreFactory; 43 import com.android.tradefed.util.keystore.KeyStoreException; 44 45 import com.google.common.annotations.VisibleForTesting; 46 47 import java.io.File; 48 import java.io.IOException; 49 import java.io.PrintStream; 50 import java.io.PrintWriter; 51 import java.util.ArrayList; 52 import java.util.Collection; 53 import java.util.Collections; 54 import java.util.LinkedHashMap; 55 import java.util.LinkedList; 56 import java.util.List; 57 import java.util.ListIterator; 58 import java.util.Map; 59 import java.util.TreeMap; 60 import java.util.regex.Pattern; 61 62 import jline.ConsoleReader; 63 import sun.misc.Signal; 64 import sun.misc.SignalHandler; 65 66 /** 67 * Main TradeFederation console providing user with the interface to interact 68 * <p/> 69 * Currently supports operations such as 70 * <ul> 71 * <li>add a command to test 72 * <li>list devices and their state 73 * <li>list invocations in progress 74 * <li>list commands in queue 75 * <li>dump invocation log to file/stdout 76 * <li>shutdown 77 * </ul> 78 */ 79 public class Console extends Thread { 80 81 private static final String CONSOLE_PROMPT = "\u001B[0;32mtf >\u001B[0;0m"; 82 83 protected static final String HELP_PATTERN = "\\?|h|help"; 84 protected static final String LIST_PATTERN = "l(?:ist)?"; 85 protected static final String DUMP_PATTERN = "d(?:ump)?"; 86 protected static final String RUN_PATTERN = "r(?:un)?"; 87 protected static final String EXIT_PATTERN = "(?:q|exit)"; 88 protected static final String SET_PATTERN = "s(?:et)?"; 89 protected static final String INVOC_PATTERN = "i(?:nvocation)?"; 90 protected static final String VERSION_PATTERN = "version"; 91 protected static final String REMOVE_PATTERN = "remove"; 92 protected static final String DEBUG_PATTERN = "debug"; 93 protected static final String LIST_COMMANDS_PATTERN = "c(?:ommands)?"; 94 95 protected static final String LINE_SEPARATOR = System.getProperty("line.separator"); 96 97 private static ConsoleReaderOutputStream sConsoleStream = null; 98 99 protected ICommandScheduler mScheduler; 100 protected IKeyStoreFactory mKeyStoreFactory; 101 protected ConsoleReader mConsoleReader; 102 private RegexTrie<Runnable> mCommandTrie = new RegexTrie<Runnable>(); 103 private boolean mShouldExit = false; 104 private List<String> mMainArgs = new ArrayList<String>(0); 105 private long mConsoleStartTime; 106 107 /** A convenience type for <code>{@literal List<List<String>>}</code> */ 108 @SuppressWarnings("serial") 109 protected static class CaptureList extends LinkedList<List<String>> { CaptureList()110 CaptureList() { 111 super(); 112 } 113 CaptureList(Collection<? extends List<String>> c)114 CaptureList(Collection<? extends List<String>> c) { 115 super(c); 116 } 117 } 118 119 /** A {@link Runnable} with a {@code run} method that can take an argument */ 120 protected abstract static class ArgRunnable<T> implements Runnable { 121 @Override run()122 public void run() { 123 run(null); 124 } 125 run(T args)126 abstract public void run(T args); 127 } 128 129 /** 130 * This is a sentinel class that will cause TF to shut down. This enables a user to get TF to 131 * shut down via the RegexTrie input handling mechanism. 132 */ 133 private class QuitRunnable extends ArgRunnable<CaptureList> { 134 @Option(name = "handover-port", description = 135 "Used to indicate that currently managed devices should be 'handed over' to new " + 136 "tradefed process, which is listening on specified port") 137 private Integer mHandoverPort = null; 138 139 @Option(name = "wait-for-commands", shortName = 'c', description = 140 "only exit after all commands have executed ") 141 private boolean mExitOnEmpty = false; 142 143 @Override run(CaptureList args)144 public void run(CaptureList args) { 145 try { 146 if (args.size() >= 2 && !args.get(1).isEmpty()) { 147 List<String> optionArgs = getFlatArgs(1, args); 148 ArgsOptionParser parser = new ArgsOptionParser(this); 149 if (mKeyStoreFactory != null) { 150 parser.setKeyStore(mKeyStoreFactory.createKeyStoreClient()); 151 } 152 parser.parse(optionArgs); 153 } 154 String exitMode = "invocations"; 155 if (mHandoverPort == null) { 156 if (mExitOnEmpty) { 157 exitMode = "commands"; 158 mScheduler.shutdownOnEmpty(); 159 } else { 160 mScheduler.shutdown(); 161 } 162 } else { 163 if (!mScheduler.handoverShutdown(mHandoverPort)) { 164 // failure message should already be logged 165 return; 166 } 167 } 168 printLine("Signalling command scheduler for shutdown."); 169 printLine(String.format("TF will exit without warning when remaining %s complete.", 170 exitMode)); 171 } catch (ConfigurationException e) { 172 printLine(e.toString()); 173 } catch (KeyStoreException e) { 174 printLine(e.toString()); 175 } 176 } 177 } 178 179 /** 180 * Like {@link QuitRunnable}, but attempts to harshly shut down current invocations by 181 * killing the adb connection 182 */ 183 private class ForceQuitRunnable extends QuitRunnable { 184 @Override run(CaptureList args)185 public void run(CaptureList args) { 186 mScheduler.shutdownHard(); 187 } 188 } 189 190 /** 191 * Retrieve the {@link RegexTrie} that defines the console behavior. Exposed for unit testing. 192 */ getCommandTrie()193 RegexTrie<Runnable> getCommandTrie() { 194 return mCommandTrie; 195 } 196 197 /** 198 * Return a new ConsoleReader, or {@code null} if an IOException occurs. Note that this 199 * function must be static so that we can run it before the superclass constructor. 200 */ getReader()201 protected static ConsoleReader getReader() { 202 try { 203 if (sConsoleStream == null) { 204 final ConsoleReader reader = new ConsoleReader(); 205 sConsoleStream = new ConsoleReaderOutputStream(reader); 206 System.setOut(new PrintStream(sConsoleStream, true)); 207 } 208 return sConsoleStream.getConsoleReader(); 209 } catch (IOException e) { 210 System.err.format("Failed to initialize ConsoleReader: %s\n", e.getMessage()); 211 return null; 212 } 213 } 214 Console()215 protected Console() { 216 this(getReader()); 217 } 218 219 /** 220 * Create a {@link Console} with provided console reader. 221 * Also, set up console command handling. 222 * <p/> 223 * Exposed for unit testing 224 */ Console(ConsoleReader reader)225 Console(ConsoleReader reader) { 226 super("TfConsole"); 227 mConsoleStartTime = System.currentTimeMillis(); 228 mConsoleReader = reader; 229 if (reader != null) { 230 mConsoleReader.addCompletor( 231 new ConfigCompletor(getConfigurationFactory().getConfigList())); 232 } 233 234 List<String> genericHelp = new LinkedList<String>(); 235 Map<String, String> commandHelp = new LinkedHashMap<String, String>(); 236 addDefaultCommands(mCommandTrie, genericHelp, commandHelp); 237 setCustomCommands(mCommandTrie, genericHelp, commandHelp); 238 generateHelpListings(mCommandTrie, genericHelp, commandHelp); 239 } 240 setCommandScheduler(ICommandScheduler scheduler)241 void setCommandScheduler(ICommandScheduler scheduler) { 242 mScheduler = scheduler; 243 } 244 setKeyStoreFactory(IKeyStoreFactory factory)245 void setKeyStoreFactory(IKeyStoreFactory factory) { 246 mKeyStoreFactory = factory; 247 } 248 249 /** 250 * Register shutdown signals. 251 * 252 * <p>TSTP signal for quitting tradefed which waits all invocation finish. TERM signal for 253 * killing tradefed. We use TSTP and INT because these two signals are not used by JVM. 254 */ registerShutdownSignals()255 void registerShutdownSignals() { 256 Signal.handle( 257 new Signal("TSTP"), 258 new SignalHandler() { 259 @Override 260 public void handle(Signal sig) { 261 CLog.logAndDisplay( 262 LogLevel.INFO, 263 String.format("Received signal %s. Quit.", sig.getName())); 264 new QuitRunnable().run(new CaptureList()); 265 } 266 }); 267 Signal.handle( 268 new Signal("TERM"), 269 new SignalHandler() { 270 @Override 271 public void handle(Signal sig) { 272 CLog.logAndDisplay( 273 LogLevel.INFO, 274 String.format("Received signal %s. Kill.", sig.getName())); 275 new ForceQuitRunnable().run(new CaptureList()); 276 } 277 }); 278 } 279 280 /** 281 * A customization point that subclasses can use to alter which commands are available in the 282 * console. 283 * <p /> 284 * Implementations should modify the {@code genericHelp} and {@code commandHelp} variables to 285 * document what functionality they may have added, modified, or removed. 286 * 287 * @param trie The {@link RegexTrie} to add the commands to 288 * @param genericHelp A {@link List} of lines to print when the user runs the "help" command 289 * with no arguments. 290 * @param commandHelp A {@link Map} containing documentation for any new commands that may have 291 * been added. The key is a regular expression to use as a key for {@link RegexTrie}. 292 * The value should be a String containing the help text to print for that command. 293 */ setCustomCommands(RegexTrie<Runnable> trie, List<String> genericHelp, Map<String, String> commandHelp)294 protected void setCustomCommands(RegexTrie<Runnable> trie, List<String> genericHelp, 295 Map<String, String> commandHelp) { 296 // Meant to be overridden by subclasses 297 } 298 299 /** 300 * Generate help listings based on the contents of {@code genericHelp} and {@code commandHelp}. 301 * 302 * @param trie The {@link RegexTrie} to add the commands to 303 * @param genericHelp A {@link List} of lines to print when the user runs the "help" command 304 * with no arguments. 305 * @param commandHelp A {@link Map} containing documentation for any new commands that may have 306 * been added. The key is a regular expression to use as a key for {@link RegexTrie}. 307 * The value should be a String containing the help text to print for that command. 308 */ generateHelpListings(RegexTrie<Runnable> trie, List<String> genericHelp, Map<String, String> commandHelp)309 void generateHelpListings(RegexTrie<Runnable> trie, List<String> genericHelp, 310 Map<String, String> commandHelp) { 311 final String genHelpString = getGenericHelpString(genericHelp); 312 313 final ArgRunnable<CaptureList> genericHelpRunnable = new ArgRunnable<CaptureList>() { 314 @Override 315 public void run(CaptureList args) { 316 printLine(genHelpString); 317 } 318 }; 319 trie.put(genericHelpRunnable, HELP_PATTERN); 320 321 StringBuilder allHelpBuilder = new StringBuilder(); 322 323 // Add help entries for everything listed in the commandHelp map 324 for (Map.Entry<String, String> helpPair : commandHelp.entrySet()) { 325 final String key = helpPair.getKey(); 326 final String helpText = helpPair.getValue(); 327 328 trie.put(new Runnable() { 329 @Override 330 public void run() { 331 printLine(helpText); 332 } 333 }, HELP_PATTERN, key); 334 335 allHelpBuilder.append(helpText); 336 allHelpBuilder.append(LINE_SEPARATOR); 337 } 338 339 final String allHelpText = allHelpBuilder.toString(); 340 trie.put(new Runnable() { 341 @Override 342 public void run() { 343 printLine(allHelpText); 344 } 345 }, HELP_PATTERN, "all"); 346 347 // Add a generic "not found" help message for everything else 348 trie.put(new ArgRunnable<CaptureList>() { 349 @Override 350 public void run(CaptureList args) { 351 // Command will be the only capture in the second argument 352 // (first argument is helpPattern) 353 printLine(String.format( 354 "No help for '%s'; command is unknown or undocumented", 355 args.get(1).get(0))); 356 genericHelpRunnable.run(args); 357 } 358 }, HELP_PATTERN, null); 359 360 // Add a fallback input handler 361 trie.put(new ArgRunnable<CaptureList>() { 362 @Override 363 public void run(CaptureList args) { 364 if (args.isEmpty()) { 365 // User hit <Enter> with a blank line 366 return; 367 } 368 369 // Command will be the only capture in the first argument 370 printLine(String.format("Unknown command: '%s'", args.get(0).get(0))); 371 genericHelpRunnable.run(args); 372 } 373 }, (Pattern)null); 374 } 375 376 /** 377 * Return the generic help string to display 378 * 379 * @param genericHelp a list of {@link String} representing the generic help to be aggregated. 380 */ getGenericHelpString(List<String> genericHelp)381 protected String getGenericHelpString(List<String> genericHelp) { 382 return ArrayUtil.join(LINE_SEPARATOR, genericHelp); 383 } 384 385 /** 386 * A utility function to return the arguments that were passed to an {@link ArgRunnable}. In 387 * particular, it expects all first-level elements of {@code cl} after {@code argIdx} to be 388 * singleton {@link List}s. It will then coalesce the first element of each of those singleton 389 * {@link List}s as a single {@link List}. 390 * 391 * @param argIdx The zero-based index of the first argument. 392 * @param cl The {@link CaptureList} of arguments that was passed to the {@link ArgRunnable} 393 * @return A flattened {@link List} of arguments that were passed to the {@link ArgRunnable} 394 * @throws IllegalArgumentException if the data isn't formatted as expected 395 * @throws IndexOutOfBoundsException if {@code argIdx} isn't consistent with {@code cl} 396 */ getFlatArgs(int argIdx, CaptureList cl)397 static List<String> getFlatArgs(int argIdx, CaptureList cl) { 398 if (argIdx < 0 || argIdx >= cl.size()) { 399 throw new IndexOutOfBoundsException(String.format("argIdx is %d, cl size is %d", 400 argIdx, cl.size())); 401 } 402 403 List<String> flat = new ArrayList<String>(cl.size() - argIdx); 404 ListIterator<List<String>> iter = cl.listIterator(argIdx); 405 while (iter.hasNext()) { 406 List<String> single = iter.next(); 407 int len = single.size(); 408 if (len != 1) { 409 throw new IllegalArgumentException(String.format( 410 "Expected a singleton List, but got a List with %d elements: %s", 411 len, single.toString())); 412 } 413 flat.add(single.get(0)); 414 } 415 416 return flat; 417 } 418 419 /** 420 * Utility function to actually parse and execute a command file. 421 */ runCmdfile(String cmdfileName, List<String> extraArgs)422 void runCmdfile(String cmdfileName, List<String> extraArgs) { 423 try { 424 mScheduler.addCommandFile(cmdfileName, extraArgs); 425 } catch (ConfigurationException e) { 426 printLine(String.format("Failed to run %s: %s", cmdfileName, e)); 427 if (mScheduler.shouldShutdownOnCmdfileError()) { 428 printLine("shutdownOnCmdFileError is enabled, stopping TF"); 429 mScheduler.shutdown(); 430 } 431 } 432 } 433 434 /** 435 * Add commands to create the default Console experience 436 * <p /> 437 * Adds relevant documentation to {@code genericHelp} and {@code commandHelp}. 438 * 439 * @param trie The {@link RegexTrie} to add the commands to 440 * @param genericHelp A {@link List} of lines to print when the user runs the "help" command 441 * with no arguments. 442 * @param commandHelp A {@link Map} containing documentation for any new commands that may have 443 * been added. The key is a regular expression to use as a key for {@link RegexTrie}. 444 * The value should be a String containing the help text to print for that command. 445 */ addDefaultCommands(RegexTrie<Runnable> trie, List<String> genericHelp, Map<String, String> commandHelp)446 void addDefaultCommands(RegexTrie<Runnable> trie, List<String> genericHelp, 447 Map<String, String> commandHelp) { 448 449 450 // Help commands 451 genericHelp.add("Enter 'q' or 'exit' to exit. " + 452 "Use '--wait-for-command|-c' to exit only after all commands have executed."); 453 genericHelp.add("Enter 'kill' to attempt to forcibly exit, by shutting down adb"); 454 genericHelp.add(""); 455 genericHelp.add("Enter 'help all' to see all embedded documentation at once."); 456 genericHelp.add(""); 457 genericHelp.add("Enter 'help list' for help with 'list' commands"); 458 genericHelp.add("Enter 'help run' for help with 'run' commands"); 459 genericHelp.add("Enter 'help invocation' for help with 'invocation' commands"); 460 genericHelp.add("Enter 'help dump' for help with 'dump' commands"); 461 genericHelp.add("Enter 'help set' for help with 'set' commands"); 462 genericHelp.add("Enter 'help remove' for help with 'remove' commands"); 463 genericHelp.add("Enter 'help debug' for help with 'debug' commands"); 464 genericHelp.add("Enter 'version' to get the current version of Tradefed"); 465 466 commandHelp.put(LIST_PATTERN, String.format( 467 "%s help:" + LINE_SEPARATOR + 468 "\ti[nvocations] List all invocation threads" + LINE_SEPARATOR + 469 "\td[evices] List all detected or known devices" + LINE_SEPARATOR + 470 "\td[devices] all List all devices including placeholders" + LINE_SEPARATOR + 471 "\tc[ommands] List all commands currently waiting to be executed" + 472 LINE_SEPARATOR + 473 "\tc[ommands] [pattern] List all commands matching the pattern and currently " + 474 "waiting to be executed" + LINE_SEPARATOR + 475 "\tconfigs List all known configurations" + LINE_SEPARATOR, 476 LIST_PATTERN)); 477 478 commandHelp.put(DUMP_PATTERN, String.format( 479 "%s help:" + LINE_SEPARATOR + 480 "\ts[tack] Dump the stack traces of all threads" + LINE_SEPARATOR + 481 "\tl[ogs] Dump the logs of all invocations to files" + LINE_SEPARATOR + 482 "\tb[ugreport] Dump a bugreport for the running Tradefed instance" + 483 LINE_SEPARATOR + 484 "\tc[onfig] <config> Dump the content of the specified config" + LINE_SEPARATOR + 485 "\tcommandQueue Dump the contents of the commmand execution queue" + 486 LINE_SEPARATOR + 487 "\tcommands Dump all the config XML for the commands waiting to be " + 488 "executed" + LINE_SEPARATOR + 489 "\tcommands [pattern] Dump all the config XML for the commands matching the " + 490 "pattern and waiting to be executed" + LINE_SEPARATOR + 491 "\te[nv] Dump the environment variables available to test harness " + 492 "process" + LINE_SEPARATOR + 493 "\tu[ptime] Dump how long the TradeFed process has been running" + 494 LINE_SEPARATOR, 495 DUMP_PATTERN)); 496 497 commandHelp.put(RUN_PATTERN, String.format( 498 "%s help:" + LINE_SEPARATOR + 499 "\tcommand <config> [options] Run the specified command" + LINE_SEPARATOR + 500 "\t<config> [options] Shortcut for the above: run specified " + 501 "command" + LINE_SEPARATOR + 502 "\tcmdfile <cmdfile.txt> Run the specified commandfile" + 503 LINE_SEPARATOR + 504 "\tcommandAndExit <config> [options] Run the specified command, and run " + 505 "'exit -c' immediately afterward" + LINE_SEPARATOR + 506 "\tcmdfileAndExit <cmdfile.txt> Run the specified commandfile, and run " + 507 "'exit -c' immediately afterward" + LINE_SEPARATOR, 508 RUN_PATTERN)); 509 510 commandHelp.put(SET_PATTERN, String.format( 511 "%s help:" + LINE_SEPARATOR + 512 "\tlog-level-display <level> Sets the global display log level to <level>" + 513 LINE_SEPARATOR, 514 SET_PATTERN)); 515 516 commandHelp.put(REMOVE_PATTERN, String.format( 517 "%s help:" + LINE_SEPARATOR + 518 "\tremove allCommands Remove all commands currently waiting to be executed" + 519 LINE_SEPARATOR, 520 REMOVE_PATTERN)); 521 522 commandHelp.put(DEBUG_PATTERN, String.format( 523 "%s help:" + LINE_SEPARATOR + 524 "\tgc Attempt to force a GC" + LINE_SEPARATOR, 525 DEBUG_PATTERN)); 526 527 commandHelp.put(INVOC_PATTERN, String.format( 528 "%s help:" + LINE_SEPARATOR + 529 "\ti[nvocation] [Command Id] Information of the invocation thread" + 530 LINE_SEPARATOR + 531 "\ti[nvocation] [Command Id] stop Notify to stop the invocation" + LINE_SEPARATOR, 532 INVOC_PATTERN)); 533 534 // Handle quit commands 535 trie.put(new QuitRunnable(), EXIT_PATTERN, null); 536 trie.put(new QuitRunnable(), EXIT_PATTERN); 537 trie.put(new ForceQuitRunnable(), "kill"); 538 539 // List commands 540 trie.put(new Runnable() { 541 @Override 542 public void run() { 543 mScheduler.displayInvocationsInfo(new PrintWriter(System.out, true)); 544 } 545 }, LIST_PATTERN, "i(?:nvocations)?"); 546 trie.put(new Runnable() { 547 @Override 548 public void run() { 549 IDeviceManager manager = 550 GlobalConfiguration.getDeviceManagerInstance(); 551 manager.displayDevicesInfo(new PrintWriter(System.out, true), false); 552 } 553 }, LIST_PATTERN, "d(?:evices)?"); 554 trie.put(new Runnable() { 555 @Override 556 public void run() { 557 IDeviceManager manager = 558 GlobalConfiguration.getDeviceManagerInstance(); 559 manager.displayDevicesInfo(new PrintWriter(System.out, true), true); 560 } 561 }, LIST_PATTERN, "d(?:evices)?", "all"); 562 trie.put(new Runnable() { 563 @Override 564 public void run() { 565 mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), null); 566 } 567 }, LIST_PATTERN, LIST_COMMANDS_PATTERN); 568 ArgRunnable<CaptureList> listCmdRun = new ArgRunnable<CaptureList>() { 569 @Override 570 public void run(CaptureList args) { 571 // Skip 2 tokens to get past listPattern and "commands" 572 String pattern = args.get(2).get(0); 573 mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), pattern); 574 } 575 }; 576 trie.put(listCmdRun, LIST_PATTERN, LIST_COMMANDS_PATTERN, "(.*)"); 577 trie.put(new Runnable() { 578 @Override 579 public void run() { 580 printLine("Use 'run command <configuration_name> --help' to get list of options " 581 + "for a configuration"); 582 printLine("Use 'dump config <configuration_name>' to display the configuration's " 583 + "XML content."); 584 printLine(""); 585 printLine("Available configurations include:"); 586 getConfigurationFactory().printHelp(System.out); 587 } 588 }, LIST_PATTERN, "configs"); 589 590 // Invocation commands 591 trie.put(new ArgRunnable<CaptureList>() { 592 @Override 593 public void run(CaptureList args) { 594 int invocId = Integer.parseInt(args.get(1).get(0)); 595 String info = mScheduler.getInvocationInfo(invocId); 596 if (info != null) { 597 printLine(String.format("invocation %s: %s", invocId, info)); 598 } else { 599 printLine(String.format("No information found for invocation %s.", 600 invocId)); 601 } 602 } 603 }, INVOC_PATTERN, "([0-9]*)"); 604 trie.put(new ArgRunnable<CaptureList>() { 605 @Override 606 public void run(CaptureList args) { 607 int invocId = Integer.parseInt(args.get(1).get(0)); 608 if (mScheduler.stopInvocation(invocId)) { 609 printLine(String.format("Invocation %s has been requested to stop." 610 + " It may take some times.", 611 invocId)); 612 } else { 613 printLine(String.format("Could not stop invocation %s, try 'list " 614 + "invocation' or 'invocation %s' for more information.", 615 invocId, invocId)); 616 } 617 } 618 }, INVOC_PATTERN, "([0-9]*)", "stop"); 619 620 // Dump commands 621 trie.put(new Runnable() { 622 @Override 623 public void run() { 624 dumpStacks(System.out); 625 } 626 }, DUMP_PATTERN, "s(?:tacks?)?"); 627 trie.put(new Runnable() { 628 @Override 629 public void run() { 630 dumpLogs(); 631 } 632 }, DUMP_PATTERN, "l(?:ogs?)?"); 633 trie.put(new Runnable() { 634 @Override 635 public void run() { 636 dumpTfBugreport(); 637 } 638 }, DUMP_PATTERN, "b(?:ugreport?)?"); 639 trie.put(new Runnable() { 640 @Override 641 public void run() { 642 printElapsedTime(); 643 } 644 }, DUMP_PATTERN, "u(?:ptime?)?"); 645 ArgRunnable<CaptureList> dumpConfigRun = new ArgRunnable<CaptureList>() { 646 @Override 647 public void run(CaptureList args) { 648 // Skip 2 tokens to get past dumpPattern and "config" 649 String configArg = args.get(2).get(0); 650 getConfigurationFactory().dumpConfig(configArg, System.out); 651 } 652 }; 653 trie.put(dumpConfigRun, DUMP_PATTERN, "c(?:onfig?)?", "(.*)"); 654 655 trie.put(new Runnable() { 656 @Override 657 public void run() { 658 mScheduler.displayCommandQueue(new PrintWriter(System.out, true)); 659 } 660 }, DUMP_PATTERN, "commandQueue"); 661 662 trie.put(new Runnable() { 663 @Override 664 public void run() { 665 mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), null); 666 } 667 }, DUMP_PATTERN, LIST_COMMANDS_PATTERN); 668 ArgRunnable<CaptureList> dumpCmdRun = new ArgRunnable<CaptureList>() { 669 @Override 670 public void run(CaptureList args) { 671 // Skip 2 tokens to get past listPattern and "commands" 672 String pattern = args.get(2).get(0); 673 mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), pattern); 674 } 675 }; 676 trie.put(dumpCmdRun, DUMP_PATTERN, LIST_COMMANDS_PATTERN, "(.*)"); 677 678 trie.put(new Runnable() { 679 @Override 680 public void run() { 681 dumpEnv(); 682 } 683 }, DUMP_PATTERN, "e(?:nv)?"); 684 685 // Run commands 686 ArgRunnable<CaptureList> runRunCommand = 687 new ArgRunnable<CaptureList>() { 688 @Override 689 public void run(CaptureList args) { 690 // The second argument "command" may also be missing, if the 691 // caller used the shortcut. 692 int startIdx = 1; 693 if (args.get(1).isEmpty()) { 694 // Empty array (that is, not even containing an empty string) means that 695 // we matched and skipped /(?:singleC|c)ommand/ 696 startIdx = 2; 697 } 698 699 String[] flatArgs = new String[args.size() - startIdx]; 700 for (int i = startIdx; i < args.size(); i++) { 701 flatArgs[i - startIdx] = args.get(i).get(0); 702 } 703 try { 704 mScheduler.addCommand(flatArgs); 705 } catch (ConfigurationException e) { 706 printLine( 707 String.format( 708 "Failed to run command: %s\n%s", 709 e.toString(), StreamUtil.getStackTrace(e))); 710 } 711 } 712 }; 713 trie.put(runRunCommand, RUN_PATTERN, "c(?:ommand)?", null); 714 trie.put(runRunCommand, RUN_PATTERN, null); 715 trie.put(new Runnable() { 716 @Override 717 public void run() { 718 String version = VersionParser.fetchVersion(); 719 if (version != null) { 720 printLine(version); 721 } else { 722 printLine("Failed to fetch version information for Tradefed."); 723 } 724 } 725 }, VERSION_PATTERN); 726 727 ArgRunnable<CaptureList> runAndExitCommand = new ArgRunnable<CaptureList>() { 728 @Override 729 public void run(CaptureList args) { 730 // Skip 2 tokens to get past runPattern and "singleCommand" 731 String[] flatArgs = new String[args.size() - 2]; 732 for (int i = 2; i < args.size(); i++) { 733 flatArgs[i - 2] = args.get(i).get(0); 734 } 735 try { 736 if (mScheduler.addCommand(flatArgs)) { 737 mScheduler.shutdownOnEmpty(); 738 } 739 } catch (ConfigurationException e) { 740 printLine("Failed to run command: " + e.toString()); 741 } 742 743 // Intentionally kill the console before CommandScheduler finishes 744 mShouldExit = true; 745 } 746 }; 747 trie.put(runAndExitCommand, RUN_PATTERN, "s(?:ingleCommand)?", null); 748 trie.put(runAndExitCommand, RUN_PATTERN, "commandAndExit", null); 749 750 // Missing required argument: show help 751 // FIXME: fix this functionality 752 // trie.put(runHelpRun, runPattern, "(?:singleC|c)ommand"); 753 754 final ArgRunnable<CaptureList> runRunCmdfile = new ArgRunnable<CaptureList>() { 755 @Override 756 public void run(CaptureList args) { 757 // Skip 2 tokens to get past runPattern and "cmdfile". We're guaranteed to have at 758 // least 3 tokens if we got #run. 759 int startIdx = 2; 760 List<String> flatArgs = getFlatArgs(startIdx, args); 761 String file = flatArgs.get(0); 762 List<String> extraArgs = flatArgs.subList(1, flatArgs.size()); 763 printLine(String.format("Attempting to run cmdfile %s with args %s", file, 764 extraArgs.toString())); 765 runCmdfile(file, extraArgs); 766 } 767 }; 768 trie.put(runRunCmdfile, RUN_PATTERN, "cmdfile", "(.*)"); 769 trie.put(runRunCmdfile, RUN_PATTERN, "cmdfile", "(.*)", null); 770 771 ArgRunnable<CaptureList> runRunCmdfileAndExit = new ArgRunnable<CaptureList>() { 772 @Override 773 public void run(CaptureList args) { 774 runRunCmdfile.run(args); 775 mScheduler.shutdownOnEmpty(); 776 } 777 }; 778 trie.put(runRunCmdfileAndExit, RUN_PATTERN, "cmdfileAndExit", "(.*)"); 779 trie.put(runRunCmdfileAndExit, RUN_PATTERN, "cmdfileAndExit", "(.*)", null); 780 781 ArgRunnable<CaptureList> runRunAllCmdfilesAndExit = new ArgRunnable<CaptureList>() { 782 @Override 783 public void run(CaptureList args) { 784 // skip 2 tokens to get past runPattern and "allCmdfilesAndExit" 785 if (args.size() <= 2) { 786 printLine("No cmdfiles specified!"); 787 } else { 788 // Each group should have exactly one element, given how the null wildcard 789 // operates; so we flatten them. 790 for (String cmdfile : getFlatArgs(2 /* startIdx */, args)) { 791 runCmdfile(cmdfile, new ArrayList<String>(0)); 792 } 793 } 794 mScheduler.shutdownOnEmpty(); 795 } 796 }; 797 trie.put(runRunAllCmdfilesAndExit, RUN_PATTERN, "allCmdfilesAndExit"); 798 trie.put(runRunAllCmdfilesAndExit, RUN_PATTERN, "allCmdfilesAndExit", null); 799 800 // Missing required argument: show help 801 // FIXME: fix this functionality 802 //trie.put(runHelpRun, runPattern, "cmdfile"); 803 804 // Set commands 805 ArgRunnable<CaptureList> runSetLog = new ArgRunnable<CaptureList>() { 806 @Override 807 public void run(CaptureList args) { 808 // Skip 2 tokens to get past "set" and "log-level-display" 809 String logLevelStr = args.get(2).get(0); 810 LogLevel newLogLevel = LogLevel.getByString(logLevelStr); 811 LogLevel currentLogLevel = LogRegistry.getLogRegistry().getGlobalLogDisplayLevel(); 812 if (newLogLevel != null) { 813 LogRegistry.getLogRegistry().setGlobalLogDisplayLevel(newLogLevel); 814 // Make sure that the level was set. 815 currentLogLevel = LogRegistry.getLogRegistry().getGlobalLogDisplayLevel(); 816 if (currentLogLevel != null) { 817 printLine(String.format("Log level now set to '%s'.", currentLogLevel)); 818 } 819 } else { 820 if (currentLogLevel == null) { 821 printLine(String.format("Invalid log level '%s'.", newLogLevel)); 822 } else{ 823 printLine(String.format( 824 "Invalid log level '%s'; log level remains at '%s'.", 825 newLogLevel, currentLogLevel)); 826 } 827 } 828 } 829 }; 830 trie.put(runSetLog, SET_PATTERN, "log-level-display", "(.*)"); 831 832 // Debug commands 833 trie.put(new Runnable() { 834 @Override 835 public void run() { 836 System.gc(); 837 } 838 }, DEBUG_PATTERN, "gc"); 839 840 // Remove commands 841 trie.put(new Runnable() { 842 @Override 843 public void run() { 844 mScheduler.removeAllCommands(); 845 } 846 }, REMOVE_PATTERN, "allCommands"); 847 } 848 849 /** 850 * Print the uptime of the Tradefed process. 851 */ printElapsedTime()852 private void printElapsedTime() { 853 long elapsedTime = System.currentTimeMillis() - mConsoleStartTime; 854 String elapsed = String.format("TF has been running for %s", 855 TimeUtil.formatElapsedTime(elapsedTime)); 856 printLine(elapsed); 857 } 858 859 /** 860 * Get input from the console 861 * 862 * @return A {@link String} containing the input to parse and run. Will return {@code null} if 863 * console is not available or user entered EOF ({@code ^D}). 864 */ 865 @VisibleForTesting getConsoleInput()866 String getConsoleInput() throws IOException { 867 if (mConsoleReader != null) { 868 if (sConsoleStream != null) { 869 // While we're reading the console, the only tasks which will print to the console 870 // are asynchronous. In particular, after this point, we assume that the last line 871 // on the screen is the command prompt. 872 sConsoleStream.setAsyncMode(); 873 } 874 875 final String input = mConsoleReader.readLine(getConsolePrompt()); 876 877 if (sConsoleStream != null) { 878 // The opposite of the above. From here on out, we should expect that the 879 // command prompt is _not_ the most recent line on the screen. In particular, while 880 // synchronous tasks are running, sConsoleStream will avoid redisplaying the command 881 // prompt. 882 sConsoleStream.setSyncMode(); 883 } 884 return input; 885 } else { 886 return null; 887 } 888 } 889 890 /** 891 * @return the text {@link String} to display for the console prompt 892 */ getConsolePrompt()893 protected String getConsolePrompt() { 894 return CONSOLE_PROMPT; 895 } 896 897 /** 898 * Display a line of text on console 899 * @param output 900 */ printLine(String output)901 protected void printLine(String output) { 902 System.out.print(output); 903 System.out.println(); 904 } 905 906 /** 907 * Print the line to a Printwriter 908 * @param output 909 */ printLine(String output, PrintStream pw)910 protected void printLine(String output, PrintStream pw) { 911 pw.print(output); 912 pw.println(); 913 } 914 915 /** 916 * Execute a command. 917 * <p /> 918 * Exposed for unit testing 919 */ 920 @SuppressWarnings("unchecked") executeCmdRunnable(Runnable command, CaptureList groups)921 void executeCmdRunnable(Runnable command, CaptureList groups) { 922 try { 923 if (command instanceof ArgRunnable) { 924 // FIXME: verify that command implements ArgRunnable<CaptureList> instead 925 // FIXME: of just ArgRunnable 926 ((ArgRunnable<CaptureList>) command).run(groups); 927 } else { 928 command.run(); 929 } 930 } catch (RuntimeException e) { 931 e.printStackTrace(); 932 } 933 } 934 935 /** 936 * Return whether we should expect the console to be usable. 937 * <p /> 938 * Exposed for unit testing. 939 */ isConsoleFunctional()940 boolean isConsoleFunctional() { 941 return System.console() != null; 942 } 943 944 /** 945 * The main method to launch the console. Will keep running until shutdown command is issued. 946 */ 947 @Override run()948 public void run() { 949 List<String> arrrgs = mMainArgs; 950 951 if (mScheduler == null) { 952 throw new IllegalStateException("command scheduler hasn't been set"); 953 } 954 955 try { 956 // Check System.console() since jline doesn't seem to consistently know whether or not 957 // the console is functional. 958 if (!isConsoleFunctional()) { 959 if (arrrgs.isEmpty()) { 960 printLine("No commands for non-interactive mode; exiting."); 961 // FIXME: need to run the scheduler here so that the things blocking on it 962 // FIXME: will be released. 963 mScheduler.start(); 964 mScheduler.await(); 965 return; 966 } else { 967 printLine("Non-interactive mode: Running initial command then exiting."); 968 mShouldExit = true; 969 } 970 } 971 972 // Wait for the CommandScheduler to start. It will hold the JVM open (since the Console 973 // thread is a Daemon thread), and also we require it to have started so that we can 974 // start processing user input. 975 mScheduler.start(); 976 mScheduler.await(); 977 978 String input = ""; 979 CaptureList groups = new CaptureList(); 980 String[] tokens; 981 982 // Note: since Console is a daemon thread, the JVM may exit without us actually leaving 983 // this read loop. This is by design. 984 do { 985 if (arrrgs.isEmpty()) { 986 input = getConsoleInput(); 987 988 if (input == null) { 989 // Usually the result of getting EOF on the console 990 printLine(""); 991 printLine("Received EOF; quitting..."); 992 mShouldExit = true; 993 break; 994 } 995 996 tokens = null; 997 try { 998 tokens = QuotationAwareTokenizer.tokenizeLine(input); 999 } catch (IllegalArgumentException e) { 1000 printLine(String.format("Invalid input: %s.", input)); 1001 continue; 1002 } 1003 1004 if (tokens == null || tokens.length == 0) { 1005 continue; 1006 } 1007 } else { 1008 printLine(String.format("Using commandline arguments as starting command: %s", 1009 arrrgs)); 1010 if (mConsoleReader != null) { 1011 // Add the starting command as the first item in the console history 1012 // FIXME: this will not properly escape commands that were properly escaped 1013 // FIXME: on the commandline. That said, it will still be more convenient 1014 // FIXME: than copying by hand. 1015 final String cmd = ArrayUtil.join(" ", arrrgs); 1016 mConsoleReader.getHistory().addToHistory(cmd); 1017 } 1018 tokens = arrrgs.toArray(new String[0]); 1019 if (arrrgs.get(0).matches(HELP_PATTERN)) { 1020 // if started from command line for help, return to shell 1021 mShouldExit = true; 1022 } 1023 arrrgs = Collections.emptyList(); 1024 } 1025 1026 Runnable command = mCommandTrie.retrieve(groups, tokens); 1027 if (command != null) { 1028 executeCmdRunnable(command, groups); 1029 } else { 1030 printLine(String.format( 1031 "Unable to handle command '%s'. Enter 'help' for help.", tokens[0])); 1032 } 1033 RunUtil.getDefault().sleep(100); 1034 } while (!mShouldExit); 1035 } catch (Exception e) { 1036 printLine("Console received an unexpected exception (shown below); shutting down TF."); 1037 e.printStackTrace(); 1038 } finally { 1039 mScheduler.shutdown(); 1040 GlobalConfiguration.getInstance().cleanup(); 1041 // Make sure that we don't quit with messages still in the buffers 1042 System.err.flush(); 1043 System.out.flush(); 1044 } 1045 } 1046 1047 /** 1048 * set the flag to exit the console. 1049 */ 1050 @VisibleForTesting exitConsole()1051 void exitConsole() { 1052 mShouldExit = true; 1053 } 1054 awaitScheduler()1055 void awaitScheduler() throws InterruptedException { 1056 mScheduler.await(); 1057 } 1058 1059 /** 1060 * Method for getting a {@link IConfigurationFactory}. 1061 * <p/> 1062 * Exposed for unit testing. 1063 */ getConfigurationFactory()1064 IConfigurationFactory getConfigurationFactory() { 1065 return ConfigurationFactory.getInstance(); 1066 } 1067 dumpStacks(PrintStream ps)1068 private void dumpStacks(PrintStream ps) { 1069 Map<Thread, StackTraceElement[]> threadMap = Thread.getAllStackTraces(); 1070 for (Map.Entry<Thread, StackTraceElement[]> threadEntry : threadMap.entrySet()) { 1071 dumpThreadStack(threadEntry.getKey(), threadEntry.getValue(), ps); 1072 } 1073 } 1074 dumpThreadStack(Thread thread, StackTraceElement[] trace, PrintStream ps)1075 private void dumpThreadStack(Thread thread, StackTraceElement[] trace, PrintStream ps) { 1076 printLine(String.format("%s", thread), ps); 1077 for (int i=0; i < trace.length; i++) { 1078 printLine(String.format("\t%s", trace[i]), ps); 1079 } 1080 printLine("", ps); 1081 } 1082 dumpLogs()1083 private void dumpLogs() { 1084 LogRegistry.getLogRegistry().dumpLogs(); 1085 } 1086 1087 /** 1088 * Dumps the environment variables to console, sorted by variable names 1089 */ dumpEnv()1090 private void dumpEnv() { 1091 // use TreeMap to sort variables by name 1092 Map<String, String> env = new TreeMap<>(System.getenv()); 1093 for (Map.Entry<String, String> entry : env.entrySet()) { 1094 printLine(String.format("\t%s=%s", entry.getKey(), entry.getValue())); 1095 } 1096 } 1097 1098 /** 1099 * Dump a Tradefed Bugreport containing the stack traces and logs. 1100 */ dumpTfBugreport()1101 private void dumpTfBugreport() { 1102 File tmpBugreportDir = null; 1103 PrintStream ps = null; 1104 try { 1105 // dump stacks 1106 tmpBugreportDir = FileUtil.createNamedTempDir("bugreport_tf"); 1107 File tmpStackFile = FileUtil.createTempFile("dump_stacks_", ".log", tmpBugreportDir); 1108 ps = new PrintStream(tmpStackFile); 1109 dumpStacks(ps); 1110 ps.flush(); 1111 // dump logs 1112 ((LogRegistry)LogRegistry.getLogRegistry()).dumpLogsToDir(tmpBugreportDir); 1113 // add them to a zip and log. 1114 File zippedBugreport = ZipUtil.createZip(tmpBugreportDir, "tradefed_bugreport_"); 1115 printLine(String.format("Output bugreport zip in %s", 1116 zippedBugreport.getAbsolutePath())); 1117 } catch (IOException io) { 1118 printLine("Error when trying to dump bugreport"); 1119 } finally { 1120 ps.close(); 1121 FileUtil.recursiveDelete(tmpBugreportDir); 1122 } 1123 } 1124 1125 /** 1126 * Sets the console starting arguments. 1127 * 1128 * @param mainArgs the arguments 1129 */ setArgs(List<String> mainArgs)1130 public void setArgs(List<String> mainArgs) { 1131 mMainArgs = mainArgs; 1132 } 1133 main(final String[] mainArgs)1134 public static void main(final String[] mainArgs) throws InterruptedException, 1135 ConfigurationException { 1136 Console console = new Console(); 1137 startConsole(console, mainArgs); 1138 } 1139 1140 /** 1141 * Starts the given Tradefed console with given args 1142 * 1143 * @param console the {@link Console} to start 1144 * @param args the command line arguments 1145 */ startConsole(Console console, String[] args)1146 public static void startConsole(Console console, String[] args) throws InterruptedException, 1147 ConfigurationException { 1148 ClearcutClient client = new ClearcutClient(); 1149 Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client)); 1150 client.notifyTradefedStartEvent(); 1151 1152 List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args); 1153 GlobalConfiguration.getInstance().setup(); 1154 console.setArgs(nonGlobalArgs); 1155 console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler()); 1156 console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory()); 1157 console.setDaemon(true); 1158 1159 GlobalConfiguration.getInstance().getCommandScheduler().setClearcutClient(client); 1160 1161 console.start(); 1162 1163 // Wait for the CommandScheduler to get started before we exit the main thread. See full 1164 // explanation near the top of #run() 1165 console.awaitScheduler(); 1166 console.registerShutdownSignals(); 1167 } 1168 } 1169