// 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.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.eventbus.SubscriberExceptionContext; import com.google.common.eventbus.SubscriberExceptionHandler; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Uninterruptibles; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.analysis.BlazeVersionInfo; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.WorkspaceStatusAction; import com.google.devtools.build.lib.analysis.config.BinTools; import com.google.devtools.build.lib.analysis.config.BuildOptions; import com.google.devtools.build.lib.analysis.config.ConfigurationFactory; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.OutputFilter; import com.google.devtools.build.lib.flags.CommandNameCache; import com.google.devtools.build.lib.packages.PackageFactory; import com.google.devtools.build.lib.packages.Preprocessor; import com.google.devtools.build.lib.packages.RuleClassProvider; import com.google.devtools.build.lib.profiler.AutoProfiler; import com.google.devtools.build.lib.profiler.MemoryProfiler; import com.google.devtools.build.lib.profiler.ProfilePhase; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.Profiler.ProfiledTaskKinds; import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.query2.AbstractBlazeQueryEnvironment; import com.google.devtools.build.lib.query2.QueryEnvironmentFactory; import com.google.devtools.build.lib.query2.output.OutputFormatter; import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory; import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.LockingMode; import com.google.devtools.build.lib.runtime.commands.BuildCommand; import com.google.devtools.build.lib.runtime.commands.CanonicalizeCommand; import com.google.devtools.build.lib.runtime.commands.CleanCommand; import com.google.devtools.build.lib.runtime.commands.DumpCommand; import com.google.devtools.build.lib.runtime.commands.HelpCommand; import com.google.devtools.build.lib.runtime.commands.InfoCommand; import com.google.devtools.build.lib.runtime.commands.MobileInstallCommand; import com.google.devtools.build.lib.runtime.commands.ProfileCommand; import com.google.devtools.build.lib.runtime.commands.QueryCommand; import com.google.devtools.build.lib.runtime.commands.RunCommand; import com.google.devtools.build.lib.runtime.commands.ShutdownCommand; import com.google.devtools.build.lib.runtime.commands.TestCommand; import com.google.devtools.build.lib.runtime.commands.VersionCommand; import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; import com.google.devtools.build.lib.server.AfUnixServer; import com.google.devtools.build.lib.server.RPCServer; import com.google.devtools.build.lib.server.signal.InterruptSignalHandler; import com.google.devtools.build.lib.skyframe.DiffAwareness; import com.google.devtools.build.lib.skyframe.PrecomputedValue; import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutorFactory; import com.google.devtools.build.lib.skyframe.SkyValueDirtinessChecker; import com.google.devtools.build.lib.skyframe.SkyframeExecutor; import com.google.devtools.build.lib.skyframe.SkyframeExecutorFactory; import com.google.devtools.build.lib.util.AbruptExitException; import com.google.devtools.build.lib.util.BlazeClock; import com.google.devtools.build.lib.util.Clock; import com.google.devtools.build.lib.util.ExitCode; import com.google.devtools.build.lib.util.LoggingUtil; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.OsUtils; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.ThreadUtils; import com.google.devtools.build.lib.util.io.OutErr; import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.JavaIoFileSystem; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.UnixFileSystem; import com.google.devtools.build.lib.vfs.WindowsFileSystem; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunctionName; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionPriority; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsClassProvider; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsParsingException; import com.google.devtools.common.options.OptionsProvider; import com.google.devtools.common.options.TriState; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * The BlazeRuntime class encapsulates the immutable configuration of the current instance. These * runtime settings and services are available to most parts of any Blaze application for the * duration of the batch run or server lifetime. * *

