blob: 7f48615ee2bca2789421264182216a0fb6614dd3 [file] [log] [blame]
// 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.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.
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);
// 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;
Path mountPoint = sandboxBase.getRelative("sandboxfs");
// 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 (sandboxfsProcess != null) {
if (options.sandboxDebug) {
env.getReporter()
.handle(
Event.info(
"Unmounting sandboxfs instance left behind on "
+ mountPoint
+ " by a previous command"));
}
sandboxfsProcess.destroy();
sandboxfsProcess = null;
}
if (sandboxBase.exists()) {
FileSystemUtils.deleteTree(sandboxBase);
}
sandboxBase.createDirectoryAndParents();
if (options.useSandboxfs) {
mountPoint.createDirectory();
Path logFile = sandboxBase.getRelative("sandboxfs.log");
if (sandboxfsProcess == null) {
if (options.sandboxDebug) {
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("", ImmutableList.of("sandboxed"));
}
}
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,
env.getBlazeWorkspace().getBinTools());
}
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 (sandboxSpawnRunner.canExec(spawn)) {
return sandboxSpawnRunner.exec(spawn, context);
} else {
return fallbackSpawnRunner.exec(spawn, context);
}
}
@Override
public boolean canExec(Spawn spawn) {
return sandboxSpawnRunner.canExec(spawn) || fallbackSpawnRunner.canExec(spawn);
}
}
/**
* Unmounts an existing sandboxfs instance unless the user asked not to by providing the {@code
* --sandbox_debug} flag.
*/
private void unmountSandboxfs() {
if (sandboxfsProcess != null) {
if (shouldCleanupSandboxBase) {
sandboxfsProcess.destroy();
sandboxfsProcess = null;
} else {
checkNotNull(env, "env not initialized; was beforeCommand called?");
env.getReporter()
.handle(Event.info("Leaving sandboxfs mounted because of --sandbox_debug"));
}
}
}
/** Silently tries to unmount an existing sandboxfs instance, ignoring errors. */
private void tryUnmountSandboxfsOnShutdown() {
if (sandboxfsProcess != null) {
sandboxfsProcess.destroy();
sandboxfsProcess = null;
}
}
@Subscribe
public void buildComplete(@SuppressWarnings("unused") BuildCompleteEvent event) {
unmountSandboxfs();
}
@Subscribe
public void buildInterrupted(@SuppressWarnings("unused") BuildInterruptedEvent event) {
unmountSandboxfs();
}
@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;
}
@Override
public void blazeShutdown() {
tryUnmountSandboxfsOnShutdown();
}
@Override
public void blazeShutdownOnCrash() {
tryUnmountSandboxfsOnShutdown();
}
}