| // Copyright 2014 Google Inc. 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.collect.ArrayListMultimap; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.ListMultimap; | |
| import com.google.common.collect.Lists; | |
| 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.util.AbruptExitException; | |
| import com.google.devtools.build.lib.util.AnsiStrippingOutputStream; | |
| 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.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.OptionPriority; | |
| import com.google.devtools.common.options.OptionsBase; | |
| 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.Collections; | |
| import java.util.LinkedHashMap; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.UUID; | |
| import java.util.logging.Level; | |
| /** | |
| * 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 { | |
| // 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"); | |
| /** | |
| * 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; | |
| 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 Map<String, BlazeCommand> commandsByName = new LinkedHashMap<>(); | |
| private OutputStream logOutputStream = null; | |
| /** | |
| * Create a Blaze dispatcher that uses the specified {@code BlazeRuntime} | |
| * instance, and no default options, and delegates to {@code commands} as | |
| * appropriate. | |
| */ | |
| @VisibleForTesting | |
| public BlazeCommandDispatcher(BlazeRuntime runtime, BlazeCommand... commands) { | |
| this(runtime, ImmutableList.copyOf(commands)); | |
| } | |
| /** | |
| * Create a Blaze dispatcher that uses the specified {@code BlazeRuntime} | |
| * instance, and delegates to {@code commands} as appropriate. | |
| */ | |
| public BlazeCommandDispatcher(BlazeRuntime runtime, Iterable<BlazeCommand> commands) { | |
| this.runtime = runtime; | |
| for (BlazeCommand command : commands) { | |
| addCommandByName(command); | |
| } | |
| for (BlazeModule module : runtime.getBlazeModules()) { | |
| for (BlazeCommand command : module.getCommands()) { | |
| addCommandByName(command); | |
| } | |
| } | |
| runtime.setCommandMap(commandsByName); | |
| } | |
| /** | |
| * Adds the given command under the given name to the map of commands. | |
| * | |
| * @throws AssertionError if the name is already used by another command. | |
| */ | |
| private void addCommandByName(BlazeCommand command) { | |
| String name = command.getClass().getAnnotation(Command.class).name(); | |
| if (commandsByName.containsKey(name)) { | |
| throw new IllegalStateException("Command name or alias " + name + " is already used."); | |
| } | |
| commandsByName.put(name, command); | |
| } | |
| /** | |
| * 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(Command commandAnnotation, String commandName, | |
| OutErr outErr) { | |
| if (!commandAnnotation.mustRunInWorkspace()) { | |
| return ExitCode.SUCCESS; | |
| } | |
| if (!runtime.inWorkspace()) { | |
| outErr.printErrLn("The '" + commandName + "' command is only supported from within a " | |
| + "workspace."); | |
| return ExitCode.COMMAND_LINE_ERROR; | |
| } | |
| Path workspace = runtime.getWorkspace(); | |
| Path doNotBuild = workspace.getParentDirectory().getRelative( | |
| BlazeRuntime.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 CommonCommandOptions checkOptions(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, | |
| commandsByName.keySet()); | |
| parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap, null); | |
| // Fix-point iteration until all configs are loaded. | |
| List<String> configsLoaded = ImmutableList.of(); | |
| 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); | |
| configsLoaded = commonOptions.configs; | |
| commonOptions = optionsParser.getOptions(CommonCommandOptions.class); | |
| } | |
| return commonOptions; | |
| } | |
| /** | |
| * Sends {@code EventKind.{STDOUT|STDERR}} messages to the given {@link OutErr}. | |
| * | |
| * <p>This is necessary because we cannot delete the output files from the previous Blaze run | |
| * because there can be processes spawned by the previous invocation that are still processing | |
| * them, in which case we need to print a warning message about that. | |
| * | |
| * <p>Thus, messages sent to {@link Reporter#getOutErr} get sent to this event handler, then | |
| * to its {@link OutErr}. We need to go deeper! | |
| */ | |
| private static class OutErrEventHandler implements EventHandler { | |
| private final OutErr outErr; | |
| private OutErrEventHandler(OutErr outErr) { | |
| this.outErr = outErr; | |
| } | |
| @Override | |
| public void handle(Event event) { | |
| try { | |
| switch (event.getKind()) { | |
| case STDOUT: | |
| outErr.getOutputStream().write(event.getMessageBytes()); | |
| break; | |
| case STDERR: | |
| outErr.getErrorStream().write(event.getMessageBytes()); | |
| break; | |
| } | |
| } catch (IOException e) { | |
| // We cannot do too much here -- ErrorEventListener#handle does not provide us with ways to | |
| // report an error. | |
| } | |
| } | |
| } | |
| /** | |
| * 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. | |
| */ | |
| public int exec(List<String> args, OutErr originalOutErr, long firstContactTime) | |
| throws ShutdownBlazeServerException { | |
| // Record the start time for the profiler and the timestamp granularity monitor. Do not put | |
| // anything before this! | |
| long execStartTimeNanos = runtime.getClock().nanoTime(); | |
| // Record the command's starting time for use by the commands themselves. | |
| runtime.recordCommandStartTime(firstContactTime); | |
| // Record the command's starting time again, for use by | |
| // TimestampGranularityMonitor.waitForTimestampGranularity(). | |
| // This should be done as close as possible to the start of | |
| // the command's execution - that's why we do this separately, | |
| // rather than in runtime.beforeCommand(). | |
| runtime.getTimestampGranularityMonitor().setCommandStartTime(); | |
| runtime.initEventBus(); | |
| // Give a chance for module.beforeCommand() to report an errors to stdout and stderr. | |
| // Once we can close the old streams, this event handler is removed. | |
| OutErrEventHandler originalOutErrEventHandler = | |
| new OutErrEventHandler(originalOutErr); | |
| runtime.getReporter().addHandler(originalOutErrEventHandler); | |
| OutErr outErr = originalOutErr; | |
| runtime.getReporter().removeHandler(originalOutErrEventHandler); | |
| 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 = commandsByName.get(commandName); | |
| if (command == null) { | |
| outErr.printErrLn("Command '" + commandName + "' not found. " + "Try 'blaze help'."); | |
| return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode(); | |
| } | |
| Command commandAnnotation = command.getClass().getAnnotation(Command.class); | |
| AbruptExitException exitCausingException = null; | |
| for (BlazeModule module : runtime.getBlazeModules()) { | |
| try { | |
| module.beforeCommand(runtime, commandAnnotation); | |
| } 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(); | |
| } | |
| try { | |
| Path commandLog = getCommandLogPath(runtime.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(originalOutErr, OutErr.create(logOutputStream, logOutputStream)); | |
| } catch (IOException ioException) { | |
| LoggingUtil.logToRemote( | |
| Level.WARNING, "Unable to delete or open command.log", ioException); | |
| } | |
| // Create the UUID for this command. | |
| runtime.setCommandId(UUID.randomUUID()); | |
| ExitCode result = checkCwdInWorkspace(commandAnnotation, commandName, outErr); | |
| if (result != ExitCode.SUCCESS) { | |
| return result.getNumericExitCode(); | |
| } | |
| OptionsParser optionsParser; | |
| CommonCommandOptions commonOptions; | |
| // 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); | |
| commonOptions = checkOptions(optionsParser, commandAnnotation, args, rcfileNotes, outErr); | |
| } 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); | |
| if (!eventHandlerOptions.useColor()) { | |
| if (!commandAnnotation.binaryStdOut()) { | |
| outErr = ansiStripOut(outErr); | |
| } | |
| if (!commandAnnotation.binaryStdErr()) { | |
| outErr = ansiStripErr(outErr); | |
| } | |
| } | |
| BlazeRuntime.setupLogging(commonOptions.verbosity); | |
| // Do this before an actual crash so we don't have to worry about | |
| // allocating memory post-crash. | |
| String[] crashData = runtime.getCrashData(); | |
| int numericExitCode = ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode(); | |
| PrintStream savedOut = System.out; | |
| PrintStream savedErr = System.err; | |
| EventHandler handler = createEventHandler(outErr, eventHandlerOptions); | |
| Reporter reporter = runtime.getReporter(); | |
| reporter.addHandler(handler); | |
| 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)); | |
| if (commonOptions.announceRcOptions) { | |
| for (String note : rcfileNotes) { | |
| reporter.handle(Event.info(note)); | |
| } | |
| } | |
| try { | |
| // Notify the BlazeRuntime, so it can do some initial setup. | |
| runtime.beforeCommand(commandName, optionsParser, commonOptions, execStartTimeNanos); | |
| // Allow the command to edit options after parsing: | |
| command.editOptions(runtime, optionsParser); | |
| } catch (AbruptExitException e) { | |
| reporter.handle(Event.error(e.getMessage())); | |
| return e.getExitCode().getNumericExitCode(); | |
| } | |
| // Print warnings for odd options usage | |
| for (String warning : optionsParser.getWarnings()) { | |
| reporter.handle(Event.warn(warning)); | |
| } | |
| ExitCode outcome = command.exec(runtime, optionsParser); | |
| outcome = runtime.precompleteCommand(outcome); | |
| numericExitCode = outcome.getNumericExitCode(); | |
| return numericExitCode; | |
| } catch (ShutdownBlazeServerException e) { | |
| numericExitCode = e.getExitStatus(); | |
| throw e; | |
| } catch (Throwable e) { | |
| BugReport.printBug(outErr, e); | |
| BugReport.sendBugReport(e, args, crashData); | |
| numericExitCode = e instanceof OutOfMemoryError | |
| ? ExitCode.OOM_ERROR.getNumericExitCode() | |
| : ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode(); | |
| throw new ShutdownBlazeServerException(numericExitCode, e); | |
| } finally { | |
| runtime.afterCommand(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); | |
| runtime.getTimestampGranularityMonitor().waitForTimestampGranularity(outErr); | |
| } | |
| } | |
| /** | |
| * For testing ONLY. Same as {@link #exec(List, OutErr, long)}, but automatically uses the current | |
| * time. | |
| */ | |
| @VisibleForTesting | |
| public int exec(List<String> args, OutErr originalOutErr) throws ShutdownBlazeServerException { | |
| return exec(args, originalOutErr, 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 | |
| * @throws OptionsParsingException | |
| */ | |
| protected static void parseOptionsForCommand(List<String> rcfileNotes, Command commandAnnotation, | |
| OptionsParser optionsParser, List<Pair<String, ListMultimap<String, String>>> optionsMap, | |
| Iterable<String> configs) throws OptionsParsingException { | |
| 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) { | |
| allOptions.addAll(entry.second.get(commandToParse + ":" + config)); | |
| } | |
| } | |
| processOptionList(optionsParser, commandToParse, | |
| commandAnnotation.name(), rcfileNotes, entry.first, allOptions); | |
| if (allOptions.isEmpty()) { | |
| continue; | |
| } | |
| } | |
| } | |
| } | |
| // 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 "; | |
| rcfileNotes.add("Reading options for '" + originalCommand + | |
| "' from " + rcfile + ":\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<>(); | |
| getCommandNamesToParseHelper(commandAnnotation, result); | |
| result.add("common"); | |
| // TODO(bazel-team): This statement is a NO-OP: Lists.reverse(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); | |
| List<Class<? extends OptionsBase>> allOptions = Lists.newArrayList(); | |
| allOptions.addAll(BlazeCommandUtils.getOptions( | |
| command.getClass(), getRuntime().getBlazeModules(), getRuntime().getRuleClassProvider())); | |
| OptionsParser parser = OptionsParser.newOptionsParser(allOptions); | |
| 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.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(); | |
| } | |
| } | |
| /** | |
| * Returns the runtime instance shared by the commands that this dispatcher | |
| * dispatches to. | |
| */ | |
| public BlazeRuntime getRuntime() { | |
| return runtime; | |
| } | |
| /** | |
| * The map from command names to commands that this dispatcher dispatches to. | |
| */ | |
| Map<String, BlazeCommand> getCommandsByName() { | |
| return Collections.unmodifiableMap(commandsByName); | |
| } | |
| /** | |
| * 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; | |
| } | |
| } |