| // Copyright 2015 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.sandbox; | |
| import static com.google.common.base.Preconditions.checkNotNull; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.devtools.build.lib.actions.ExecException; | |
| import com.google.devtools.build.lib.actions.ExecutorInitException; | |
| import com.google.devtools.build.lib.actions.ResourceManager; | |
| import com.google.devtools.build.lib.actions.Spawn; | |
| import com.google.devtools.build.lib.actions.SpawnActionContext; | |
| import com.google.devtools.build.lib.actions.SpawnResult; | |
| import com.google.devtools.build.lib.actions.Spawns; | |
| import com.google.devtools.build.lib.buildtool.BuildRequest; | |
| import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent; | |
| import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.exec.ExecutorBuilder; | |
| import com.google.devtools.build.lib.exec.SpawnRunner; | |
| import com.google.devtools.build.lib.exec.apple.XcodeLocalEnvProvider; | |
| import com.google.devtools.build.lib.exec.local.LocalEnvProvider; | |
| import com.google.devtools.build.lib.exec.local.LocalExecutionOptions; | |
| import com.google.devtools.build.lib.exec.local.LocalSpawnRunner; | |
| import com.google.devtools.build.lib.exec.local.PosixLocalEnvProvider; | |
| import com.google.devtools.build.lib.runtime.BlazeModule; | |
| import com.google.devtools.build.lib.runtime.Command; | |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; | |
| import com.google.devtools.build.lib.util.Fingerprint; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.common.options.OptionsBase; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| import javax.annotation.Nullable; | |
| /** | |
| * This module provides the Sandbox spawn strategy. | |
| */ | |
| public final class SandboxModule extends BlazeModule { | |
| /** Environment for the running command. */ | |
| private @Nullable CommandEnvironment env; | |
| /** Path to the location of the sandboxes. */ | |
| private @Nullable Path sandboxBase; | |
| /** Instance of the sandboxfs process in use, if enabled. */ | |
| private @Nullable SandboxfsProcess sandboxfsProcess; | |
| /** | |
| * Whether to remove the sandbox worker directories after a build or not. Useful for debugging | |
| * to inspect the state of files on failures. | |
| */ | |
| private boolean shouldCleanupSandboxBase; | |
| @Override | |
| public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) { | |
| return "build".equals(command.name()) | |
| ? ImmutableList.of(SandboxOptions.class) | |
| : ImmutableList.of(); | |
| } | |
| /** Computes the path to the sandbox base tree for the given running command. */ | |
| private static Path computeSandboxBase(SandboxOptions options, CommandEnvironment env) | |
| throws IOException { | |
| if (options.sandboxBase.isEmpty()) { | |
| return env.getOutputBase().getRelative("sandbox"); | |
| } else { | |
| String dirName = | |
| String.format( | |
| "%s-sandbox.%s", | |
| env.getRuntime().getProductName(), | |
| Fingerprint.getHexDigest(env.getOutputBase().toString())); | |
| FileSystem fileSystem = env.getRuntime().getFileSystem(); | |
| Path resolvedSandboxBase = fileSystem.getPath(options.sandboxBase).resolveSymbolicLinks(); | |
| return resolvedSandboxBase.getRelative(dirName); | |
| } | |
| } | |
| @Override | |
| public void beforeCommand(CommandEnvironment env) { | |
| // We can't assert that env is null because the Blaze runtime does not guarantee that | |
| // afterCommand() will be called if the command fails due to, e.g. a syntax error. | |
| this.env = env; | |
| env.getEventBus().register(this); | |
| // Don't attempt cleanup unless the executor is initialized. | |
| sandboxfsProcess = null; | |
| shouldCleanupSandboxBase = false; | |
| } | |
| @Override | |
| public void executorInit(CommandEnvironment cmdEnv, BuildRequest request, ExecutorBuilder builder) | |
| throws ExecutorInitException { | |
| checkNotNull(env, "env not initialized; was beforeCommand called?"); | |
| try { | |
| setup(cmdEnv, builder); | |
| } catch (IOException e) { | |
| throw new ExecutorInitException("Failed to initialize sandbox", e); | |
| } | |
| } | |
| private void setup(CommandEnvironment cmdEnv, ExecutorBuilder builder) | |
| throws IOException { | |
| SandboxOptions options = checkNotNull(env.getOptions().getOptions(SandboxOptions.class)); | |
| sandboxBase = computeSandboxBase(options, env); | |
| // Ensure that each build starts with a clean sandbox base directory. Otherwise using the `id` | |
| // that is provided by SpawnExecutionPolicy#getId to compute a base directory for a sandbox | |
| // might result in an already existing directory. | |
| if (sandboxBase.exists()) { | |
| FileSystemUtils.deleteTree(sandboxBase); | |
| } | |
| sandboxBase.createDirectoryAndParents(); | |
| if (options.useSandboxfs) { | |
| Path mountPoint = sandboxBase.getRelative("sandboxfs"); | |
| mountPoint.createDirectory(); | |
| Path logFile = sandboxBase.getRelative("sandboxfs.log"); | |
| env.getReporter().handle(Event.info("Mounting sandboxfs instance on " + mountPoint)); | |
| sandboxfsProcess = RealSandboxfsProcess.mount( | |
| PathFragment.create(options.sandboxfsPath), mountPoint, logFile); | |
| } | |
| Duration timeoutKillDelay = | |
| cmdEnv.getOptions().getOptions(LocalExecutionOptions.class).getLocalSigkillGraceSeconds(); | |
| boolean processWrapperSupported = ProcessWrapperSandboxedSpawnRunner.isSupported(cmdEnv); | |
| boolean linuxSandboxSupported = LinuxSandboxedSpawnRunner.isSupported(cmdEnv); | |
| boolean darwinSandboxSupported = DarwinSandboxedSpawnRunner.isSupported(cmdEnv); | |
| // This works on most platforms, but isn't the best choice, so we put it first and let later | |
| // platform-specific sandboxing strategies become the default. | |
| if (processWrapperSupported) { | |
| SpawnRunner spawnRunner = | |
| withFallback( | |
| cmdEnv, | |
| new ProcessWrapperSandboxedSpawnRunner( | |
| cmdEnv, sandboxBase, cmdEnv.getRuntime().getProductName(), timeoutKillDelay)); | |
| builder.addActionContext( | |
| new ProcessWrapperSandboxedStrategy(cmdEnv.getExecRoot(), spawnRunner)); | |
| } | |
| if (options.enableDockerSandbox) { | |
| // This strategy uses Docker to execute spawns. It should work on all platforms that support | |
| // Docker. | |
| Path pathToDocker = getPathToDockerClient(cmdEnv); | |
| // DockerSandboxedSpawnRunner.isSupported is expensive! It runs docker as a subprocess, and | |
| // docker hangs sometimes. | |
| if (pathToDocker != null && DockerSandboxedSpawnRunner.isSupported(cmdEnv, pathToDocker)) { | |
| String defaultImage = options.dockerImage; | |
| boolean useCustomizedImages = options.dockerUseCustomizedImages; | |
| SpawnRunner spawnRunner = | |
| withFallback( | |
| cmdEnv, | |
| new DockerSandboxedSpawnRunner( | |
| cmdEnv, | |
| pathToDocker, | |
| sandboxBase, | |
| defaultImage, | |
| timeoutKillDelay, | |
| useCustomizedImages)); | |
| builder.addActionContext( | |
| new DockerSandboxedStrategy(cmdEnv.getExecRoot(), spawnRunner)); | |
| } | |
| } else if (options.dockerVerbose) { | |
| cmdEnv.getReporter().handle(Event.info( | |
| "Docker sandboxing disabled. Use the '--experimental_enable_docker_sandbox' command " | |
| + "line option to enable it")); | |
| } | |
| // This is the preferred sandboxing strategy on Linux. | |
| if (linuxSandboxSupported) { | |
| SpawnRunner spawnRunner = | |
| withFallback( | |
| cmdEnv, | |
| LinuxSandboxedStrategy.create( | |
| cmdEnv, sandboxBase, timeoutKillDelay, sandboxfsProcess)); | |
| builder.addActionContext(new LinuxSandboxedStrategy(cmdEnv.getExecRoot(), spawnRunner)); | |
| } | |
| // This is the preferred sandboxing strategy on macOS. | |
| if (darwinSandboxSupported) { | |
| SpawnRunner spawnRunner = | |
| withFallback( | |
| cmdEnv, | |
| new DarwinSandboxedSpawnRunner( | |
| cmdEnv, sandboxBase, timeoutKillDelay, sandboxfsProcess)); | |
| builder.addActionContext(new DarwinSandboxedStrategy(cmdEnv.getExecRoot(), spawnRunner)); | |
| } | |
| if (processWrapperSupported || linuxSandboxSupported || darwinSandboxSupported) { | |
| // This makes the "sandboxed" strategy available via --spawn_strategy=sandboxed, | |
| // but it is not necessarily the default. | |
| builder.addStrategyByContext(SpawnActionContext.class, "sandboxed"); | |
| // This makes the "sandboxed" strategy the default Spawn strategy, unless it is | |
| // overridden by a later BlazeModule. | |
| builder.addStrategyByMnemonic("", "sandboxed"); | |
| } | |
| // Do not remove the sandbox base when --sandbox_debug was specified so that people can check | |
| // out the contents of the generated sandbox directories. | |
| shouldCleanupSandboxBase = !options.sandboxDebug; | |
| } | |
| private static Path getPathToDockerClient(CommandEnvironment cmdEnv) { | |
| String path = cmdEnv.getClientEnv().getOrDefault("PATH", ""); | |
| // TODO(philwo): Does this return the correct result if one of the elements intentionally ends | |
| // in white space? | |
| Splitter pathSplitter = | |
| Splitter.on(OS.getCurrent() == OS.WINDOWS ? ';' : ':').trimResults().omitEmptyStrings(); | |
| FileSystem fs = cmdEnv.getRuntime().getFileSystem(); | |
| for (String pathElement : pathSplitter.split(path)) { | |
| // Sometimes the PATH contains the non-absolute entry "." - this resolves it against the | |
| // current working directory. | |
| pathElement = new File(pathElement).getAbsolutePath(); | |
| try { | |
| for (Path dentry : fs.getPath(pathElement).getDirectoryEntries()) { | |
| if (dentry.getBaseName().replace(".exe", "").equals("docker")) { | |
| return dentry; | |
| } | |
| } | |
| } catch (IOException e) { | |
| continue; | |
| } | |
| } | |
| return null; | |
| } | |
| private static SpawnRunner withFallback(CommandEnvironment env, SpawnRunner sandboxSpawnRunner) { | |
| return new SandboxFallbackSpawnRunner(sandboxSpawnRunner, createFallbackRunner(env)); | |
| } | |
| private static SpawnRunner createFallbackRunner(CommandEnvironment env) { | |
| LocalExecutionOptions localExecutionOptions = | |
| env.getOptions().getOptions(LocalExecutionOptions.class); | |
| LocalEnvProvider localEnvProvider = | |
| OS.getCurrent() == OS.DARWIN | |
| ? new XcodeLocalEnvProvider(env.getClientEnv()) | |
| : new PosixLocalEnvProvider(env.getClientEnv()); | |
| return | |
| new LocalSpawnRunner( | |
| env.getExecRoot(), | |
| localExecutionOptions, | |
| ResourceManager.instance(), | |
| localEnvProvider); | |
| } | |
| private static final class SandboxFallbackSpawnRunner implements SpawnRunner { | |
| private final SpawnRunner sandboxSpawnRunner; | |
| private final SpawnRunner fallbackSpawnRunner; | |
| SandboxFallbackSpawnRunner(SpawnRunner sandboxSpawnRunner, SpawnRunner fallbackSpawnRunner) { | |
| this.sandboxSpawnRunner = sandboxSpawnRunner; | |
| this.fallbackSpawnRunner = fallbackSpawnRunner; | |
| } | |
| @Override | |
| public String getName() { | |
| return "sandbox-fallback"; | |
| } | |
| @Override | |
| public SpawnResult exec(Spawn spawn, SpawnExecutionContext context) | |
| throws InterruptedException, IOException, ExecException { | |
| if (!Spawns.mayBeSandboxed(spawn)) { | |
| return fallbackSpawnRunner.exec(spawn, context); | |
| } else { | |
| return sandboxSpawnRunner.exec(spawn, context); | |
| } | |
| } | |
| } | |
| private void unmountSandboxfs(String reason) { | |
| if (sandboxfsProcess != null) { | |
| checkNotNull(env, "env not initialized; was beforeCommand called?"); | |
| env.getReporter().handle(Event.info(reason)); | |
| // TODO(jmmv): This can be incredibly slow. Either fix sandboxfs or do it in the background. | |
| sandboxfsProcess.destroy(); | |
| sandboxfsProcess = null; | |
| } | |
| } | |
| @Subscribe | |
| public void buildComplete(@SuppressWarnings("unused") BuildCompleteEvent event) { | |
| unmountSandboxfs("Build complete; unmounting sandboxfs..."); | |
| } | |
| @Subscribe | |
| public void buildInterrupted(@SuppressWarnings("unused") BuildInterruptedEvent event) { | |
| unmountSandboxfs("Build interrupted; unmounting sandboxfs..."); | |
| } | |
| @Override | |
| public void afterCommand() { | |
| checkNotNull(env, "env not initialized; was beforeCommand called?"); | |
| if (shouldCleanupSandboxBase) { | |
| try { | |
| FileSystemUtils.deleteTree(sandboxBase); | |
| } catch (IOException e) { | |
| env.getReporter().handle(Event.warn("Failed to delete sandbox base " + sandboxBase | |
| + ": " + e)); | |
| } | |
| shouldCleanupSandboxBase = false; | |
| } | |
| checkState(sandboxfsProcess == null, "sandboxfs instance should have been shut down at this " | |
| + "point; were the buildComplete/buildInterrupted events sent?"); | |
| sandboxBase = null; | |
| env.getEventBus().unregister(this); | |
| env = null; | |
| } | |
| } |