Home | History | Annotate | Download | only in command
      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         try {
    877             if (command instanceof ArgRunnable) {
    878                 // FIXME: verify that command implements ArgRunnable<CaptureList> instead
    879                 // FIXME: of just ArgRunnable
    880                 ((ArgRunnable<CaptureList>) command).run(groups);
    881             } else {
    882                 command.run();
    883             }
    884         } catch (RuntimeException e) {
    885             e.printStackTrace();
    886         }
    887     }
    888 
    889     /**
    890      * Return whether we should expect the console to be usable.
    891      * <p />
    892      * Exposed for unit testing.
    893      */
    894     boolean isConsoleFunctional() {
    895         return System.console() != null;
    896     }
    897 
    898     /**
    899      * The main method to launch the console. Will keep running until shutdown command is issued.
    900      */
    901     @Override
    902     public void run() {
    903         List<String> arrrgs = mMainArgs;
    904 
    905         if (mScheduler == null) {
    906             throw new IllegalStateException("command scheduler hasn't been set");
    907         }
    908 
    909         try {
    910             // Check System.console() since jline doesn't seem to consistently know whether or not
    911             // the console is functional.
    912             if (!isConsoleFunctional()) {
    913                 if (arrrgs.isEmpty()) {
    914                     printLine("No commands for non-interactive mode; exiting.");
    915                     // FIXME: need to run the scheduler here so that the things blocking on it
    916                     // FIXME: will be released.
    917                     mScheduler.start();
    918                     mScheduler.await();
    919                     return;
    920                 } else {
    921                     printLine("Non-interactive mode: Running initial command then exiting.");
    922                     mShouldExit = true;
    923                 }
    924             }
    925 
    926             // Wait for the CommandScheduler to start.  It will hold the JVM open (since the Console
    927             // thread is a Daemon thread), and also we require it to have started so that we can
    928             // start processing user input.
    929             mScheduler.start();
    930             mScheduler.await();
    931 
    932             String input = "";
    933             CaptureList groups = new CaptureList();
    934             String[] tokens;
    935 
    936             // Note: since Console is a daemon thread, the JVM may exit without us actually leaving
    937             // this read loop.  This is by design.
    938             do {
    939                 if (arrrgs.isEmpty()) {
    940                     input = getConsoleInput();
    941 
    942                     if (input == null) {
    943                         // Usually the result of getting EOF on the console
    944                         printLine("");
    945                         printLine("Received EOF; quitting...");
    946                         mShouldExit = true;
    947                         break;
    948                     }
    949 
    950                     tokens = null;
    951                     try {
    952                         tokens = QuotationAwareTokenizer.tokenizeLine(input);
    953                     } catch (IllegalArgumentException e) {
    954                         printLine(String.format("Invalid input: %s.", input));
    955                         continue;
    956                     }
    957 
    958                     if (tokens == null || tokens.length == 0) {
    959                         continue;
    960                     }
    961                 } else {
    962                     printLine(String.format("Using commandline arguments as starting command: %s",
    963                             arrrgs));
    964                     if (mConsoleReader != null) {
    965                         // Add the starting command as the first item in the console history
    966                         // FIXME: this will not properly escape commands that were properly escaped
    967                         // FIXME: on the commandline.  That said, it will still be more convenient
    968                         // FIXME: than copying by hand.
    969                         final String cmd = ArrayUtil.join(" ", arrrgs);
    970                         mConsoleReader.getHistory().addToHistory(cmd);
    971                     }
    972                     tokens = arrrgs.toArray(new String[0]);
    973                     if (arrrgs.get(0).matches(HELP_PATTERN)) {
    974                         // if started from command line for help, return to shell
    975                         mShouldExit = true;
    976                     }
    977                     arrrgs = Collections.emptyList();
    978                 }
    979 
    980                 Runnable command = mCommandTrie.retrieve(groups, tokens);
    981                 if (command != null) {
    982                     executeCmdRunnable(command, groups);
    983                 } else {
    984                     printLine(String.format(
    985                             "Unable to handle command '%s'.  Enter 'help' for help.", tokens[0]));
    986                 }
    987                 RunUtil.getDefault().sleep(100);
    988             } while (!mShouldExit);
    989         } catch (Exception e) {
    990             printLine("Console received an unexpected exception (shown below); shutting down TF.");
    991             e.printStackTrace();
    992         } finally {
    993             mScheduler.shutdown();
    994             // Make sure that we don't quit with messages still in the buffers
    995             System.err.flush();
    996             System.out.flush();
    997         }
    998     }
    999 
   1000     /**
   1001      * set the flag to exit the console.
   1002      */
   1003     @VisibleForTesting
   1004     void exitConsole() {
   1005         mShouldExit = true;
   1006     }
   1007 
   1008     void awaitScheduler() throws InterruptedException {
   1009         mScheduler.await();
   1010     }
   1011 
   1012     /**
   1013      * Method for getting a {@link IConfigurationFactory}.
   1014      * <p/>
   1015      * Exposed for unit testing.
   1016      */
   1017     IConfigurationFactory getConfigurationFactory() {
   1018         return ConfigurationFactory.getInstance();
   1019     }
   1020 
   1021     private void dumpStacks(PrintStream ps) {
   1022         Map<Thread, StackTraceElement[]> threadMap = Thread.getAllStackTraces();
   1023         for (Map.Entry<Thread, StackTraceElement[]> threadEntry : threadMap.entrySet()) {
   1024             dumpThreadStack(threadEntry.getKey(), threadEntry.getValue(), ps);
   1025         }
   1026     }
   1027 
   1028     private void dumpThreadStack(Thread thread, StackTraceElement[] trace, PrintStream ps) {
   1029         printLine(String.format("%s", thread), ps);
   1030         for (int i=0; i < trace.length; i++) {
   1031             printLine(String.format("\t%s", trace[i]), ps);
   1032         }
   1033         printLine("", ps);
   1034     }
   1035 
   1036     private void dumpLogs() {
   1037         LogRegistry.getLogRegistry().dumpLogs();
   1038     }
   1039 
   1040     /**
   1041      * Dumps the environment variables to console, sorted by variable names
   1042      */
   1043     private void dumpEnv() {
   1044         // use TreeMap to sort variables by name
   1045         Map<String, String> env = new TreeMap<>(System.getenv());
   1046         for (Map.Entry<String, String> entry : env.entrySet()) {
   1047             printLine(String.format("\t%s=%s", entry.getKey(), entry.getValue()));
   1048         }
   1049     }
   1050 
   1051     /**
   1052      * Dump a Tradefed Bugreport containing the stack traces and logs.
   1053      */
   1054     private void dumpTfBugreport() {
   1055         File tmpBugreportDir = null;
   1056         PrintStream ps = null;
   1057         try {
   1058             // dump stacks
   1059             tmpBugreportDir = FileUtil.createNamedTempDir("bugreport_tf");
   1060             File tmpStackFile = FileUtil.createTempFile("dump_stacks_", ".log", tmpBugreportDir);
   1061             ps = new PrintStream(tmpStackFile);
   1062             dumpStacks(ps);
   1063             ps.flush();
   1064             // dump logs
   1065             ((LogRegistry)LogRegistry.getLogRegistry()).dumpLogsToDir(tmpBugreportDir);
   1066             // add them to a zip and log.
   1067             File zippedBugreport = ZipUtil.createZip(tmpBugreportDir, "tradefed_bugreport_");
   1068             printLine(String.format("Output bugreport zip in %s",
   1069                     zippedBugreport.getAbsolutePath()));
   1070         } catch (IOException io) {
   1071             printLine("Error when trying to dump bugreport");
   1072         } finally {
   1073             ps.close();
   1074             FileUtil.recursiveDelete(tmpBugreportDir);
   1075         }
   1076     }
   1077 
   1078     /**
   1079      * Sets the console starting arguments.
   1080      *
   1081      * @param mainArgs the arguments
   1082      */
   1083     public void setArgs(List<String> mainArgs) {
   1084         mMainArgs = mainArgs;
   1085     }
   1086 
   1087     public static void main(final String[] mainArgs) throws InterruptedException,
   1088             ConfigurationException {
   1089         Console console = new Console();
   1090         startConsole(console, mainArgs);
   1091     }
   1092 
   1093     /**
   1094      * Starts the given Tradefed console with given args
   1095      *
   1096      * @param console the {@link Console} to start
   1097      * @param args the command line arguments
   1098      */
   1099     public static void startConsole(Console console, String[] args) throws InterruptedException,
   1100             ConfigurationException {
   1101         List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args);
   1102 
   1103         console.setArgs(nonGlobalArgs);
   1104         console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler());
   1105         console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory());
   1106         console.setDaemon(true);
   1107         console.start();
   1108 
   1109         // Wait for the CommandScheduler to get started before we exit the main thread.  See full
   1110         // explanation near the top of #run()
   1111         console.awaitScheduler();
   1112     }
   1113 }
   1114