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