blob: 352700fc1c963e35f835834542ef7843ee851412 [file] [log] [blame]
/*
* 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.clearcut.ClearcutClient;
import com.android.tradefed.clearcut.TerminateClearcutClient;
import com.android.tradefed.command.CommandRunner.ExitCode;
import com.android.tradefed.command.console.ConfigCompleter;
import com.android.tradefed.command.console.ConsoleReaderOutputStream;
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.config.proxy.AutomatedReporters;
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInvocation;
import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.proto.FileProtoResultReporter;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.service.TradefedFeatureServer;
import com.android.tradefed.service.management.DeviceManagementGrpcServer;
import com.android.tradefed.service.management.TestInvocationManagementServer;
import com.android.tradefed.testtype.suite.TestSuiteInfo;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RegexTrie;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.TimeUtil;
import com.android.tradefed.util.VersionParser;
import com.android.tradefed.util.ZipUtil;
import com.android.tradefed.util.keystore.IKeyStoreFactory;
import com.android.tradefed.util.keystore.KeyStoreException;
import com.google.common.annotations.VisibleForTesting;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.impl.history.DefaultHistory;
import org.jline.terminal.TerminalBuilder;
import java.io.File;
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.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import sun.misc.Signal;
import sun.misc.SignalHandler;
/**
* 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 APPNAME = "Tradefed";
private static final String CONSOLE_PROMPT = "\u001B[0;32mtf >\u001B[0;0m";
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 INVOC_PATTERN = "i(?:nvocation)?";
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 LIST_COMMANDS_PATTERN = "c(?:ommands)?";
protected static final String LINE_SEPARATOR = System.getProperty("line.separator");
private static ConsoleReaderOutputStream sConsoleStream = null;
protected ICommandScheduler mScheduler;
protected IKeyStoreFactory mKeyStoreFactory;
protected LineReader mConsoleReader;
private RegexTrie<Runnable> mCommandTrie = new RegexTrie<Runnable>();
private AtomicBoolean mShouldExit = new AtomicBoolean(false);
private List<String> mMainArgs = new ArrayList<String>(0);
private long mConsoleStartTime;
/** 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);
}
public abstract 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 = "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);
if (mKeyStoreFactory != null) {
parser.setKeyStore(mKeyStoreFactory.createKeyStoreClient());
}
parser.parse(optionArgs);
}
String exitMode = "invocations";
if (mExitOnEmpty) {
exitMode = "commands";
mScheduler.shutdownOnEmpty();
} else {
mScheduler.shutdown(true);
}
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());
} catch (KeyStoreException 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) {
mScheduler.shutdownHard();
}
}
/**
* Retrieve the {@link RegexTrie} that defines the console behavior. Exposed for unit testing.
*/
RegexTrie<Runnable> getCommandTrie() {
return mCommandTrie;
}
/**
* Return a new LineReader, 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 LineReader getReader() {
try {
if (sConsoleStream == null) {
final LineReader reader =
LineReaderBuilder.builder()
.appName(APPNAME)
.terminal(TerminalBuilder.builder().system(true).dumb(true).build())
.completer(
new ConfigCompleter(
ConfigurationFactory.getInstance().getConfigList()))
.history(new DefaultHistory())
.build();
sConsoleStream = new ConsoleReaderOutputStream(reader);
System.setOut(new PrintStream(sConsoleStream, true));
}
return sConsoleStream.getConsoleReader();
} catch (IOException e) {
System.err.format("Failed to initialize LineReader: %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(LineReader reader) {
super("TfConsole");
mConsoleStartTime = System.currentTimeMillis();
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;
}
void setKeyStoreFactory(IKeyStoreFactory factory) {
mKeyStoreFactory = factory;
}
/**
* Register shutdown signals.
*
* <p>TSTP signal for quitting tradefed which waits all invocation finish. TERM signal for
* killing tradefed. We use TSTP and INT because these two signals are not used by JVM.
*/
void registerShutdownSignals() {
Signal.handle(
new Signal("TSTP"),
new SignalHandler() {
@Override
public void handle(Signal sig) {
CLog.logAndDisplay(
LogLevel.INFO,
String.format("Received signal %s. Quit.", sig.getName()));
new QuitRunnable().run(new CaptureList());
}
});
Signal.handle(
new Signal("TERM"),
new SignalHandler() {
@Override
public void handle(Signal sig) {
CLog.logAndDisplay(
LogLevel.INFO,
String.format("Received signal %s. Kill.", sig.getName()));
new ForceQuitRunnable().run(new CaptureList());
}
});
}
/**
* 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 ArgRunnable<CaptureList> genericHelpRunnable =
new ArgRunnable<CaptureList>() {
@Override
public void run(CaptureList args) {
printLine(genHelpString);
}
};
trie.put(genericHelpRunnable, HELP_PATTERN);
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);
}
},
HELP_PATTERN,
key);
allHelpBuilder.append(helpText);
allHelpBuilder.append(LINE_SEPARATOR);
}
final String allHelpText = allHelpBuilder.toString();
trie.put(
new Runnable() {
@Override
public void run() {
printLine(allHelpText);
}
},
HELP_PATTERN,
"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);
}
},
HELP_PATTERN,
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 a list of {@link String} representing the generic help to be aggregated.
*/
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));
if (mScheduler.shouldShutdownOnCmdfileError()) {
printLine("shutdownOnCmdFileError is enabled, stopping TF");
mScheduler.shutdown();
}
reportProtoResults(e);
}
}
private void reportProtoResults(Exception e) {
String protoRes = System.getenv(AutomatedReporters.PROTO_REPORTING_FILE);
if (protoRes == null) {
printLine(
String.format(
"No %s specified to output results",
AutomatedReporters.PROTO_REPORTING_FILE));
return;
}
if (new File(protoRes).exists()) {
printLine(String.format("File %s already exists", protoRes));
return;
}
FileProtoResultReporter reporter = new FileProtoResultReporter();
reporter.setOutputFile(new File(protoRes));
reporter.setDelimitedOutput(false);
reporter.invocationStarted(new InvocationContext());
FailureDescription failure =
TestInvocation.createFailureFromException(e, FailureStatus.INFRA_FAILURE);
reporter.invocationFailed(failure);
reporter.invocationEnded(0L);
}
/**
* 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 invocation' for help with 'invocation' 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
+ "\td[devices] all List all devices including placeholders"
+ LINE_SEPARATOR
+ "\tc[ommands] List all commands currently waiting to"
+ " be executed"
+ LINE_SEPARATOR
+ "\tc[ommands] [pattern] List all commands matching the pattern"
+ " and 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
+ "\tb[ugreport] Dump a bugreport for the running Tradefed"
+ " instance"
+ 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
+ "\tcommands Dump all the config XML for the commands"
+ " waiting to be executed"
+ LINE_SEPARATOR
+ "\tcommands [pattern] Dump all the config XML for the commands"
+ " matching the pattern and waiting to be executed"
+ LINE_SEPARATOR
+ "\te[nv] Dump the environment variables available"
+ " to test harness process"
+ LINE_SEPARATOR
+ "\tu[ptime] Dump how long the TradeFed process has"
+ " been running"
+ 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));
commandHelp.put(
INVOC_PATTERN,
String.format(
"%s help:"
+ LINE_SEPARATOR
+ "\ti[nvocation] [Command Id] Information of the"
+ " invocation thread"
+ LINE_SEPARATOR
+ "\ti[nvocation] [Command Id] stop Notify to stop the invocation"
+ LINE_SEPARATOR,
INVOC_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), false);
}
},
LIST_PATTERN,
"d(?:evices)?");
trie.put(
new Runnable() {
@Override
public void run() {
IDeviceManager manager = GlobalConfiguration.getDeviceManagerInstance();
manager.displayDevicesInfo(new PrintWriter(System.out, true), true);
}
},
LIST_PATTERN,
"d(?:evices)?",
"all");
trie.put(
new Runnable() {
@Override
public void run() {
mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), null);
}
},
LIST_PATTERN,
LIST_COMMANDS_PATTERN);
ArgRunnable<CaptureList> listCmdRun =
new ArgRunnable<CaptureList>() {
@Override
public void run(CaptureList args) {
// Skip 2 tokens to get past listPattern and "commands"
String pattern = args.get(2).get(0);
mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), pattern);
}
};
trie.put(listCmdRun, LIST_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
trie.put(
new Runnable() {
@Override
public void run() {
printLine(
"Use 'run command <configuration_name> --help' to get list of"
+ " options for a configuration");
printLine(
"Use 'dump config <configuration_name>' to display the"
+ " configuration's XML content.");
printLine("");
printLine("Available configurations include:");
getConfigurationFactory().printHelp(System.out);
}
},
LIST_PATTERN,
"configs");
// Invocation commands
trie.put(
new ArgRunnable<CaptureList>() {
@Override
public void run(CaptureList args) {
int invocId = Integer.parseInt(args.get(1).get(0));
String info = mScheduler.getInvocationInfo(invocId);
if (info != null) {
printLine(String.format("invocation %s: %s", invocId, info));
} else {
printLine(
String.format(
"No information found for invocation %s.", invocId));
}
}
},
INVOC_PATTERN,
"([0-9]*)");
trie.put(
new ArgRunnable<CaptureList>() {
@Override
public void run(CaptureList args) {
int invocId = Integer.parseInt(args.get(1).get(0));
if (mScheduler.stopInvocation(invocId)) {
printLine(
String.format(
"Invocation %s has been requested to stop."
+ " It may take some times.",
invocId));
} else {
printLine(
String.format(
"Could not stop invocation %s, try 'list invocation'"
+ " or 'invocation %s' for more information.",
invocId, invocId));
}
}
},
INVOC_PATTERN,
"([0-9]*)",
"stop");
// Dump commands
trie.put(
new Runnable() {
@Override
public void run() {
dumpStacks(System.out);
}
},
DUMP_PATTERN,
"s(?:tacks?)?");
trie.put(
new Runnable() {
@Override
public void run() {
dumpLogs();
}
},
DUMP_PATTERN,
"l(?:ogs?)?");
trie.put(
new Runnable() {
@Override
public void run() {
dumpTfBugreport();
}
},
DUMP_PATTERN,
"b(?:ugreport?)?");
trie.put(
new Runnable() {
@Override
public void run() {
printElapsedTime();
}
},
DUMP_PATTERN,
"u(?:ptime?)?");
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");
trie.put(
new Runnable() {
@Override
public void run() {
mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), null);
}
},
DUMP_PATTERN,
LIST_COMMANDS_PATTERN);
ArgRunnable<CaptureList> dumpCmdRun =
new ArgRunnable<CaptureList>() {
@Override
public void run(CaptureList args) {
// Skip 2 tokens to get past listPattern and "commands"
String pattern = args.get(2).get(0);
mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), pattern);
}
};
trie.put(dumpCmdRun, DUMP_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
trie.put(
new Runnable() {
@Override
public void run() {
dumpEnv();
}
},
DUMP_PATTERN,
"e(?:nv)?");
// 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(
String.format(
"Failed to run command: %s\n%s",
e.toString(), StreamUtil.getStackTrace(e)));
}
}
};
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).first) {
mScheduler.shutdownOnEmpty();
}
} catch (ConfigurationException e) {
printLine("Failed to run command: " + e.toString());
}
// Intentionally kill the console before CommandScheduler finishes
mShouldExit.set(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");
}
/** Print the uptime of the Tradefed process. */
private void printElapsedTime() {
long elapsedTime = System.currentTimeMillis() - mConsoleStartTime;
String elapsed =
String.format(
"TF has been running for %s", TimeUtil.formatElapsedTime(elapsedTime));
printLine(elapsed);
}
/**
* 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}).
*/
@VisibleForTesting
String getConsoleInput() throws IOException {
if (mConsoleReader != null) {
try {
return mConsoleReader.readLine(getConsolePrompt());
} catch (EndOfFileException e) {
return null;
}
} 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();
}
/**
* Print the line to a Printwriter
*
* @param output
*/
protected void printLine(String output, PrintStream pw) {
pw.print(output);
pw.println();
}
/**
* Execute a command.
*
* <p>Exposed for unit testing
*/
@SuppressWarnings("unchecked")
void executeCmdRunnable(Runnable command, CaptureList groups) {
try {
if (command instanceof ArgRunnable) {
// FIXME: verify that command implements ArgRunnable<CaptureList> instead
// FIXME: of just ArgRunnable
((ArgRunnable<CaptureList>) command).run(groups);
} else {
command.run();
}
} catch (RuntimeException e) {
e.printStackTrace();
}
}
/**
* Return whether we should expect the console to be usable.
*
* <p>Exposed for unit testing.
*/
@SuppressWarnings("SystemConsoleNull") // https://errorprone.info/bugpattern/SystemConsoleNull
boolean isConsoleFunctional() {
java.io.Console systemConsole = System.console();
if (Runtime.version().feature() < 22) {
return systemConsole != null;
}
try {
return (Boolean) java.io.Console.class.getMethod("isTerminal").invoke(systemConsole);
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
}
/**
* 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.set(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.set(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().add(cmd);
}
tokens = arrrgs.toArray(new String[0]);
if (arrrgs.get(0).matches(HELP_PATTERN)) {
// if started from command line for help, return to shell
mShouldExit.set(true);
}
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.get());
} catch (Exception e) {
printLine("Console received an unexpected exception (shown below); shutting down TF.");
e.printStackTrace();
} finally {
mScheduler.shutdown();
GlobalConfiguration.getInstance().cleanup();
// Make sure that we don't quit with messages still in the buffers
System.err.flush();
System.out.flush();
}
}
/** set the flag to exit the console. */
@VisibleForTesting
void exitConsole() {
mShouldExit.set(true);
}
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(PrintStream ps) {
Map<Thread, StackTraceElement[]> threadMap = Thread.getAllStackTraces();
for (Map.Entry<Thread, StackTraceElement[]> threadEntry : threadMap.entrySet()) {
dumpThreadStack(threadEntry.getKey(), threadEntry.getValue(), ps);
}
}
private void dumpThreadStack(Thread thread, StackTraceElement[] trace, PrintStream ps) {
printLine(String.format("%s", thread), ps);
for (int i = 0; i < trace.length; i++) {
printLine(String.format("\t%s", trace[i]), ps);
}
printLine("", ps);
}
private void dumpLogs() {
LogRegistry.getLogRegistry().dumpLogs();
}
/** Dumps the environment variables to console, sorted by variable names */
private void dumpEnv() {
// use TreeMap to sort variables by name
Map<String, String> env = new TreeMap<>(System.getenv());
for (Map.Entry<String, String> entry : env.entrySet()) {
printLine(String.format("\t%s=%s", entry.getKey(), entry.getValue()));
}
}
/** Dump a Tradefed Bugreport containing the stack traces and logs. */
private void dumpTfBugreport() {
File tmpBugreportDir = null;
PrintStream ps = null;
try {
// dump stacks
tmpBugreportDir = FileUtil.createNamedTempDir("bugreport_tf");
File tmpStackFile = FileUtil.createTempFile("dump_stacks_", ".log", tmpBugreportDir);
ps = new PrintStream(tmpStackFile);
dumpStacks(ps);
ps.flush();
// dump logs
((LogRegistry) LogRegistry.getLogRegistry()).dumpLogsToDir(tmpBugreportDir);
// add them to a zip and log.
File zippedBugreport = ZipUtil.createZip(tmpBugreportDir, "tradefed_bugreport_");
printLine(
String.format("Output bugreport zip in %s", zippedBugreport.getAbsolutePath()));
} catch (IOException io) {
printLine("Error when trying to dump bugreport");
} finally {
ps.close();
FileUtil.recursiveDelete(tmpBugreportDir);
}
}
/**
* Sets the console starting arguments.
*
* @param mainArgs the arguments
*/
public void setArgs(List<String> mainArgs) {
mMainArgs = mainArgs;
}
private static class TerminateGRPCServers extends Thread {
private final TradefedFeatureServer mFeatureServer;
private final TestInvocationManagementServer mInvocationServer;
private final DeviceManagementGrpcServer mDeviceServer;
public TerminateGRPCServers(
TradefedFeatureServer featureServer,
TestInvocationManagementServer invocationServer,
DeviceManagementGrpcServer deviceServer) {
mFeatureServer = featureServer;
mInvocationServer = invocationServer;
mDeviceServer = deviceServer;
}
@Override
public void run() {
if (mFeatureServer != null) {
try {
mFeatureServer.shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
if (mInvocationServer != null) {
try {
mInvocationServer.shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
if (mDeviceServer != null) {
try {
mDeviceServer.shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
}
}
public static void main(final String[] mainArgs)
throws InterruptedException, ConfigurationException {
Console console = new Console();
try {
startConsole(console, mainArgs);
} catch (ConfigurationException e) {
e.printStackTrace();
System.exit(ExitCode.CONFIG_EXCEPTION.getCodeValue());
} catch (InterruptedException interrupt) {
interrupt.printStackTrace();
System.exit(ExitCode.THROWABLE_EXCEPTION.getCodeValue());
}
}
/**
* 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 {
ClearcutClient client =
new ClearcutClient(
TestSuiteInfo.getInstance().didLoadFromProperties()
? TestSuiteInfo.getInstance().getName()
: "");
Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
client.notifyTradefedStartEvent();
TradefedFeatureServer server = null;
try {
server = new TradefedFeatureServer();
server.start();
} catch (RuntimeException e) {
System.out.println(String.format("Error starting feature server: %s", e));
// Abort the start if we fail to start the server, it is a critical component.
throw e;
}
TestInvocationManagementServer invocationManagementServer = null;
DeviceManagementGrpcServer deviceManagementServer = null;
try {
List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args);
GlobalConfiguration.getInstance().setup();
if (server != null) {
GlobalConfiguration.getInstance().setTradefedFeatureServer(server);
}
console.setArgs(nonGlobalArgs);
console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler());
console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory());
console.setDaemon(true);
GlobalConfiguration.getInstance().getCommandScheduler().setClearcutClient(client);
// Initialize the locks for the TF session
GlobalConfiguration.getInstance().getHostOptions().initConcurrentLocks();
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();
console.registerShutdownSignals();
// Gate the server starting to a port being explicitly defined
Integer deviceManagementPort = DeviceManagementGrpcServer.getPort();
if (deviceManagementPort != null) {
try {
deviceManagementServer =
new DeviceManagementGrpcServer(
deviceManagementPort,
GlobalConfiguration.getDeviceManagerInstance(),
GlobalConfiguration.getInstance().getCommandScheduler());
GlobalConfiguration.getInstance()
.setDeviceManagementServer(deviceManagementServer);
deviceManagementServer.start();
} catch (RuntimeException e) {
System.out.println(
String.format("Error starting device management server: %s", e));
}
}
Integer port = TestInvocationManagementServer.getPort();
if (port != null) {
try {
invocationManagementServer =
new TestInvocationManagementServer(
port,
GlobalConfiguration.getInstance().getCommandScheduler(),
deviceManagementServer);
GlobalConfiguration.getInstance()
.setInvocationServer(invocationManagementServer);
// Start the server last to ensure that command scheduler is started
invocationManagementServer.start();
} catch (RuntimeException e) {
System.out.println(String.format("Error starting invocation server: %s", e));
}
}
} finally {
Runtime.getRuntime()
.addShutdownHook(
new TerminateGRPCServers(
server, invocationManagementServer, deviceManagementServer));
}
}
}