// 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.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.collect.Lists; import com.google.common.io.Flushables; import com.google.devtools.build.lib.Constants; 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.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.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.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 INTERNAL_COMMAND_OPTIONS = ImmutableSet.of( "rc_source", "default_override", "isatty", "terminal_columns", "ignore_client_env", "client_env", "client_cwd"); private static final ImmutableList HELP_COMMAND = ImmutableList.of("help"); private static final Set 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 Object commandLock; private String currentClientDescription = null; private OutputStream logOutputStream = null; /** * 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(Constants.PRODUCT_NAME + " 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 args, List rcfileNotes, OutErr outErr) throws OptionsParsingException { Function commandOptionSourceFunction = new Function() { @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 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>> 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 configsLoaded = ImmutableList.of(); Set unknownConfigs = new LinkedHashSet<>(); CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class); while (!commonOptions.configs.equals(configsLoaded)) { Set 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 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, Constants.PRODUCT_NAME)); return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode(); } synchronized (commandLock) { if (currentClientDescription != null) { switch (lockingMode) { case WAIT: outErr.printErrLn("Another command (" + currentClientDescription + ") is running. " + " Waiting for it to complete..."); commandLock.wait(); 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 { return execExclusively(args, outErr, firstContactTime, commandName, command); } finally { synchronized (commandLock) { currentClientDescription = null; commandLock.notify(); } } } private int execExclusively(List args, OutErr outErr, long firstContactTime, String commandName, BlazeCommand command) 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(); } 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 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); // 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(); } // 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) { 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(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 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. * *

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. * *

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). * *

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 rcfileNotes, Command commandAnnotation, OptionsParser optionsParser, List>> optionsMap, @Nullable Collection configs, @Nullable Collection unknownConfigs) throws OptionsParsingException { Set knownConfigs = new HashSet<>(); for (String commandToParse : getCommandNamesToParse(commandAnnotation)) { for (Pair> entry : optionsMap) { List allOptions = new ArrayList<>(); if (configs == null) { allOptions.addAll(entry.second.get(commandToParse)); } else { for (String config : configs) { Collection 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 rcfileNotes, String rcfile, List 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 getCommandNamesToParse(Command commandAnnotation) { List result = new ArrayList<>(); result.add("common"); getCommandNamesToParseHelper(commandAnnotation, result); return result; } private static void getCommandNamesToParseHelper(Command commandAnnotation, List accumulator) { for (Class 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. * *

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> 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>> getOptionsMap( OutErr outErr, List rcFiles, List overrides, Set validCommands) { List>> result = new ArrayList<>(); String lastRcFile = null; ListMultimap 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; } }