blob: 2e363c382adcefd824f08545935d79cd1b94f538 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.runtime;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
import com.google.common.base.Verify;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.io.Flushables;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.flags.InvocationPolicyEnforcer;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.AnsiStrippingOutputStream;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.DelegatingOutErr;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.OpaqueOptionsData;
import com.google.devtools.common.options.OptionPriority;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import javax.annotation.Nullable;
/**
* Dispatches to the Blaze commands; that is, given a command line, this
* abstraction looks up the appropriate command object, parses the options
* required by the object, and calls its exec method. Also, this object provides
* the runtime state (BlazeRuntime) to the commands.
*/
public class BlazeCommandDispatcher {
/**
* What to do if the command lock is not available.
*/
public enum LockingMode {
WAIT, // Wait until it is available
ERROR_OUT, // Return with an error
}
// Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
private static final Set<String> INTERNAL_COMMAND_OPTIONS = ImmutableSet.of(
"rc_source", "default_override", "isatty", "terminal_columns", "ignore_client_env",
"client_env", "client_cwd");
private static final ImmutableList<String> HELP_COMMAND = ImmutableList.of("help");
private static final Set<String> ALL_HELP_OPTIONS = ImmutableSet.of("--help", "-help", "-h");
/**
* If the server needs to be shut down and how.
*/
public enum ShutdownMethod {
/** The server doesn't need to be shut down. */
NONE,
/** The server is to be shut down cleanly, e.g. as a result of "blaze shutdown". */
CLEAN,
/**
* The server is shut down as a result of a "blaze clean --expunge.
*
* <p>In this case, no files should be deleted on shutdown hooks, since clean also deletes the
* lock file, and there is a small possibility of the following sequence of events:
*
* <ol>
* <li> Client 1 runs "blaze clean --expunge"
* <li> Client 2 runs a command and waits for client 1 to finish
* <li> The clean command deletes everything including the lock file
* <li> Client 2 starts running and since the output base is empty, starts up a new server,
* which creates its own socket and PID files
* <li> The server used by client runs its shutdown hooks, deleting the PID files created by
* the new server
* </ol>
*/
EXPUNGE,
}
/**
* By throwing this exception, a command indicates that it wants to shutdown
* the Blaze server process.
* See {@link BlazeCommandDispatcher#exec(List, OutErr, long)}.
*/
public static class ShutdownBlazeServerException extends Exception {
private final int exitStatus;
private final ShutdownMethod method;
public ShutdownBlazeServerException(int exitStatus, ShutdownMethod method, Throwable cause) {
super(cause);
this.exitStatus = exitStatus;
this.method = method;
}
public ShutdownBlazeServerException(int exitStatus, ShutdownMethod method) {
Preconditions.checkState(method != ShutdownMethod.NONE);
this.exitStatus = exitStatus;
this.method = method;
}
public int getExitStatus() {
return exitStatus;
}
public ShutdownMethod getMethod() {
return method;
}
}
private final BlazeRuntime runtime;
private final Object commandLock;
private String currentClientDescription = null;
private String shutdownReason = null;
private OutputStream logOutputStream = null;
private final LoadingCache<BlazeCommand, OpaqueOptionsData> optionsDataCache =
CacheBuilder.newBuilder().build(
new CacheLoader<BlazeCommand, OpaqueOptionsData>() {
@Override
public OpaqueOptionsData load(BlazeCommand command) {
return OptionsParser.getOptionsData(BlazeCommandUtils.getOptions(
command.getClass(),
runtime.getBlazeModules(),
runtime.getRuleClassProvider()));
}
});
/**
* Create a Blaze dispatcher that uses the specified {@code BlazeRuntime} instance, but overrides
* the command map with the given commands (plus any commands from modules).
*/
@VisibleForTesting
public BlazeCommandDispatcher(BlazeRuntime runtime, BlazeCommand... commands) {
this(runtime);
runtime.overrideCommands(Arrays.asList(commands));
}
/**
* Create a Blaze dispatcher that uses the specified {@code BlazeRuntime} instance.
*/
@VisibleForTesting
public BlazeCommandDispatcher(BlazeRuntime runtime) {
this.runtime = runtime;
this.commandLock = new Object();
}
/**
* Only some commands work if cwd != workspaceSuffix in Blaze. In that case, also check if Blaze
* was called from the output directory and fail if it was.
*/
private ExitCode checkCwdInWorkspace(CommandEnvironment env, Command commandAnnotation,
String commandName, OutErr outErr) {
if (!commandAnnotation.mustRunInWorkspace()) {
return ExitCode.SUCCESS;
}
if (!env.inWorkspace()) {
outErr.printErrLn(
"The '" + commandName + "' command is only supported from within a workspace.");
return ExitCode.COMMAND_LINE_ERROR;
}
Path workspace = env.getWorkspace();
// TODO(kchodorow): Remove this once spaces are supported.
if (workspace.getPathString().contains(" ")) {
outErr.printErrLn(runtime.getProductName() + " does not currently work properly from paths "
+ "containing spaces (" + workspace + ").");
return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
}
Path doNotBuild = workspace.getParentDirectory().getRelative(
BlazeWorkspace.DO_NOT_BUILD_FILE_NAME);
if (doNotBuild.exists()) {
if (!commandAnnotation.canRunInOutputDirectory()) {
outErr.printErrLn(getNotInRealWorkspaceError(doNotBuild));
return ExitCode.COMMAND_LINE_ERROR;
} else {
outErr.printErrLn("WARNING: Blaze is run from output directory. This is unsound.");
}
}
return ExitCode.SUCCESS;
}
private void parseArgsAndConfigs(OptionsParser optionsParser, Command commandAnnotation,
List<String> args, List<String> rcfileNotes, OutErr outErr)
throws OptionsParsingException {
Function<String, String> commandOptionSourceFunction = new Function<String, String>() {
@Override
public String apply(String input) {
if (INTERNAL_COMMAND_OPTIONS.contains(input)) {
return "options generated by Blaze launcher";
} else {
return "command line options";
}
}
};
// Explicit command-line options:
List<String> cmdLineAfterCommand = args.subList(1, args.size());
optionsParser.parseWithSourceFunction(OptionPriority.COMMAND_LINE,
commandOptionSourceFunction, cmdLineAfterCommand);
// Command-specific options from .blazerc passed in via --default_override
// and --rc_source. A no-op if none are provided.
CommonCommandOptions rcFileOptions = optionsParser.getOptions(CommonCommandOptions.class);
List<Pair<String, ListMultimap<String, String>>> optionsMap =
getOptionsMap(outErr, rcFileOptions.rcSource, rcFileOptions.optionsOverrides,
runtime.getCommandMap().keySet());
parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap, null, null);
// Fix-point iteration until all configs are loaded.
List<String> configsLoaded = ImmutableList.of();
Set<String> unknownConfigs = new LinkedHashSet<>();
CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
while (!commonOptions.configs.equals(configsLoaded)) {
Set<String> missingConfigs = new LinkedHashSet<>(commonOptions.configs);
missingConfigs.removeAll(configsLoaded);
parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap,
missingConfigs, unknownConfigs);
configsLoaded = commonOptions.configs;
commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
}
if (!unknownConfigs.isEmpty()) {
outErr.printErrLn("WARNING: Config values are not defined in any .rc file: "
+ Joiner.on(", ").join(unknownConfigs));
}
}
/**
* Executes a single command. Returns the Unix exit status for the Blaze
* client process, or throws {@link ShutdownBlazeServerException} to
* indicate that a command wants to shutdown the Blaze server.
*/
int exec(List<String> args, OutErr outErr, LockingMode lockingMode, String clientDescription,
long firstContactTime) throws ShutdownBlazeServerException, InterruptedException {
Preconditions.checkNotNull(clientDescription);
if (args.isEmpty()) { // Default to help command if no arguments specified.
args = HELP_COMMAND;
}
String commandName = args.get(0);
// Be gentle to users who want to find out about Blaze invocation.
if (ALL_HELP_OPTIONS.contains(commandName)) {
commandName = "help";
}
BlazeCommand command = runtime.getCommandMap().get(commandName);
if (command == null) {
outErr.printErrLn(String.format(
"Command '%s' not found. Try '%s help'.", commandName, runtime.getProductName()));
return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
}
long waitTimeInMs = 0;
synchronized (commandLock) {
boolean warningPrinted = false;
while (currentClientDescription != null) {
switch (lockingMode) {
case WAIT:
if (!warningPrinted) {
outErr.printErrLn("Another command (" + currentClientDescription + ") is running. "
+ " Waiting for it to complete...");
warningPrinted = true;
}
long clockBefore = BlazeClock.nanoTime();
commandLock.wait();
waitTimeInMs = (BlazeClock.nanoTime() - clockBefore) / (1000L * 1000L);
break;
case ERROR_OUT:
outErr.printErrLn(String.format("Another command (" + currentClientDescription + ") is "
+ "running. Exiting immediately."));
return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
default:
throw new IllegalStateException();
}
}
Verify.verify(currentClientDescription == null);
currentClientDescription = clientDescription;
}
try {
if (shutdownReason != null) {
outErr.printErrLn("Server shut down " + shutdownReason);
return ExitCode.LOCAL_ENVIRONMENTAL_ERROR.getNumericExitCode();
}
return execExclusively(args, outErr, firstContactTime, commandName, command, waitTimeInMs);
} catch (ShutdownBlazeServerException e) {
shutdownReason = "explicitly by client " + currentClientDescription;
throw e;
} finally {
synchronized (commandLock) {
currentClientDescription = null;
commandLock.notify();
}
}
}
private int execExclusively(List<String> args, OutErr outErr, long firstContactTime,
String commandName, BlazeCommand command, long waitTimeInMs)
throws ShutdownBlazeServerException {
Command commandAnnotation = command.getClass().getAnnotation(Command.class);
// Record the start time for the profiler. Do not put anything before this!
long execStartTimeNanos = runtime.getClock().nanoTime();
// The initCommand call also records the start time for the timestamp granularity monitor.
CommandEnvironment env = runtime.getWorkspace().initCommand();
// Record the command's starting time for use by the commands themselves.
env.recordCommandStartTime(firstContactTime);
AbruptExitException exitCausingException = null;
for (BlazeModule module : runtime.getBlazeModules()) {
try {
module.beforeCommand(commandAnnotation, env);
} catch (AbruptExitException e) {
// Don't let one module's complaints prevent the other modules from doing necessary
// setup. We promised to call beforeCommand exactly once per-module before each command
// and will be calling afterCommand soon in the future - a module's afterCommand might
// rightfully assume its beforeCommand has already been called.
outErr.printErrLn(e.getMessage());
// It's not ideal but we can only return one exit code, so we just pick the code of the
// last exception.
exitCausingException = e;
}
}
if (exitCausingException != null) {
return exitCausingException.getExitCode().getNumericExitCode();
}
if (env.getRuntime().writeCommandLog()) {
try {
Path commandLog = getCommandLogPath(env.getOutputBase());
// Unlink old command log from previous build, if present, so scripts
// reading it don't conflate it with the command log we're about to write.
commandLog.delete();
logOutputStream = commandLog.getOutputStream();
outErr = tee(outErr, OutErr.create(logOutputStream, logOutputStream));
} catch (IOException ioException) {
LoggingUtil.logToRemote(
Level.WARNING, "Unable to delete or open command.log", ioException);
}
}
ExitCode result = checkCwdInWorkspace(env, commandAnnotation, commandName, outErr);
if (result != ExitCode.SUCCESS) {
return result.getNumericExitCode();
}
OptionsParser optionsParser;
// Delay output of notes regarding the parsed rc file, so it's possible to disable this in the
// rc file.
List<String> rcfileNotes = new ArrayList<>();
try {
optionsParser = createOptionsParser(command);
parseArgsAndConfigs(optionsParser, commandAnnotation, args, rcfileNotes, outErr);
InvocationPolicyEnforcer optionsPolicyEnforcer =
new InvocationPolicyEnforcer(runtime.getInvocationPolicy());
optionsPolicyEnforcer.enforce(optionsParser, commandName);
optionsPolicyEnforcer =
InvocationPolicyEnforcer.create(
getRuntime()
.getStartupOptionsProvider()
.getOptions(BlazeServerStartupOptions.class)
.invocationPolicy);
optionsPolicyEnforcer.enforce(optionsParser, commandName);
} catch (OptionsParsingException e) {
for (String note : rcfileNotes) {
outErr.printErrLn("INFO: " + note);
}
outErr.printErrLn(e.getMessage());
return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
}
// Setup log filtering
BlazeCommandEventHandler.Options eventHandlerOptions =
optionsParser.getOptions(BlazeCommandEventHandler.Options.class);
OutErr colorfulOutErr = outErr;
if (!eventHandlerOptions.useColor()) {
outErr = ansiStripOut(ansiStripErr(outErr));
if (!commandAnnotation.binaryStdOut()) {
colorfulOutErr = ansiStripOut(colorfulOutErr);
}
if (!commandAnnotation.binaryStdErr()) {
colorfulOutErr = ansiStripErr(colorfulOutErr);
}
}
CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
BlazeRuntime.setupLogging(commonOptions.verbosity);
// Do this before an actual crash so we don't have to worry about
// allocating memory post-crash.
String[] crashData = env.getCrashData();
int numericExitCode = ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode();
PrintStream savedOut = System.out;
PrintStream savedErr = System.err;
EventHandler handler = createEventHandler(outErr, eventHandlerOptions);
Reporter reporter = env.getReporter();
reporter.addHandler(handler);
env.getEventBus().register(handler);
// We register an ANSI-allowing handler associated with {@code handler} so that ANSI control
// codes can be re-introduced later even if blaze is invoked with --color=no. This is useful
// for commands such as 'blaze run' where the output of the final executable shouldn't be
// modified.
EventHandler ansiAllowingHandler = null;
if (!eventHandlerOptions.useColor()) {
ansiAllowingHandler = createEventHandler(colorfulOutErr, eventHandlerOptions);
reporter.registerAnsiAllowingHandler(handler, ansiAllowingHandler);
if (ansiAllowingHandler instanceof ExperimentalEventHandler) {
env.getEventBus()
.register(
new PassiveExperimentalEventHandler(
(ExperimentalEventHandler) ansiAllowingHandler));
}
}
try {
// While a Blaze command is active, direct all errors to the client's
// event handler (and out/err streams).
OutErr reporterOutErr = reporter.getOutErr();
System.setOut(new PrintStream(reporterOutErr.getOutputStream(), /*autoflush=*/true));
System.setErr(new PrintStream(reporterOutErr.getErrorStream(), /*autoflush=*/true));
for (BlazeModule module : runtime.getBlazeModules()) {
module.checkEnvironment(env);
}
if (commonOptions.announceRcOptions) {
for (String note : rcfileNotes) {
reporter.handle(Event.info(note));
}
}
try {
// Notify the BlazeRuntime, so it can do some initial setup.
env.beforeCommand(commandAnnotation, optionsParser, commonOptions, execStartTimeNanos,
waitTimeInMs);
// Allow the command to edit options after parsing:
command.editOptions(env, optionsParser);
} catch (AbruptExitException e) {
reporter.handle(Event.error(e.getMessage()));
return e.getExitCode().getNumericExitCode();
}
for (BlazeModule module : runtime.getBlazeModules()) {
module.handleOptions(optionsParser);
}
// Print warnings for odd options usage
for (String warning : optionsParser.getWarnings()) {
reporter.handle(Event.warn(warning));
}
ExitCode outcome = command.exec(env, optionsParser);
outcome = env.precompleteCommand(outcome);
numericExitCode = outcome.getNumericExitCode();
return numericExitCode;
} catch (ShutdownBlazeServerException e) {
numericExitCode = e.getExitStatus();
throw e;
} catch (Throwable e) {
e.printStackTrace();
BugReport.printBug(outErr, e);
BugReport.sendBugReport(e, args, crashData);
numericExitCode = BugReport.getExitCodeForThrowable(e);
throw new ShutdownBlazeServerException(numericExitCode, ShutdownMethod.CLEAN, e);
} finally {
env.getEventBus().post(new AfterCommandEvent());
runtime.afterCommand(env, numericExitCode);
// Swallow IOException, as we are already in a finally clause
Flushables.flushQuietly(outErr.getOutputStream());
Flushables.flushQuietly(outErr.getErrorStream());
System.setOut(savedOut);
System.setErr(savedErr);
reporter.removeHandler(handler);
releaseHandler(handler);
if (!eventHandlerOptions.useColor()) {
reporter.removeHandler(ansiAllowingHandler);
releaseHandler(ansiAllowingHandler);
}
env.getTimestampGranularityMonitor().waitForTimestampGranularity(outErr);
}
}
/**
* For testing ONLY. Same as {@link #exec(List, OutErr, boolean, String, long)}, but automatically
* uses the current time.
*/
@VisibleForTesting
public int exec(List<String> args, LockingMode lockingMode, String clientDescription,
OutErr originalOutErr) throws ShutdownBlazeServerException, InterruptedException {
return exec(args, originalOutErr, LockingMode.ERROR_OUT, clientDescription,
runtime.getClock().currentTimeMillis());
}
/**
* Parses the options from .rc files for a command invocation. It works in one of two modes;
* either it loads the non-config options, or the config options that are specified in the {@code
* configs} parameter.
*
* <p>This method adds every option pertaining to the specified command to the options parser. To
* do that, it needs the command -> option mapping that is generated from the .rc files.
*
* <p>It is not as trivial as simply taking the list of options for the specified command because
* commands can inherit arguments from each other, and we have to respect that (e.g. if an option
* is specified for 'build', it needs to take effect for the 'test' command, too).
*
* <p>Note that the order in which the options are parsed is well-defined: all options from the
* same rc file are parsed at the same time, and the rc files are handled in the order in which
* they were passed in from the client.
*
* @param rcfileNotes note message that would be printed during parsing
* @param commandAnnotation the command for which options should be parsed.
* @param optionsParser parser to receive parsed options.
* @param optionsMap .rc files in structured format: a list of pairs, where the first part is the
* name of the rc file, and the second part is a multimap of command name (plus config, if
* present) to the list of options for that command
* @param configs the configs for which to parse options; if {@code null}, non-config options are
* parsed
* @param unknownConfigs optional; a collection that the method will populate with the config
* values in {@code configs} that none of the .rc files had entries for
* @throws OptionsParsingException
*/
protected static void parseOptionsForCommand(List<String> rcfileNotes, Command commandAnnotation,
OptionsParser optionsParser, List<Pair<String, ListMultimap<String, String>>> optionsMap,
@Nullable Collection<String> configs, @Nullable Collection<String> unknownConfigs)
throws OptionsParsingException {
Set<String> knownConfigs = new HashSet<>();
for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
for (Pair<String, ListMultimap<String, String>> entry : optionsMap) {
List<String> allOptions = new ArrayList<>();
if (configs == null) {
allOptions.addAll(entry.second.get(commandToParse));
} else {
for (String config : configs) {
Collection<String> values = entry.second.get(commandToParse + ":" + config);
if (!values.isEmpty()) {
allOptions.addAll(values);
knownConfigs.add(config);
}
}
}
processOptionList(optionsParser, commandToParse,
commandAnnotation.name(), rcfileNotes, entry.first, allOptions);
}
}
if (unknownConfigs != null && configs != null && configs.size() > knownConfigs.size()) {
Iterables.addAll(
unknownConfigs,
Iterables.filter(configs, Predicates.not(Predicates.in(knownConfigs))));
}
}
// Processes the option list for an .rc file - command pair.
private static void processOptionList(OptionsParser optionsParser, String commandToParse,
String originalCommand, List<String> rcfileNotes, String rcfile, List<String> rcfileOptions)
throws OptionsParsingException {
if (!rcfileOptions.isEmpty()) {
String inherited = commandToParse.equals(originalCommand) ? "" : "Inherited ";
String source = rcfile.equals("client") ? "Options provided by the client"
: "Reading options for '" + originalCommand + "' from " + rcfile;
rcfileNotes.add(source + ":\n"
+ " " + inherited + "'" + commandToParse + "' options: "
+ Joiner.on(' ').join(rcfileOptions));
optionsParser.parse(OptionPriority.RC_FILE, rcfile, rcfileOptions);
}
}
private static List<String> getCommandNamesToParse(Command commandAnnotation) {
List<String> result = new ArrayList<>();
result.add("common");
getCommandNamesToParseHelper(commandAnnotation, result);
return result;
}
private static void getCommandNamesToParseHelper(Command commandAnnotation,
List<String> accumulator) {
for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
getCommandNamesToParseHelper(base.getAnnotation(Command.class), accumulator);
}
accumulator.add(commandAnnotation.name());
}
private OutErr ansiStripOut(OutErr outErr) {
OutputStream wrappedOut = new AnsiStrippingOutputStream(outErr.getOutputStream());
return OutErr.create(wrappedOut, outErr.getErrorStream());
}
private OutErr ansiStripErr(OutErr outErr) {
OutputStream wrappedErr = new AnsiStrippingOutputStream(outErr.getErrorStream());
return OutErr.create(outErr.getOutputStream(), wrappedErr);
}
private String getNotInRealWorkspaceError(Path doNotBuildFile) {
String message = "Blaze should not be called from a Blaze output directory. ";
try {
String realWorkspace =
new String(FileSystemUtils.readContentAsLatin1(doNotBuildFile));
message += String.format("The pertinent workspace directory is: '%s'",
realWorkspace);
} catch (IOException e) {
// We are exiting anyway.
}
return message;
}
/**
* For a given output_base directory, returns the command log file path.
*/
public static Path getCommandLogPath(Path outputBase) {
return outputBase.getRelative("command.log");
}
private OutErr tee(OutErr outErr1, OutErr outErr2) {
DelegatingOutErr outErr = new DelegatingOutErr();
outErr.addSink(outErr1);
outErr.addSink(outErr2);
return outErr;
}
private void closeSilently(OutputStream logOutputStream) {
if (logOutputStream != null) {
try {
logOutputStream.close();
} catch (IOException e) {
LoggingUtil.logToRemote(Level.WARNING, "Unable to close command.log", e);
}
}
}
/**
* Creates an option parser using the common options classes and the
* command-specific options classes.
*
* <p>An overriding method should first call this method and can then
* override default values directly or by calling {@link
* #parseOptionsForCommand} for command-specific options.
*
* @throws OptionsParsingException
*/
protected OptionsParser createOptionsParser(BlazeCommand command)
throws OptionsParsingException {
Command annotation = command.getClass().getAnnotation(Command.class);
OptionsParser parser = OptionsParser.newOptionsParser(optionsDataCache.getUnchecked(command));
parser.setAllowResidue(annotation.allowResidue());
return parser;
}
/**
* Convert a list of option override specifications to a more easily digestible
* form.
*
* @param overrides list of option override specifications
*/
@VisibleForTesting
static List<Pair<String, ListMultimap<String, String>>> getOptionsMap(
OutErr outErr,
List<String> rcFiles,
List<CommonCommandOptions.OptionOverride> overrides,
Set<String> validCommands) {
List<Pair<String, ListMultimap<String, String>>> result = new ArrayList<>();
String lastRcFile = null;
ListMultimap<String, String> lastMap = null;
for (CommonCommandOptions.OptionOverride override : overrides) {
if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
outErr.printErrLn("WARNING: inconsistency in generated command line "
+ "args. Ignoring bogus argument\n");
continue;
}
String rcFile = rcFiles.get(override.blazeRc);
String command = override.command;
int index = command.indexOf(':');
if (index > 0) {
command = command.substring(0, index);
}
if (!validCommands.contains(command) && !command.equals("common")) {
outErr.printErrLn("WARNING: while reading option defaults file '"
+ rcFile + "':\n"
+ " invalid command name '" + override.command + "'.");
continue;
}
if (!rcFile.equals(lastRcFile)) {
if (lastRcFile != null) {
result.add(Pair.of(lastRcFile, lastMap));
}
lastRcFile = rcFile;
lastMap = ArrayListMultimap.create();
}
lastMap.put(override.command, override.option);
}
if (lastRcFile != null) {
result.add(Pair.of(lastRcFile, lastMap));
}
return result;
}
/**
* Returns the event handler to use for this Blaze command.
*/
private EventHandler createEventHandler(OutErr outErr,
BlazeCommandEventHandler.Options eventOptions) {
EventHandler eventHandler;
if (eventOptions.experimentalUi) {
// The experimental event handler is not to be rate limited.
return new ExperimentalEventHandler(outErr, eventOptions, runtime.getClock());
} else if ((eventOptions.useColor() || eventOptions.useCursorControl())) {
eventHandler = new FancyTerminalEventHandler(outErr, eventOptions);
} else {
eventHandler = new BlazeCommandEventHandler(outErr, eventOptions);
}
return RateLimitingEventHandler.create(eventHandler, eventOptions.showProgressRateLimit);
}
/**
* Unsets the event handler.
*/
private void releaseHandler(EventHandler eventHandler) {
if (eventHandler instanceof FancyTerminalEventHandler) {
// Make sure that the terminal state of the old event handler is clear
// before creating a new one.
((FancyTerminalEventHandler) eventHandler).resetTerminal();
}
if (eventHandler instanceof ExperimentalEventHandler) {
((ExperimentalEventHandler) eventHandler).resetTerminal();
}
}
/**
* Returns the runtime instance shared by the commands that this dispatcher
* dispatches to.
*/
public BlazeRuntime getRuntime() {
return runtime;
}
/**
* Shuts down all the registered commands to give them a chance to cleanup or
* close resources. Should be called by the owner of this command dispatcher
* in all termination cases.
*/
public void shutdown() {
closeSilently(logOutputStream);
logOutputStream = null;
}
}