| // 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; | |
| } | |
| } |