com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.java

Source

// 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.runtime.commands.ProjectFileSupport;
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");

    /**
     * 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: Blaze 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 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);
        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(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();
        }

        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);
            parseArgsAndConfigs(env, 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);
            }
        }

        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);
                // 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, 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 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 = "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;
    }
}