// 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.Throwables; 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.common.util.concurrent.UncheckedExecutionException; 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.runtime.commands.ProjectFileSupport; import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; 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 ImmutableSet<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 ImmutableSet<String> ALL_HELP_OPTIONS = ImmutableSet.of("--help", "-help", "-h"); /** * By throwing this exception, a command indicates that it wants to shutdown * the Blaze server process. */ public static class ShutdownBlazeServerException extends Exception { private final int exitStatus; public ShutdownBlazeServerException(int exitStatus, Throwable cause) { super(cause); this.exitStatus = exitStatus; } public ShutdownBlazeServerException(int exitStatus) { this.exitStatus = exitStatus; } public int getExitStatus() { return exitStatus; } } private final BlazeRuntime runtime; private final Object commandLock; private String currentClientDescription = null; private String shutdownReason = null; private OutputStream logOutputStream = null; private Level lastLogVerbosityLevel = 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: " + runtime.getProductName() + " is run from output directory. This is unsound."); } } return ExitCode.SUCCESS; } private void parseArgsAndConfigs(CommandEnvironment env, 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 " + runtime.getProductName() + " 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); if (commandAnnotation.builds()) { ProjectFileSupport.handleProjectFiles(env, optionsParser, commandAnnotation.name()); } // 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( InvocationPolicy invocationPolicy, List<String> args, OutErr outErr, LockingMode lockingMode, String clientDescription, long firstContactTime) throws ShutdownBlazeServerException, InterruptedException { OriginalCommandLineEvent originalCommandLine = new OriginalCommandLineEvent(args); 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(originalCommandLine, invocationPolicy, 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( OriginalCommandLineEvent originalCommandLine, InvocationPolicy invocationPolicy, 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(); } for (BlazeModule module : runtime.getBlazeModules()) { OutErr listener = module.getOutputListener(); if (listener != null) { outErr = tee(outErr, listener); } } 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. closeSilently(logOutputStream); logOutputStream = null; commandLog.delete(); if (env.getRuntime().writeCommandLog() && commandAnnotation.writeCommandLog()) { 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.equals(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); } catch (OptionsParser.ConstructionException e) { outErr.printErrLn("Internal error while constructing options parser: " + e.getMessage()); return ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode(); } try { parseArgsAndConfigs(env, optionsParser, commandAnnotation, args, rcfileNotes, outErr); // Merge the invocation policy that is user-supplied, from the command line, and any // invocation policy that was added by a module. The module one goes 'first,' so the user // one has priority. InvocationPolicy combinedPolicy = InvocationPolicy.newBuilder() .mergeFrom(runtime.getModuleInvocationPolicy()) .mergeFrom(invocationPolicy) .build(); InvocationPolicyEnforcer optionsPolicyEnforcer = new InvocationPolicyEnforcer(combinedPolicy); 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); } } if (!commandAnnotation.binaryStdOut()) { outErr = lineBufferOut(outErr); } if (!commandAnnotation.binaryStdErr()) { outErr = lineBufferErr(outErr); } CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class); if (!commonOptions.verbosity.equals(lastLogVerbosityLevel)) { BlazeRuntime.setupLogging(commonOptions.verbosity); lastLogVerbosityLevel = 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, invocationPolicy); // 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)); } env.getEventBus().post(originalCommandLine); for (BlazeModule module : runtime.getBlazeModules()) { env.getSkyframeExecutor().injectExtraPrecomputedValues(module.getPrecomputedValues()); } 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, 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, eventHandlerOptions); if (!eventHandlerOptions.useColor()) { reporter.removeHandler(ansiAllowingHandler); releaseHandler(ansiAllowingHandler, eventHandlerOptions); } env.getTimestampGranularityMonitor().waitForTimestampGranularity(outErr); } } /** * For testing ONLY. Same as {@link #exec}, but automatically * uses the current time. */ @VisibleForTesting public int exec(List<String> args, LockingMode lockingMode, String clientDescription, OutErr originalOutErr) throws ShutdownBlazeServerException, InterruptedException { return exec(InvocationPolicy.getDefaultInstance(), 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 lineBufferOut(OutErr outErr) { OutputStream wrappedOut = new LineBufferedOutputStream(outErr.getOutputStream()); return OutErr.create(wrappedOut, outErr.getErrorStream()); } private OutErr lineBufferErr(OutErr outErr) { OutputStream wrappedErr = new LineBufferedOutputStream(outErr.getErrorStream()); return OutErr.create(outErr.getOutputStream(), wrappedErr); } 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 = String.format( "%1$s should not be called from a %1$s output directory. ", runtime.getProductName()); 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. */ protected OptionsParser createOptionsParser(BlazeCommand command) throws OptionsParser.ConstructionException { OpaqueOptionsData optionsData = null; try { optionsData = optionsDataCache.getUnchecked(command); } catch (UncheckedExecutionException e) { Throwables.throwIfInstanceOf(e.getCause(), OptionsParser.ConstructionException.class); throw new IllegalStateException(e); } Command annotation = command.getClass().getAnnotation(Command.class); OptionsParser parser = OptionsParser.newOptionsParser(optionsData); 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, BlazeCommandEventHandler.Options eventOptions) { 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) && (eventOptions.useColor())) { ((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; } }