The parts specific to the current command are stored in {@link CommandEnvironment}. */ public final class BlazeRuntime { private static final Pattern suppressFromLog = Pattern.compile(".*(auth|pass|cookie).*", Pattern.CASE_INSENSITIVE); private static final Logger LOG = Logger.getLogger(BlazeRuntime.class.getName()); private final Iterable blazeModules; private final Map commandMap = new LinkedHashMap<>(); private final Clock clock; private final PackageFactory packageFactory; private final ConfigurationFactory configurationFactory; private final ConfiguredRuleClassProvider ruleClassProvider; private final AtomicInteger storedExitCode = new AtomicInteger(); // We pass this through here to make it available to the MasterLogWriter. private final OptionsProvider startupOptionsProvider; private final ProjectFile.Provider projectFileProvider; @Nullable private final InvocationPolicy invocationPolicy; private final String defaultsPackageContent; private final QueryEnvironmentFactory queryEnvironmentFactory; private final SubscriberExceptionHandler eventBusExceptionHandler; // Workspace state (currently exactly one workspace per server) private BlazeWorkspace workspace; private BlazeRuntime( QueryEnvironmentFactory queryEnvironmentFactory, PackageFactory pkgFactory, ConfiguredRuleClassProvider ruleClassProvider, ConfigurationFactory configurationFactory, Clock clock, OptionsProvider startupOptionsProvider, Iterable blazeModules, SubscriberExceptionHandler eventBusExceptionHandler, ProjectFile.Provider projectFileProvider, InvocationPolicy invocationPolicy, Iterable commands) { // Server state this.blazeModules = blazeModules; overrideCommands(commands); this.packageFactory = pkgFactory; this.projectFileProvider = projectFileProvider; this.invocationPolicy = invocationPolicy; this.ruleClassProvider = ruleClassProvider; this.configurationFactory = configurationFactory; this.clock = clock; this.startupOptionsProvider = startupOptionsProvider; this.queryEnvironmentFactory = queryEnvironmentFactory; this.eventBusExceptionHandler = eventBusExceptionHandler; this.defaultsPackageContent = ruleClassProvider.getDefaultsPackageContent(getInvocationPolicy()); CommandNameCache.CommandNameCacheInstance.INSTANCE.setCommandNameCache( new CommandNameCacheImpl(getCommandMap())); } private static InvocationPolicy createInvocationPolicyFromModules( InvocationPolicy initialInvocationPolicy, Iterable modules) { InvocationPolicy.Builder builder = InvocationPolicy.newBuilder(); builder.mergeFrom(initialInvocationPolicy); // Merge the policies from the modules for (BlazeModule module : modules) { InvocationPolicy modulePolicy = module.getInvocationPolicy(); if (modulePolicy != null) { builder.mergeFrom(module.getInvocationPolicy()); } } return builder.build(); } public void initWorkspace(BlazeDirectories directories, BinTools binTools) throws AbruptExitException { SkyframeExecutorFactory skyframeExecutorFactory = null; for (BlazeModule module : blazeModules) { SkyframeExecutorFactory skyFactory = module.getSkyframeExecutorFactory(directories); if (skyFactory != null) { Preconditions.checkState(skyframeExecutorFactory == null, "At most one Skyframe factory supported. But found two: %s and %s", skyFactory, skyframeExecutorFactory); skyframeExecutorFactory = skyFactory; } } if (skyframeExecutorFactory == null) { skyframeExecutorFactory = new SequencedSkyframeExecutorFactory(); } WorkspaceStatusAction.Factory workspaceStatusActionFactory = null; for (BlazeModule module : blazeModules) { WorkspaceStatusAction.Factory candidate = module.getWorkspaceStatusActionFactory(); if (candidate != null) { Preconditions.checkState(workspaceStatusActionFactory == null, "more than one module defines a workspace status action factory"); workspaceStatusActionFactory = candidate; } } Iterable diffAwarenessFactories; { ImmutableList.Builder builder = new ImmutableList.Builder<>(); boolean watchFS = startupOptionsProvider != null && startupOptionsProvider.getOptions(BlazeServerStartupOptions.class).watchFS; for (BlazeModule module : blazeModules) { builder.addAll(module.getDiffAwarenessFactories(watchFS)); } diffAwarenessFactories = builder.build(); } // Merge filters from Blaze modules that allow some action inputs to be missing. Predicate allowedMissingInputs = null; for (BlazeModule module : blazeModules) { Predicate modulePredicate = module.getAllowedMissingInputs(); if (modulePredicate != null) { Preconditions.checkArgument(allowedMissingInputs == null, "More than one Blaze module allows missing inputs."); allowedMissingInputs = modulePredicate; } } if (allowedMissingInputs == null) { allowedMissingInputs = Predicates.alwaysFalse(); } Preprocessor.Factory.Supplier preprocessorFactorySupplier = null; for (BlazeModule module : blazeModules) { Preprocessor.Factory.Supplier modulePreprocessorFactorySupplier = module.getPreprocessorFactorySupplier(); if (modulePreprocessorFactorySupplier != null) { Preconditions.checkState(preprocessorFactorySupplier == null, "more than one module defines a preprocessor factory supplier"); preprocessorFactorySupplier = modulePreprocessorFactorySupplier; } } if (preprocessorFactorySupplier == null) { preprocessorFactorySupplier = Preprocessor.Factory.Supplier.NullSupplier.INSTANCE; } // We use an immutable map builder for the nice side effect that it throws if a duplicate key // is inserted. ImmutableMap.Builder skyFunctions = ImmutableMap.builder(); for (BlazeModule module : blazeModules) { skyFunctions.putAll(module.getSkyFunctions(directories)); } ImmutableList.Builder precomputedValues = ImmutableList.builder(); for (BlazeModule module : blazeModules) { precomputedValues.addAll(module.getPrecomputedSkyframeValues()); } ImmutableList.Builder customDirtinessCheckers = ImmutableList.builder(); for (BlazeModule module : blazeModules) { customDirtinessCheckers.addAll(module.getCustomDirtinessCheckers()); } SkyframeExecutor skyframeExecutor = skyframeExecutorFactory.create( packageFactory, directories, binTools, workspaceStatusActionFactory, ruleClassProvider.getBuildInfoFactories(), diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier, skyFunctions.build(), precomputedValues.build(), customDirtinessCheckers.build()); this.workspace = new BlazeWorkspace( this, directories, skyframeExecutor, eventBusExceptionHandler, workspaceStatusActionFactory, binTools); } @Nullable public CoverageReportActionFactory getCoverageReportActionFactory() { CoverageReportActionFactory firstFactory = null; for (BlazeModule module : blazeModules) { CoverageReportActionFactory factory = module.getCoverageReportFactory(); if (factory != null) { Preconditions.checkState(firstFactory == null, "only one Blaze Module can have a Coverage Report Factory"); firstFactory = factory; } } return firstFactory; } /** * Adds the given command under the given name to the map of commands. * * @throws AssertionError if the name is already used by another command. */ private void addCommand(BlazeCommand command) { String name = command.getClass().getAnnotation(Command.class).name(); if (commandMap.containsKey(name)) { throw new IllegalStateException("Command name or alias " + name + " is already used."); } commandMap.put(name, command); } final void overrideCommands(Iterable commands) { commandMap.clear(); for (BlazeCommand command : commands) { addCommand(command); } for (BlazeModule module : blazeModules) { for (BlazeCommand command : module.getCommands()) { addCommand(command); } } } public CommandEnvironment initCommand() { return workspace.initCommand(); } @Nullable public InvocationPolicy getInvocationPolicy() { return invocationPolicy; } /** * Conditionally enable profiling. */ private final boolean initProfiler(CommandEnvironment env, CommonCommandOptions options, UUID buildID, long execStartTimeNanos) { OutputStream out = null; boolean recordFullProfilerData = false; ProfiledTaskKinds profiledTasks = ProfiledTaskKinds.NONE; try { if (options.profilePath != null) { Path profilePath = env.getWorkspace().getRelative(options.profilePath); recordFullProfilerData = options.recordFullProfilerData; out = new BufferedOutputStream(profilePath.getOutputStream(), 1024 * 1024); env.getReporter().handle(Event.info("Writing profile data to '" + profilePath + "'")); profiledTasks = ProfiledTaskKinds.ALL; } else if (options.alwaysProfileSlowOperations) { recordFullProfilerData = false; out = null; profiledTasks = ProfiledTaskKinds.SLOWEST; } if (profiledTasks != ProfiledTaskKinds.NONE) { Profiler.instance().start(profiledTasks, out, "Blaze profile for " + env.getOutputBase() + " at " + new Date() + ", build ID: " + buildID, recordFullProfilerData, clock, execStartTimeNanos); return true; } } catch (IOException e) { env.getReporter().handle(Event.error("Error while creating profile file: " + e.getMessage())); } return false; } public BlazeWorkspace getWorkspace() { return workspace; } /** * The directory in which blaze stores the server state - that is, the socket * file and a log. */ private Path getServerDirectory() { return getWorkspace().getDirectories().getOutputBase().getChild("server"); } /** * Returns the {@link QueryEnvironmentFactory} that should be used to create a * {@link AbstractBlazeQueryEnvironment}, whenever one is needed. */ public QueryEnvironmentFactory getQueryEnvironmentFactory() { return queryEnvironmentFactory; } /** * Returns the package factory. */ public PackageFactory getPackageFactory() { return packageFactory; } public ImmutableList getQueryOutputFormatters() { ImmutableList.Builder result = ImmutableList.builder(); result.addAll(OutputFormatter.getDefaultFormatters()); for (BlazeModule module : blazeModules) { result.addAll(module.getQueryOutputFormatters()); } return result.build(); } /** * Returns the rule class provider. */ public ConfiguredRuleClassProvider getRuleClassProvider() { return ruleClassProvider; } public Iterable getBlazeModules() { return blazeModules; } @SuppressWarnings("unchecked") public T getBlazeModule(Class moduleClass) { for (BlazeModule module : blazeModules) { if (module.getClass() == moduleClass) { return (T) module; } } return null; } public ConfigurationFactory getConfigurationFactory() { return configurationFactory; } /** * Returns a provider for project file objects. Can be null if no such provider was set by any of * the modules. */ @Nullable public ProjectFile.Provider getProjectFileProvider() { return projectFileProvider; } /** * Hook method called by the BlazeCommandDispatcher prior to the dispatch of * each command. * * @param options The CommonCommandOptions used by every command. * @throws AbruptExitException if this command is unsuitable to be run as specified */ void beforeCommand(CommandEnvironment env, CommonCommandOptions options, long execStartTimeNanos) throws AbruptExitException { // Conditionally enable profiling // We need to compensate for launchTimeNanos (measurements taken outside of the jvm). long startupTimeNanos = options.startupTime * 1000000L; if (initProfiler(env, options, env.getCommandId(), execStartTimeNanos - startupTimeNanos)) { Profiler profiler = Profiler.instance(); // Instead of logEvent() we're calling the low level function to pass the timings we took in // the launcher. We're setting the INIT phase marker so that it follows immediately the LAUNCH // phase. profiler.logSimpleTaskDuration(execStartTimeNanos - startupTimeNanos, 0, ProfilerTask.PHASE, ProfilePhase.LAUNCH.description); profiler.logSimpleTaskDuration(execStartTimeNanos, 0, ProfilerTask.PHASE, ProfilePhase.INIT.description); } if (options.memoryProfilePath != null) { Path memoryProfilePath = env.getWorkingDirectory().getRelative(options.memoryProfilePath); try { MemoryProfiler.instance().start(memoryProfilePath.getOutputStream()); } catch (IOException e) { env.getReporter().handle( Event.error("Error while creating memory profile file: " + e.getMessage())); } } // Initialize exit code to dummy value for afterCommand. storedExitCode.set(ExitCode.RESERVED.getNumericExitCode()); } /** * Posts the {@link CommandCompleteEvent}, so that listeners can tidy up. Called by {@link * #afterCommand}, and by BugReport when crashing from an exception in an async thread. */ public void notifyCommandComplete(int exitCode) { if (!storedExitCode.compareAndSet(ExitCode.RESERVED.getNumericExitCode(), exitCode)) { // This command has already been called, presumably because there is a race between the main // thread and a worker thread that crashed. Don't try to arbitrate the dispute. If the main // thread won the race (unlikely, but possible), this may be incorrectly logged as a success. return; } workspace.getSkyframeExecutor().getEventBus().post(new CommandCompleteEvent(exitCode)); } /** * Hook method called by the BlazeCommandDispatcher after the dispatch of each * command. */ @VisibleForTesting public void afterCommand(CommandEnvironment env, int exitCode) { // Remove any filters that the command might have added to the reporter. env.getReporter().setOutputFilter(OutputFilter.OUTPUT_EVERYTHING); notifyCommandComplete(exitCode); for (BlazeModule module : blazeModules) { module.afterCommand(); } env.getBlazeWorkspace().clearEventBus(); try { Profiler.instance().stop(); MemoryProfiler.instance().stop(); } catch (IOException e) { env.getReporter().handle(Event.error("Error while writing profile file: " + e.getMessage())); } } // Make sure we keep a strong reference to this logger, so that the // configuration isn't lost when the gc kicks in. private static Logger templateLogger = Logger.getLogger("com.google.devtools.build"); /** * Configures "com.google.devtools.build.*" loggers to the given * {@code level}. Note: This code relies on static state. */ public static void setupLogging(Level level) { templateLogger.setLevel(level); templateLogger.info("Log level: " + templateLogger.getLevel()); } /** * Returns the Clock-instance used for the entire build. Before, * individual classes (such as Profiler) used to specify the type * of clock (e.g. EpochClock) they wanted to use. This made it * difficult to get Blaze working on Windows as some of the clocks * available for Linux aren't (directly) available on Windows. * Setting the Blaze-wide clock upon construction of BlazeRuntime * allows injecting whatever Clock instance should be used from * BlazeMain. * * @return The Blaze-wide clock */ public Clock getClock() { return clock; } public OptionsProvider getStartupOptionsProvider() { return startupOptionsProvider; } public Map getCommandMap() { return commandMap; } public void shutdown() { for (BlazeModule module : blazeModules) { module.blazeShutdown(); } } /** * Returns the defaults package for the default settings. Should only be called by commands that * do not process {@link BuildOptions}, since build options can alter the contents of the * defaults package, which will not be reflected here. */ public String getDefaultsPackageContent() { return defaultsPackageContent; } /** * Returns the defaults package for the given options taken from an optionsProvider. */ public String getDefaultsPackageContent(OptionsClassProvider optionsProvider) { return ruleClassProvider.getDefaultsPackageContent(optionsProvider); } /** * Creates a BuildOptions class for the given options taken from an optionsProvider. */ public BuildOptions createBuildOptions(OptionsClassProvider optionsProvider) { return ruleClassProvider.createBuildOptions(optionsProvider); } /** * An EventBus exception handler that will report the exception to a remote server, if a * handler is registered. */ public static final class RemoteExceptionHandler implements SubscriberExceptionHandler { @Override public void handleException(Throwable exception, SubscriberExceptionContext context) { LOG.log(Level.SEVERE, "Failure in EventBus subscriber", exception); LoggingUtil.logToRemote(Level.SEVERE, "Failure in EventBus subscriber.", exception); } } /** * An EventBus exception handler that will call BugReport.handleCrash exiting * the current thread. */ public static final class BugReportingExceptionHandler implements SubscriberExceptionHandler { @Override public void handleException(Throwable exception, SubscriberExceptionContext context) { BugReport.handleCrash(exception); } } /** * Main method for the Blaze server startup. Note: This method logs * exceptions to remote servers. Do not add this to a unittest. */ public static void main(Iterable> moduleClasses, String[] args) { setupUncaughtHandler(args); List modules = createModules(moduleClasses); // blaze.cc will put --batch first if the user set it. if (args.length >= 1 && args[0].equals("--batch")) { // Run Blaze in batch mode. System.exit(batchMain(modules, args)); } LOG.info("Starting Blaze server with args " + Arrays.toString(args)); try { // Run Blaze in server mode. System.exit(serverMain(modules, OutErr.SYSTEM_OUT_ERR, args)); } catch (RuntimeException | Error e) { // A definite bug... BugReport.printBug(OutErr.SYSTEM_OUT_ERR, e); BugReport.sendBugReport(e, Arrays.asList(args)); System.exit(ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode()); throw e; // Shouldn't get here. } } @VisibleForTesting public static List createModules( Iterable> moduleClasses) { ImmutableList.Builder result = ImmutableList.builder(); for (Class moduleClass : moduleClasses) { try { BlazeModule module = moduleClass.newInstance(); result.add(module); } catch (Throwable e) { throw new IllegalStateException("Cannot instantiate module " + moduleClass.getName(), e); } } return result.build(); } /** * Generates a string form of a request to be written to the logs, * filtering the user environment to remove anything that looks private. * The current filter criteria removes any variable whose name includes * "auth", "pass", or "cookie". * * @param requestStrings * @return the filtered request to write to the log. */ @VisibleForTesting public static String getRequestLogString(List requestStrings) { StringBuilder buf = new StringBuilder(); buf.append('['); String sep = ""; for (String s : requestStrings) { buf.append(sep); if (s.startsWith("--client_env")) { int varStart = "--client_env=".length(); int varEnd = s.indexOf('=', varStart); String varName = s.substring(varStart, varEnd); if (suppressFromLog.matcher(varName).matches()) { buf.append("--client_env="); buf.append(varName); buf.append("=__private_value_removed__"); } else { buf.append(s); } } else { buf.append(s); } sep = ", "; } buf.append(']'); return buf.toString(); } /** * Command line options split in to two parts: startup options and everything else. */ @VisibleForTesting static class CommandLineOptions { private final List startupArgs; private final List otherArgs; CommandLineOptions(List startupArgs, List otherArgs) { this.startupArgs = ImmutableList.copyOf(startupArgs); this.otherArgs = ImmutableList.copyOf(otherArgs); } public List getStartupArgs() { return startupArgs; } public List getOtherArgs() { return otherArgs; } } /** * Splits given arguments into two lists - arguments matching options defined in this class * and everything else, while preserving order in each list. */ static CommandLineOptions splitStartupOptions( Iterable modules, String... args) { List prefixes = new ArrayList<>(); List startupFields = Lists.newArrayList(); for (Class defaultOptions : BlazeCommandUtils.getStartupOptions(modules)) { startupFields.addAll(ImmutableList.copyOf(defaultOptions.getFields())); } for (Field field : startupFields) { if (field.isAnnotationPresent(Option.class)) { prefixes.add("--" + field.getAnnotation(Option.class).name()); if (field.getType() == boolean.class || field.getType() == TriState.class) { prefixes.add("--no" + field.getAnnotation(Option.class).name()); } } } List startupArgs = new ArrayList<>(); List otherArgs = Lists.newArrayList(args); for (Iterator argi = otherArgs.iterator(); argi.hasNext(); ) { String arg = argi.next(); if (!arg.startsWith("--")) { break; // stop at command - all startup options would be specified before it. } for (String prefix : prefixes) { if (arg.startsWith(prefix)) { startupArgs.add(arg); argi.remove(); break; } } } return new CommandLineOptions(startupArgs, otherArgs); } private static void captureSigint() { final Thread mainThread = Thread.currentThread(); final AtomicInteger numInterrupts = new AtomicInteger(); final Runnable interruptWatcher = new Runnable() { @Override public void run() { int count = 0; // Not an actual infinite loop because it's run in a daemon thread. while (true) { count++; Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); LOG.warning("Slow interrupt number " + count + " in batch mode"); ThreadUtils.warnAboutSlowInterrupt(); } } }; new InterruptSignalHandler() { @Override protected void onSignal() { LOG.info("User interrupt"); OutErr.SYSTEM_OUT_ERR.printErrLn("Blaze received an interrupt"); mainThread.interrupt(); int curNumInterrupts = numInterrupts.incrementAndGet(); if (curNumInterrupts == 1) { Thread interruptWatcherThread = new Thread(interruptWatcher, "interrupt-watcher"); interruptWatcherThread.setDaemon(true); interruptWatcherThread.start(); } else if (curNumInterrupts == 2) { LOG.warning("Second --batch interrupt: Reverting to JVM SIGINT handler"); uninstall(); } } }; } /** * A main method that runs blaze commands in batch mode. The return value indicates the desired * exit status of the program. */ private static int batchMain(Iterable modules, String[] args) { captureSigint(); CommandLineOptions commandLineOptions = splitStartupOptions(modules, args); LOG.info("Running Blaze in batch mode with startup args " + commandLineOptions.getStartupArgs()); BlazeRuntime runtime; try { runtime = newRuntime(modules, parseOptions(modules, commandLineOptions.getStartupArgs())); } catch (OptionsParsingException e) { OutErr.SYSTEM_OUT_ERR.printErr(e.getMessage()); return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode(); } catch (AbruptExitException e) { OutErr.SYSTEM_OUT_ERR.printErr(e.getMessage()); return e.getExitCode().getNumericExitCode(); } BlazeCommandDispatcher dispatcher = new BlazeCommandDispatcher(runtime); try { LOG.info(getRequestLogString(commandLineOptions.getOtherArgs())); return dispatcher.exec(commandLineOptions.getOtherArgs(), OutErr.SYSTEM_OUT_ERR, LockingMode.ERROR_OUT, "batch client", runtime.getClock().currentTimeMillis()); } catch (BlazeCommandDispatcher.ShutdownBlazeServerException e) { return e.getExitStatus(); } catch (InterruptedException e) { // This is almost main(), so it's okay to just swallow it. We are exiting soon. return ExitCode.INTERRUPTED.getNumericExitCode(); } finally { runtime.shutdown(); dispatcher.shutdown(); } } /** * A main method that does not send email. The return value indicates the desired exit status of * the program. */ private static int serverMain(Iterable modules, OutErr outErr, String[] args) { InterruptSignalHandler sigintHandler = null; try { final RPCServer blazeServer = createBlazeRPCServer(modules, Arrays.asList(args)); // Register the signal handler. sigintHandler = new InterruptSignalHandler() { @Override protected void onSignal() { LOG.severe("User interrupt"); blazeServer.interrupt(); } }; blazeServer.serve(); return ExitCode.SUCCESS.getNumericExitCode(); } catch (OptionsParsingException e) { outErr.printErr(e.getMessage()); return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode(); } catch (IOException e) { outErr.printErr("I/O Error: " + e.getMessage()); return ExitCode.BUILD_FAILURE.getNumericExitCode(); } catch (AbruptExitException e) { outErr.printErr(e.getMessage()); return e.getExitCode().getNumericExitCode(); } finally { if (sigintHandler != null) { sigintHandler.uninstall(); } } } private static FileSystem fileSystemImplementation() { if ("0".equals(System.getProperty("io.bazel.UnixFileSystem"))) { // Ignore UnixFileSystem, to be used for bootstrapping. return OS.getCurrent() == OS.WINDOWS ? new WindowsFileSystem() : new JavaIoFileSystem(); } // The JNI-based UnixFileSystem is faster, but on Windows it is not available. return OS.getCurrent() == OS.WINDOWS ? new WindowsFileSystem() : new UnixFileSystem(); } /** * Creates and returns a new Blaze RPCServer. Call {@link RPCServer#serve()} to start the server. */ private static RPCServer createBlazeRPCServer( Iterable modules, List args) throws IOException, OptionsParsingException, AbruptExitException { OptionsProvider options = parseOptions(modules, args); BlazeServerStartupOptions startupOptions = options.getOptions(BlazeServerStartupOptions.class); BlazeRuntime runtime = newRuntime(modules, options); BlazeCommandDispatcher dispatcher = new BlazeCommandDispatcher(runtime); CommandExecutor commandExecutor = new CommandExecutor(runtime, dispatcher); if (startupOptions.commandPort != -1) { try { // This is necessary so that Bazel kind of works during bootstrapping, at which time the // gRPC server is not compiled in so that we don't need gRPC for bootstrapping. Class factoryClass = Class.forName( "com.google.devtools.build.lib.server.GrpcServerImpl$Factory"); RPCServer.Factory factory = (RPCServer.Factory) factoryClass.newInstance(); return factory.create(commandExecutor, runtime.getClock(), startupOptions.commandPort, runtime.getServerDirectory(), startupOptions.maxIdleSeconds); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new AbruptExitException("gRPC server not compiled in", ExitCode.BLAZE_INTERNAL_ERROR); } } else { return AfUnixServer.newServerWith(runtime.getClock(), commandExecutor, runtime.getServerDirectory(), runtime.workspace.getWorkspace(), startupOptions.maxIdleSeconds); } } private static Function sourceFunctionForMap(final Map map) { return new Function() { @Override public String apply(String input) { if (!map.containsKey(input)) { return "default"; } if (map.get(input).isEmpty()) { return "command line"; } return map.get(input); } }; } /** * Parses the command line arguments into a {@link OptionsParser} object. * *

This function needs to parse the --option_sources option manually so that the real option * parser can set the source for every option correctly. If that cannot be parsed or is missing, * we just report an unknown source for every startup option. */ private static OptionsProvider parseOptions( Iterable modules, List args) throws OptionsParsingException { Set> optionClasses = Sets.newHashSet(); optionClasses.addAll(BlazeCommandUtils.getStartupOptions(modules)); // First parse the command line so that we get the option_sources argument OptionsParser parser = OptionsParser.newOptionsParser(optionClasses); parser.setAllowResidue(false); parser.parse(OptionPriority.COMMAND_LINE, null, args); Function sourceFunction = sourceFunctionForMap(parser.getOptions(BlazeServerStartupOptions.class).optionSources); // Then parse the command line again, this time with the correct option sources parser = OptionsParser.newOptionsParser(optionClasses); parser.setAllowResidue(false); parser.parseWithSourceFunction(OptionPriority.COMMAND_LINE, sourceFunction, args); return parser; } /** * Creates a new blaze runtime, given the install and output base directories. * *

Note: This method can and should only be called once per startup, as it also creates the * filesystem object that will be used for the runtime. So it should only ever be called from the * main method of the Blaze program. * * @param options Blaze startup options. * * @return a new BlazeRuntime instance initialized with the given filesystem and directories, and * an error string that, if not null, describes a fatal initialization failure that makes * this runtime unsuitable for real commands */ private static BlazeRuntime newRuntime( Iterable blazeModules, OptionsProvider options) throws AbruptExitException { for (BlazeModule module : blazeModules) { module.globalInit(options); } BlazeServerStartupOptions startupOptions = options.getOptions(BlazeServerStartupOptions.class); if (startupOptions.batch && startupOptions.oomMoreEagerly) { new OomSignalHandler(); new RetainedHeapLimiter(startupOptions.oomMoreEagerlyThreshold).install(); } PathFragment workspaceDirectory = startupOptions.workspaceDirectory; PathFragment installBase = startupOptions.installBase; PathFragment outputBase = startupOptions.outputBase; OsUtils.maybeForceJNI(installBase); // Must be before first use of JNI. // From the point of view of the Java program --install_base and --output_base // are mandatory options, despite the comment in their declarations. if (installBase == null || !installBase.isAbsolute()) { // (includes "" default case) throw new IllegalArgumentException( "Bad --install_base option specified: '" + installBase + "'"); } if (outputBase != null && !outputBase.isAbsolute()) { // (includes "" default case) throw new IllegalArgumentException( "Bad --output_base option specified: '" + outputBase + "'"); } PathFragment outputPathFragment = BlazeDirectories.outputPathFromOutputBase( outputBase, workspaceDirectory, startupOptions.deepExecRoot); FileSystem fs = null; for (BlazeModule module : blazeModules) { FileSystem moduleFs = module.getFileSystem(options, outputPathFragment); if (moduleFs != null) { Preconditions.checkState(fs == null, "more than one module returns a file system"); fs = moduleFs; } } if (fs == null) { fs = fileSystemImplementation(); } Path.setFileSystemForSerialization(fs); Path installBasePath = fs.getPath(installBase); Path outputBasePath = fs.getPath(outputBase); Path workspaceDirectoryPath = null; if (!workspaceDirectory.equals(PathFragment.EMPTY_FRAGMENT)) { workspaceDirectoryPath = fs.getPath(workspaceDirectory); } if (fs instanceof UnixFileSystem) { ((UnixFileSystem) fs).setRootsWithAllowedHardlinks( // Some tests pass nulls for these paths, so remove these from the list Iterables.filter( Arrays.asList(installBasePath, outputBasePath, workspaceDirectoryPath), Predicates.notNull())); } BlazeDirectories directories = new BlazeDirectories(installBasePath, outputBasePath, workspaceDirectoryPath, startupOptions.deepExecRoot, startupOptions.installMD5); Clock clock = BlazeClock.instance(); BinTools binTools; try { binTools = BinTools.forProduction(directories); } catch (IOException e) { throw new AbruptExitException( "Cannot enumerate embedded binaries: " + e.getMessage(), ExitCode.LOCAL_ENVIRONMENTAL_ERROR); } BlazeRuntime.Builder runtimeBuilder = new BlazeRuntime.Builder().setDirectories(directories) .setStartupOptionsProvider(options) .setBinTools(binTools) .setClock(clock) // TODO(bazel-team): Make BugReportingExceptionHandler the default. // See bug "Make exceptions in EventBus subscribers fatal" .setEventBusExceptionHandler( startupOptions.fatalEventBusExceptions || !BlazeVersionInfo.instance().isReleasedBlaze() ? new BlazeRuntime.BugReportingExceptionHandler() : new BlazeRuntime.RemoteExceptionHandler()); if (System.getenv("TEST_TMPDIR") != null && System.getenv("NO_CRASH_ON_LOGGING_IN_TEST") == null) { LoggingUtil.installRemoteLogger(getTestCrashLogger()); } for (BlazeModule blazeModule : blazeModules) { runtimeBuilder.addBlazeModule(blazeModule); } runtimeBuilder.addCommands(getBuiltinCommandList()); BlazeRuntime runtime = runtimeBuilder.build(); AutoProfiler.setClock(runtime.getClock()); BugReport.setRuntime(runtime); return runtime; } /** * Returns a logger that crashes as soon as it's written to, since tests should not cause events * that would be logged. */ @VisibleForTesting public static Future getTestCrashLogger() { Logger crashLogger = Logger.getAnonymousLogger(); crashLogger.addHandler( new Handler() { @Override public void publish(LogRecord record) { System.err.println("Remote logging disabled for testing, forcing abrupt shutdown."); System.err.printf("%s#%s: %s\n", record.getSourceClassName(), record.getSourceMethodName(), record.getMessage()); Throwable e = record.getThrown(); if (e != null) { e.printStackTrace(); } Runtime.getRuntime().halt(ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode()); } @Override public void flush() { throw new IllegalStateException(); } @Override public void close() { throw new IllegalStateException(); } }); return Futures.immediateFuture(crashLogger); } /** * Make sure async threads cannot be orphaned. This method makes sure bugs are reported to * telemetry and the proper exit code is reported. */ private static void setupUncaughtHandler(final String[] args) { Thread.setDefaultUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable throwable) { BugReport.handleCrash(throwable, args); } }); } /** * Returns an immutable list containing new instances of each Blaze command. */ @VisibleForTesting public static List getBuiltinCommandList() { return ImmutableList.of( new BuildCommand(), new CanonicalizeCommand(), new CleanCommand(), new DumpCommand(), new HelpCommand(), new InfoCommand(), new MobileInstallCommand(), new ProfileCommand(), new QueryCommand(), new RunCommand(), new ShutdownCommand(), new TestCommand(), new VersionCommand()); } /** * A builder for {@link BlazeRuntime} objects. The only required fields are the {@link * BlazeDirectories}, and the {@link RuleClassProvider} (except for testing). All other fields * have safe default values. * *

If a {@link ConfigurationFactory} is set, then the builder ignores the host system flag. *

The default behavior of the BlazeRuntime's EventBus is to exit when a subscriber throws * an exception. Please plan appropriately. */ public static class Builder { private BlazeDirectories directories; private ConfigurationFactory configurationFactory; private Clock clock; private OptionsProvider startupOptionsProvider; private final List blazeModules = new ArrayList<>(); private SubscriberExceptionHandler eventBusExceptionHandler = new RemoteExceptionHandler(); private BinTools binTools; private UUID instanceId; private final List commands = new ArrayList<>(); private InvocationPolicy invocationPolicy = InvocationPolicy.getDefaultInstance(); public BlazeRuntime build() throws AbruptExitException { Preconditions.checkNotNull(directories); Preconditions.checkNotNull(startupOptionsProvider); Clock clock = (this.clock == null) ? BlazeClock.instance() : this.clock; UUID instanceId = (this.instanceId == null) ? UUID.randomUUID() : this.instanceId; Preconditions.checkNotNull(clock); for (BlazeModule module : blazeModules) { module.blazeStartup(startupOptionsProvider, BlazeVersionInfo.instance(), instanceId, directories, clock); } QueryEnvironmentFactory queryEnvironmentFactory = null; for (BlazeModule module : blazeModules) { QueryEnvironmentFactory queryEnvFactory = module.getQueryEnvironmentFactory(); if (queryEnvFactory != null) { Preconditions.checkState(queryEnvironmentFactory == null, "At most one query environment factory supported. But found two: %s and %s", queryEnvFactory, queryEnvironmentFactory); queryEnvironmentFactory = queryEnvFactory; } } if (queryEnvironmentFactory == null) { queryEnvironmentFactory = new QueryEnvironmentFactory(); } ConfiguredRuleClassProvider.Builder ruleClassBuilder = new ConfiguredRuleClassProvider.Builder(); for (BlazeModule module : blazeModules) { module.initializeRuleClasses(ruleClassBuilder); } Map platformRegexps = null; { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); for (BlazeModule module : blazeModules) { builder.putAll(module.getPlatformSetRegexps()); } platformRegexps = builder.build(); if (platformRegexps.isEmpty()) { platformRegexps = null; // Use the default. } } ConfiguredRuleClassProvider ruleClassProvider = ruleClassBuilder.build(); List extensions = new ArrayList<>(); for (BlazeModule module : blazeModules) { extensions.add(module.getPackageEnvironmentExtension()); } PackageFactory packageFactory = new PackageFactory( ruleClassProvider, platformRegexps, extensions, BlazeVersionInfo.instance().getVersion()); if (configurationFactory == null) { configurationFactory = new ConfigurationFactory( ruleClassProvider.getConfigurationCollectionFactory(), ruleClassProvider.getConfigurationFragments()); } ProjectFile.Provider projectFileProvider = null; for (BlazeModule module : blazeModules) { ProjectFile.Provider candidate = module.createProjectFileProvider(); if (candidate != null) { Preconditions.checkState(projectFileProvider == null, "more than one module defines a project file provider"); projectFileProvider = candidate; } } invocationPolicy = createInvocationPolicyFromModules(invocationPolicy, blazeModules); BlazeRuntime runtime = new BlazeRuntime(queryEnvironmentFactory, packageFactory, ruleClassProvider, configurationFactory, clock, startupOptionsProvider, ImmutableList.copyOf(blazeModules), eventBusExceptionHandler, projectFileProvider, invocationPolicy, commands); runtime.initWorkspace(directories, binTools); return runtime; } public Builder setBinTools(BinTools binTools) { this.binTools = binTools; return this; } public Builder setInvocationPolicy(InvocationPolicy invocationPolicy) { this.invocationPolicy = invocationPolicy; return this; } public Builder setDirectories(BlazeDirectories directories) { this.directories = directories; return this; } /** * Creates and sets a new {@link BlazeDirectories} instance with the given * parameters. */ public Builder setDirectories(Path installBase, Path outputBase, Path workspace) { this.directories = new BlazeDirectories(installBase, outputBase, workspace); return this; } public Builder setConfigurationFactory(ConfigurationFactory configurationFactory) { this.configurationFactory = configurationFactory; return this; } public Builder setClock(Clock clock) { this.clock = clock; return this; } public Builder setStartupOptionsProvider(OptionsProvider startupOptionsProvider) { this.startupOptionsProvider = startupOptionsProvider; return this; } public Builder addBlazeModule(BlazeModule blazeModule) { blazeModules.add(blazeModule); return this; } public Builder setInstanceId(UUID id) { instanceId = id; return this; } @VisibleForTesting public Builder setEventBusExceptionHandler( SubscriberExceptionHandler eventBusExceptionHandler) { this.eventBusExceptionHandler = eventBusExceptionHandler; return this; } public Builder addCommands(BlazeCommand... commands) { this.commands.addAll(Arrays.asList(commands)); return this; } public Builder addCommands(Iterable commands) { Iterables.addAll(this.commands, commands); return this; } } }