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