| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tradefed.command; |
| |
| import com.android.ddmlib.Log.LogLevel; |
| import com.android.tradefed.config.ArgsOptionParser; |
| import com.android.tradefed.config.ConfigurationException; |
| import com.android.tradefed.config.ConfigurationFactory; |
| import com.android.tradefed.config.GlobalConfiguration; |
| import com.android.tradefed.config.IConfigurationFactory; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.device.IDeviceManager; |
| import com.android.tradefed.log.ConsoleReaderOutputStream; |
| import com.android.tradefed.log.LogRegistry; |
| import com.android.tradefed.util.ArrayUtil; |
| import com.android.tradefed.util.QuotationAwareTokenizer; |
| import com.android.tradefed.util.RegexTrie; |
| import com.android.tradefed.util.RunUtil; |
| import com.android.tradefed.util.VersionParser; |
| |
| import jline.ConsoleReader; |
| |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Main TradeFederation console providing user with the interface to interact |
| * <p/> |
| * Currently supports operations such as |
| * <ul> |
| * <li>add a command to test |
| * <li>list devices and their state |
| * <li>list invocations in progress |
| * <li>list commands in queue |
| * <li>dump invocation log to file/stdout |
| * <li>shutdown |
| * </ul> |
| */ |
| public class Console extends Thread { |
| |
| private static final String CONSOLE_PROMPT = "tf >"; |
| |
| protected static final String HELP_PATTERN = "\\?|h|help"; |
| protected static final String LIST_PATTERN = "l(?:ist)?"; |
| protected static final String DUMP_PATTERN = "d(?:ump)?"; |
| protected static final String RUN_PATTERN = "r(?:un)?"; |
| protected static final String EXIT_PATTERN = "(?:q|exit)"; |
| protected static final String SET_PATTERN = "s(?:et)?"; |
| protected static final String VERSION_PATTERN = "version"; |
| protected static final String REMOVE_PATTERN = "remove"; |
| protected static final String DEBUG_PATTERN = "debug"; |
| |
| protected static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
| |
| private static ConsoleReaderOutputStream sConsoleStream = null; |
| |
| protected ICommandScheduler mScheduler; |
| protected ConsoleReader mConsoleReader; |
| private RegexTrie<Runnable> mCommandTrie = new RegexTrie<Runnable>(); |
| private boolean mShouldExit = false; |
| private List<String> mMainArgs = new ArrayList<String>(0); |
| |
| /** A convenience type for <code>{@literal List<List<String>>}</code> */ |
| @SuppressWarnings("serial") |
| protected static class CaptureList extends LinkedList<List<String>> { |
| CaptureList() { |
| super(); |
| } |
| |
| CaptureList(Collection<? extends List<String>> c) { |
| super(c); |
| } |
| } |
| |
| /** |
| * A {@link Runnable} with a {@code run} method that can take an argument |
| */ |
| protected abstract static class ArgRunnable<T> implements Runnable { |
| @Override |
| public void run() { |
| run(null); |
| } |
| |
| abstract public void run(T args); |
| } |
| |
| /** |
| * This is a sentinel class that will cause TF to shut down. This enables a user to get TF to |
| * shut down via the RegexTrie input handling mechanism. |
| */ |
| private class QuitRunnable extends ArgRunnable<CaptureList> { |
| @Option(name = "handover-port", description = |
| "Used to indicate that currently managed devices should be 'handed over' to new " + |
| "tradefed process, which is listening on specified port") |
| private Integer mHandoverPort = null; |
| |
| @Option(name = "wait-for-commands", shortName = 'c', description = |
| "only exit after all commands have executed ") |
| private boolean mExitOnEmpty = false; |
| |
| |
| @Override |
| public void run(CaptureList args) { |
| try { |
| if (args.size() >= 2 && !args.get(1).isEmpty()) { |
| List<String> optionArgs = getFlatArgs(1, args); |
| ArgsOptionParser parser = new ArgsOptionParser(this); |
| parser.parse(optionArgs); |
| } |
| |
| String exitMode = "invocations"; |
| if (mHandoverPort == null) { |
| if (mExitOnEmpty) { |
| exitMode = "commands"; |
| mScheduler.shutdownOnEmpty(); |
| } else { |
| mScheduler.shutdown(); |
| } |
| } else { |
| if (!mScheduler.handoverShutdown(mHandoverPort)) { |
| // failure message should already be logged |
| return; |
| } |
| } |
| printLine("Signalling command scheduler for shutdown."); |
| printLine(String.format("TF will exit without warning when remaining %s complete.", |
| exitMode)); |
| } catch (ConfigurationException e) { |
| printLine(e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Like {@link QuitRunnable}, but attempts to harshly shut down current invocations by |
| * killing the adb connection |
| */ |
| private class ForceQuitRunnable extends QuitRunnable { |
| @Override |
| public void run(CaptureList args) { |
| super.run(args); |
| mScheduler.shutdownHard(); |
| } |
| } |
| |
| /** |
| * Retrieve the {@link RegexTrie} that defines the console behavior. Exposed for unit testing. |
| */ |
| RegexTrie<Runnable> getCommandTrie() { |
| return mCommandTrie; |
| } |
| |
| /** |
| * Return a new ConsoleReader, or {@code null} if an IOException occurs. Note that this |
| * function must be static so that we can run it before the superclass constructor. |
| */ |
| protected static ConsoleReader getReader() { |
| try { |
| if (sConsoleStream == null) { |
| final ConsoleReader reader = new ConsoleReader(); |
| sConsoleStream = new ConsoleReaderOutputStream(reader); |
| System.setOut(new PrintStream(sConsoleStream)); |
| } |
| return sConsoleStream.getConsoleReader(); |
| } catch (IOException e) { |
| System.err.format("Failed to initialize ConsoleReader: %s\n", e.getMessage()); |
| return null; |
| } |
| } |
| |
| protected Console() { |
| this(getReader()); |
| } |
| |
| /** |
| * Create a {@link Console} with provided console reader. |
| * Also, set up console command handling. |
| * <p/> |
| * Exposed for unit testing |
| */ |
| Console(ConsoleReader reader) { |
| super(); |
| mConsoleReader = reader; |
| |
| List<String> genericHelp = new LinkedList<String>(); |
| Map<String, String> commandHelp = new LinkedHashMap<String, String>(); |
| addDefaultCommands(mCommandTrie, genericHelp, commandHelp); |
| setCustomCommands(mCommandTrie, genericHelp, commandHelp); |
| generateHelpListings(mCommandTrie, genericHelp, commandHelp); |
| } |
| |
| void setCommandScheduler(ICommandScheduler scheduler) { |
| mScheduler = scheduler; |
| } |
| |
| /** |
| * A customization point that subclasses can use to alter which commands are available in the |
| * console. |
| * <p /> |
| * Implementations should modify the {@code genericHelp} and {@code commandHelp} variables to |
| * document what functionality they may have added, modified, or removed. |
| * |
| * @param trie The {@link RegexTrie} to add the commands to |
| * @param genericHelp A {@link List} of lines to print when the user runs the "help" command |
| * with no arguments. |
| * @param commandHelp A {@link Map} containing documentation for any new commands that may have |
| * been added. The key is a regular expression to use as a key for {@link RegexTrie}. |
| * The value should be a String containing the help text to print for that command. |
| */ |
| protected void setCustomCommands(RegexTrie<Runnable> trie, List<String> genericHelp, |
| Map<String, String> commandHelp) { |
| // Meant to be overridden by subclasses |
| } |
| |
| /** |
| * Generate help listings based on the contents of {@code genericHelp} and {@code commandHelp}. |
| * |
| * @param trie The {@link RegexTrie} to add the commands to |
| * @param genericHelp A {@link List} of lines to print when the user runs the "help" command |
| * with no arguments. |
| * @param commandHelp A {@link Map} containing documentation for any new commands that may have |
| * been added. The key is a regular expression to use as a key for {@link RegexTrie}. |
| * The value should be a String containing the help text to print for that command. |
| */ |
| void generateHelpListings(RegexTrie<Runnable> trie, List<String> genericHelp, |
| Map<String, String> commandHelp) { |
| final String genHelpString = getGenericHelpString(genericHelp); |
| final String helpPattern = "\\?|h|help"; |
| |
| final ArgRunnable<CaptureList> genericHelpRunnable = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| printLine(genHelpString); |
| } |
| }; |
| trie.put(genericHelpRunnable, helpPattern); |
| |
| StringBuilder allHelpBuilder = new StringBuilder(); |
| |
| // Add help entries for everything listed in the commandHelp map |
| for (Map.Entry<String, String> helpPair : commandHelp.entrySet()) { |
| final String key = helpPair.getKey(); |
| final String helpText = helpPair.getValue(); |
| |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| printLine(helpText); |
| } |
| }, helpPattern, key); |
| |
| allHelpBuilder.append(helpText); |
| allHelpBuilder.append(LINE_SEPARATOR); |
| } |
| |
| final String allHelpText = allHelpBuilder.toString(); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| printLine(allHelpText); |
| } |
| }, helpPattern, "all"); |
| |
| // Add a generic "not found" help message for everything else |
| trie.put(new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // Command will be the only capture in the second argument |
| // (first argument is helpPattern) |
| printLine(String.format( |
| "No help for '%s'; command is unknown or undocumented", |
| args.get(1).get(0))); |
| genericHelpRunnable.run(args); |
| } |
| }, helpPattern, null); |
| |
| // Add a fallback input handler |
| trie.put(new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| if (args.isEmpty()) { |
| // User hit <Enter> with a blank line |
| return; |
| } |
| |
| // Command will be the only capture in the first argument |
| printLine(String.format("Unknown command: '%s'", args.get(0).get(0))); |
| genericHelpRunnable.run(args); |
| } |
| }, (Pattern)null); |
| } |
| |
| /** |
| * Return the generic help string to display |
| * @param genericHelp |
| * @return |
| */ |
| protected String getGenericHelpString(List<String> genericHelp) { |
| return ArrayUtil.join(LINE_SEPARATOR, genericHelp); |
| } |
| |
| /** |
| * A utility function to return the arguments that were passed to an {@link ArgRunnable}. In |
| * particular, it expects all first-level elements of {@code cl} after {@code argIdx} to be |
| * singleton {@link List}s. It will then coalesce the first element of each of those singleton |
| * {@link List}s as a single {@link List}. |
| * |
| * @param argIdx The zero-based index of the first argument. |
| * @param cl The {@link CaptureList} of arguments that was passed to the {@link ArgRunnable} |
| * @return A flattened {@link List} of arguments that were passed to the {@link ArgRunnable} |
| * @throws IllegalArgumentException if the data isn't formatted as expected |
| * @throws IndexOutOfBoundsException if {@code argIdx} isn't consistent with {@code cl} |
| */ |
| static List<String> getFlatArgs(int argIdx, CaptureList cl) { |
| if (argIdx < 0 || argIdx >= cl.size()) { |
| throw new IndexOutOfBoundsException(String.format("argIdx is %d, cl size is %d", |
| argIdx, cl.size())); |
| } |
| |
| List<String> flat = new ArrayList<String>(cl.size() - argIdx); |
| ListIterator<List<String>> iter = cl.listIterator(argIdx); |
| while (iter.hasNext()) { |
| List<String> single = iter.next(); |
| int len = single.size(); |
| if (len != 1) { |
| throw new IllegalArgumentException(String.format( |
| "Expected a singleton List, but got a List with %d elements: %s", |
| len, single.toString())); |
| } |
| flat.add(single.get(0)); |
| } |
| |
| return flat; |
| } |
| |
| /** |
| * Utility function to actually parse and execute a command file. |
| */ |
| void runCmdfile(String cmdfileName, List<String> extraArgs) { |
| try { |
| mScheduler.addCommandFile(cmdfileName, extraArgs); |
| } catch (ConfigurationException e) { |
| printLine(String.format("Failed to run %s: %s", cmdfileName, e)); |
| } |
| } |
| |
| /** |
| * Add commands to create the default Console experience |
| * <p /> |
| * Adds relevant documentation to {@code genericHelp} and {@code commandHelp}. |
| * |
| * @param trie The {@link RegexTrie} to add the commands to |
| * @param genericHelp A {@link List} of lines to print when the user runs the "help" command |
| * with no arguments. |
| * @param commandHelp A {@link Map} containing documentation for any new commands that may have |
| * been added. The key is a regular expression to use as a key for {@link RegexTrie}. |
| * The value should be a String containing the help text to print for that command. |
| */ |
| void addDefaultCommands(RegexTrie<Runnable> trie, List<String> genericHelp, |
| Map<String, String> commandHelp) { |
| |
| |
| // Help commands |
| genericHelp.add("Enter 'q' or 'exit' to exit. " + |
| "Use '--wait-for-command|-c' to exit only after all commands have executed."); |
| genericHelp.add("Enter 'kill' to attempt to forcibly exit, by shutting down adb"); |
| genericHelp.add(""); |
| genericHelp.add("Enter 'help all' to see all embedded documentation at once."); |
| genericHelp.add(""); |
| genericHelp.add("Enter 'help list' for help with 'list' commands"); |
| genericHelp.add("Enter 'help run' for help with 'run' commands"); |
| genericHelp.add("Enter 'help dump' for help with 'dump' commands"); |
| genericHelp.add("Enter 'help set' for help with 'set' commands"); |
| genericHelp.add("Enter 'help remove' for help with 'remove' commands"); |
| genericHelp.add("Enter 'help debug' for help with 'debug' commands"); |
| genericHelp.add("Enter 'version' to get the current version of Tradefed"); |
| |
| commandHelp.put(LIST_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\ti[nvocations] List all invocation threads" + LINE_SEPARATOR + |
| "\td[evices] List all detected or known devices" + LINE_SEPARATOR + |
| "\tc[ommands] List all commands currently waiting to be executed" + |
| LINE_SEPARATOR + |
| "\tconfigs List all known configurations" + |
| LINE_SEPARATOR, LIST_PATTERN)); |
| |
| commandHelp.put(DUMP_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\ts[tack] Dump the stack traces of all threads" + LINE_SEPARATOR + |
| "\tl[ogs] Dump the logs of all invocations to files" + LINE_SEPARATOR + |
| "\tc[onfig] <config> Dump the content of the specified config" + LINE_SEPARATOR + |
| "\tcommandQueue Dump the contents of the commmand execution queue" + |
| LINE_SEPARATOR, |
| DUMP_PATTERN)); |
| |
| commandHelp.put(RUN_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\tcommand <config> [options] Run the specified command" + LINE_SEPARATOR + |
| "\t<config> [options] Shortcut for the above: run specified command" + |
| LINE_SEPARATOR + |
| "\tcmdfile <cmdfile.txt> Run the specified commandfile" + LINE_SEPARATOR + |
| "\tcommandAndExit <config> [options] Run the specified command, and run " + |
| "'exit -c' immediately afterward" + LINE_SEPARATOR, |
| "\tcmdfileAndExit <cmdfile.txt> Run the specified commandfile, and run " + |
| "'exit -c' immediately afterward" + LINE_SEPARATOR, |
| RUN_PATTERN)); |
| |
| commandHelp.put(SET_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\tlog-level-display <level> Sets the global display log level to <level>" + |
| LINE_SEPARATOR, |
| SET_PATTERN)); |
| |
| commandHelp.put(REMOVE_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\tremove allCommands Remove all commands currently waiting to be executed" |
| + LINE_SEPARATOR, |
| REMOVE_PATTERN)); |
| |
| commandHelp.put(DEBUG_PATTERN, String.format( |
| "%s help:" + LINE_SEPARATOR + |
| "\tgc Attempt to force a GC" + LINE_SEPARATOR, |
| DEBUG_PATTERN)); |
| |
| // Handle quit commands |
| trie.put(new QuitRunnable(), EXIT_PATTERN, null); |
| trie.put(new QuitRunnable(), EXIT_PATTERN); |
| trie.put(new ForceQuitRunnable(), "kill"); |
| |
| // List commands |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| mScheduler.displayInvocationsInfo(new PrintWriter(System.out, true)); |
| } |
| }, LIST_PATTERN, "i(?:nvocations)?"); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| IDeviceManager manager = |
| GlobalConfiguration.getDeviceManagerInstance(); |
| manager.displayDevicesInfo(new PrintWriter(System.out, true)); |
| } |
| }, LIST_PATTERN, "d(?:evices)?"); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| mScheduler.displayCommandsInfo(new PrintWriter(System.out, true)); |
| } |
| }, LIST_PATTERN, "c(?:ommands)?"); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| getConfigurationFactory().printHelp(System.out); |
| } |
| }, LIST_PATTERN, "configs"); |
| |
| |
| // Dump commands |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| dumpStacks(); |
| } |
| }, DUMP_PATTERN, "s(?:tacks?)?"); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| dumpLogs(); |
| } |
| }, DUMP_PATTERN, "l(?:ogs?)?"); |
| ArgRunnable<CaptureList> dumpConfigRun = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // Skip 2 tokens to get past dumpPattern and "config" |
| String configArg = args.get(2).get(0); |
| getConfigurationFactory().dumpConfig(configArg, System.out); |
| } |
| }; |
| trie.put(dumpConfigRun, DUMP_PATTERN, "c(?:onfig?)?", "(.*)"); |
| |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| mScheduler.displayCommandQueue(new PrintWriter(System.out, true)); |
| } |
| }, DUMP_PATTERN, "commandQueue"); |
| |
| // Run commands |
| ArgRunnable<CaptureList> runRunCommand = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // The second argument "command" may also be missing, if the |
| // caller used the shortcut. |
| int startIdx = 1; |
| if (args.get(1).isEmpty()) { |
| // Empty array (that is, not even containing an empty string) means that |
| // we matched and skipped /(?:singleC|c)ommand/ |
| startIdx = 2; |
| } |
| |
| String[] flatArgs = new String[args.size() - startIdx]; |
| for (int i = startIdx; i < args.size(); i++) { |
| flatArgs[i - startIdx] = args.get(i).get(0); |
| } |
| try { |
| mScheduler.addCommand(flatArgs); |
| } catch (ConfigurationException e) { |
| printLine("Failed to run command: " + e.toString()); |
| } |
| } |
| }; |
| trie.put(runRunCommand, RUN_PATTERN, "c(?:ommand)?", null); |
| trie.put(runRunCommand, RUN_PATTERN, null); |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| String version = VersionParser.fetchVersion(); |
| if (version != null) { |
| printLine(version); |
| } else { |
| printLine("Failed to fetch version information for Tradefed."); |
| } |
| } |
| }, VERSION_PATTERN); |
| |
| ArgRunnable<CaptureList> runAndExitCommand = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // Skip 2 tokens to get past runPattern and "singleCommand" |
| String[] flatArgs = new String[args.size() - 2]; |
| for (int i = 2; i < args.size(); i++) { |
| flatArgs[i - 2] = args.get(i).get(0); |
| } |
| try { |
| if (mScheduler.addCommand(flatArgs)) { |
| mScheduler.shutdownOnEmpty(); |
| } |
| } catch (ConfigurationException e) { |
| printLine("Failed to run command: " + e.toString()); |
| } |
| |
| // Intentionally kill the console before CommandScheduler finishes |
| mShouldExit = true; |
| } |
| }; |
| trie.put(runAndExitCommand, RUN_PATTERN, "s(?:ingleCommand)?", null); |
| trie.put(runAndExitCommand, RUN_PATTERN, "commandAndExit", null); |
| |
| // Missing required argument: show help |
| // FIXME: fix this functionality |
| // trie.put(runHelpRun, runPattern, "(?:singleC|c)ommand"); |
| |
| final ArgRunnable<CaptureList> runRunCmdfile = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // Skip 2 tokens to get past runPattern and "cmdfile". We're guaranteed to have at |
| // least 3 tokens if we got #run. |
| int startIdx = 2; |
| List<String> flatArgs = getFlatArgs(startIdx, args); |
| String file = flatArgs.get(0); |
| List<String> extraArgs = flatArgs.subList(1, flatArgs.size()); |
| printLine(String.format("Attempting to run cmdfile %s with args %s", file, |
| extraArgs.toString())); |
| runCmdfile(file, extraArgs); |
| } |
| }; |
| trie.put(runRunCmdfile, RUN_PATTERN, "cmdfile", "(.*)"); |
| trie.put(runRunCmdfile, RUN_PATTERN, "cmdfile", "(.*)", null); |
| |
| ArgRunnable<CaptureList> runRunCmdfileAndExit = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| runRunCmdfile.run(args); |
| mScheduler.shutdownOnEmpty(); |
| } |
| }; |
| trie.put(runRunCmdfileAndExit, RUN_PATTERN, "cmdfileAndExit", "(.*)"); |
| trie.put(runRunCmdfileAndExit, RUN_PATTERN, "cmdfileAndExit", "(.*)", null); |
| |
| ArgRunnable<CaptureList> runRunAllCmdfilesAndExit = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // skip 2 tokens to get past runPattern and "allCmdfilesAndExit" |
| if (args.size() <= 2) { |
| printLine("No cmdfiles specified!"); |
| } else { |
| // Each group should have exactly one element, given how the null wildcard |
| // operates; so we flatten them. |
| for (String cmdfile : getFlatArgs(2 /* startIdx */, args)) { |
| runCmdfile(cmdfile, new ArrayList<String>(0)); |
| } |
| } |
| mScheduler.shutdownOnEmpty(); |
| } |
| }; |
| trie.put(runRunAllCmdfilesAndExit, RUN_PATTERN, "allCmdfilesAndExit"); |
| trie.put(runRunAllCmdfilesAndExit, RUN_PATTERN, "allCmdfilesAndExit", null); |
| |
| // Missing required argument: show help |
| // FIXME: fix this functionality |
| //trie.put(runHelpRun, runPattern, "cmdfile"); |
| |
| // Set commands |
| ArgRunnable<CaptureList> runSetLog = new ArgRunnable<CaptureList>() { |
| @Override |
| public void run(CaptureList args) { |
| // Skip 2 tokens to get past "set" and "log-level-display" |
| String logLevelStr = args.get(2).get(0); |
| LogLevel newLogLevel = LogLevel.getByString(logLevelStr); |
| LogLevel currentLogLevel = LogRegistry.getLogRegistry().getGlobalLogDisplayLevel(); |
| if (newLogLevel != null) { |
| LogRegistry.getLogRegistry().setGlobalLogDisplayLevel(newLogLevel); |
| // Make sure that the level was set. |
| currentLogLevel = LogRegistry.getLogRegistry().getGlobalLogDisplayLevel(); |
| if (currentLogLevel != null) { |
| printLine(String.format("Log level now set to '%s'.", currentLogLevel)); |
| } |
| } else { |
| if (currentLogLevel == null) { |
| printLine(String.format("Invalid log level '%s'.", newLogLevel)); |
| } else{ |
| printLine(String.format( |
| "Invalid log level '%s'; log level remains at '%s'.", |
| newLogLevel, currentLogLevel)); |
| } |
| } |
| } |
| }; |
| trie.put(runSetLog, SET_PATTERN, "log-level-display", "(.*)"); |
| |
| // Debug commands |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| System.gc(); |
| } |
| }, DEBUG_PATTERN, "gc"); |
| |
| // Remove commands |
| trie.put(new Runnable() { |
| @Override |
| public void run() { |
| mScheduler.removeAllCommands(); |
| } |
| }, REMOVE_PATTERN, "allCommands"); |
| } |
| |
| /** |
| * Get input from the console |
| * |
| * @return A {@link String} containing the input to parse and run. Will return {@code null} if |
| * console is not available or user entered EOF ({@code ^D}). |
| */ |
| private String getConsoleInput() throws IOException { |
| if (mConsoleReader != null) { |
| if (sConsoleStream != null) { |
| // While we're reading the console, the only tasks which will print to the console |
| // are asynchronous. In particular, after this point, we assume that the last line |
| // on the screen is the command prompt. |
| sConsoleStream.setAsyncMode(); |
| } |
| |
| final String input = mConsoleReader.readLine(getConsolePrompt()); |
| |
| if (sConsoleStream != null) { |
| // The opposite of the above. From here on out, we should expect that the |
| // command prompt is _not_ the most recent line on the screen. In particular, while |
| // synchronous tasks are running, sConsoleStream will avoid redisplaying the command |
| // prompt. |
| sConsoleStream.setSyncMode(); |
| } |
| return input; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * @return the text {@link String} to display for the console prompt |
| */ |
| protected String getConsolePrompt() { |
| return CONSOLE_PROMPT; |
| } |
| |
| /** |
| * Display a line of text on console |
| * @param output |
| */ |
| protected void printLine(String output) { |
| System.out.print(output); |
| System.out.println(); |
| } |
| |
| /** |
| * Execute a command. |
| * <p /> |
| * Exposed for unit testing |
| */ |
| @SuppressWarnings("unchecked") |
| void executeCmdRunnable(Runnable command, CaptureList groups) { |
| if (command instanceof ArgRunnable) { |
| // FIXME: verify that command implements ArgRunnable<CaptureList> instead |
| // FIXME: of just ArgRunnable |
| ((ArgRunnable<CaptureList>)command).run(groups); |
| } else { |
| command.run(); |
| } |
| } |
| |
| /** |
| * Return whether we should expect the console to be usable. |
| * <p /> |
| * Exposed for unit testing. |
| */ |
| boolean isConsoleFunctional() { |
| return System.console() != null; |
| } |
| |
| /** |
| * The main method to launch the console. Will keep running until shutdown command is issued. |
| */ |
| @Override |
| public void run() { |
| List<String> arrrgs = mMainArgs; |
| |
| if (mScheduler == null) { |
| throw new IllegalStateException("command scheduler hasn't been set"); |
| } |
| |
| try { |
| // Check System.console() since jline doesn't seem to consistently know whether or not |
| // the console is functional. |
| if (!isConsoleFunctional()) { |
| if (arrrgs.isEmpty()) { |
| printLine("No commands for non-interactive mode; exiting."); |
| // FIXME: need to run the scheduler here so that the things blocking on it |
| // FIXME: will be released. |
| mScheduler.start(); |
| mScheduler.await(); |
| return; |
| } else { |
| printLine("Non-interactive mode: Running initial command then exiting."); |
| mShouldExit = true; |
| } |
| } |
| |
| // Wait for the CommandScheduler to start. It will hold the JVM open (since the Console |
| // thread is a Daemon thread), and also we require it to have started so that we can |
| // start processing user input. |
| mScheduler.start(); |
| mScheduler.await(); |
| |
| String input = ""; |
| CaptureList groups = new CaptureList(); |
| String[] tokens; |
| |
| // Note: since Console is a daemon thread, the JVM may exit without us actually leaving |
| // this read loop. This is by design. |
| do { |
| if (arrrgs.isEmpty()) { |
| input = getConsoleInput(); |
| |
| if (input == null) { |
| // Usually the result of getting EOF on the console |
| printLine(""); |
| printLine("Received EOF; quitting..."); |
| mShouldExit = true; |
| break; |
| } |
| |
| tokens = null; |
| try { |
| tokens = QuotationAwareTokenizer.tokenizeLine(input); |
| } catch (IllegalArgumentException e) { |
| printLine(String.format("Invalid input: %s.", input)); |
| continue; |
| } |
| |
| if (tokens == null || tokens.length == 0) { |
| continue; |
| } |
| } else { |
| printLine(String.format("Using commandline arguments as starting command: %s", |
| arrrgs)); |
| if (mConsoleReader != null) { |
| // Add the starting command as the first item in the console history |
| // FIXME: this will not properly escape commands that were properly escaped |
| // FIXME: on the commandline. That said, it will still be more convenient |
| // FIXME: than copying by hand. |
| final String cmd = ArrayUtil.join(" ", arrrgs); |
| mConsoleReader.getHistory().addToHistory(cmd); |
| } |
| tokens = arrrgs.toArray(new String[0]); |
| arrrgs = Collections.emptyList(); |
| } |
| |
| Runnable command = mCommandTrie.retrieve(groups, tokens); |
| if (command != null) { |
| executeCmdRunnable(command, groups); |
| } else { |
| printLine(String.format( |
| "Unable to handle command '%s'. Enter 'help' for help.", tokens[0])); |
| } |
| |
| RunUtil.getDefault().sleep(100); |
| } while (!mShouldExit); |
| } catch (Exception e) { |
| printLine("Console received an unexpected exception (shown below); shutting down TF."); |
| e.printStackTrace(); |
| } finally { |
| mScheduler.shutdown(); |
| // Make sure that we don't quit with messages still in the buffers |
| System.err.flush(); |
| System.out.flush(); |
| } |
| } |
| |
| void awaitScheduler() throws InterruptedException { |
| mScheduler.await(); |
| } |
| |
| /** |
| * Method for getting a {@link IConfigurationFactory}. |
| * <p/> |
| * Exposed for unit testing. |
| */ |
| IConfigurationFactory getConfigurationFactory() { |
| return ConfigurationFactory.getInstance(); |
| } |
| |
| private void dumpStacks() { |
| Map<Thread, StackTraceElement[]> threadMap = Thread.getAllStackTraces(); |
| for (Map.Entry<Thread, StackTraceElement[]> threadEntry : threadMap.entrySet()) { |
| dumpThreadStack(threadEntry.getKey(), threadEntry.getValue()); |
| } |
| } |
| |
| private void dumpThreadStack(Thread thread, StackTraceElement[] trace) { |
| printLine(String.format("%s", thread)); |
| for (int i=0; i < trace.length; i++) { |
| printLine(String.format("\t%s", trace[i])); |
| } |
| printLine(""); |
| } |
| |
| private void dumpLogs() { |
| LogRegistry.getLogRegistry().dumpLogs(); |
| } |
| |
| /** |
| * Sets the console starting arguments. |
| * |
| * @param mainArgs the arguments |
| */ |
| public void setArgs(List<String> mainArgs) { |
| mMainArgs = mainArgs; |
| } |
| |
| public static void main(final String[] mainArgs) throws InterruptedException, |
| ConfigurationException { |
| Console console = new Console(); |
| startConsole(console, mainArgs); |
| } |
| |
| /** |
| * Starts the given tradefed console with given args |
| * |
| * @param console the {@link Console} to start |
| * @param args the command line arguments |
| */ |
| public static void startConsole(Console console, String[] args) throws InterruptedException, |
| ConfigurationException { |
| List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args); |
| |
| console.setArgs(nonGlobalArgs); |
| console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler()); |
| console.setDaemon(true); |
| console.start(); |
| |
| // Wait for the CommandScheduler to get started before we exit the main thread. See full |
| // explanation near the top of #run() |
| console.awaitScheduler(); |
| } |
| } |