| // 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.collect.ImmutableList.toImmutableList; | |
| import static java.util.stream.Collectors.toCollection; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.common.flogger.GoogleLogger; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.exec.ExecutionOptions; | |
| import com.google.devtools.build.lib.exec.SpawnRunner; | |
| import com.google.devtools.build.lib.exec.SpawnStrategyRegistry; | |
| import com.google.devtools.build.lib.exec.TreeDeleter; | |
| import com.google.devtools.build.lib.exec.local.LocalExecutionOptions; | |
| import com.google.devtools.build.lib.profiler.Profiler; | |
| import com.google.devtools.build.lib.profiler.SilentCloseable; | |
| import com.google.devtools.build.lib.runtime.BlazeModule; | |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; | |
| import com.google.devtools.build.lib.runtime.commands.events.CleanStartingEvent; | |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; | |
| import com.google.devtools.build.lib.server.FailureDetails.Sandbox; | |
| import com.google.devtools.build.lib.util.AbruptExitException; | |
| import com.google.devtools.build.lib.util.DetailedExitCode; | |
| import com.google.devtools.build.lib.util.Fingerprint; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.vfs.Dirent; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Symlinks; | |
| import com.google.devtools.common.options.OptionsBase; | |
| import com.google.devtools.common.options.TriState; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Set; | |
| import javax.annotation.Nullable; | |
| /** This module provides the Sandbox spawn strategy. */ | |
| public final class SandboxModule extends BlazeModule { | |
| private static final String MAC_INDEX_FILE = ".DS_Store"; | |
| private static final ImmutableSet<String> SANDBOX_BASE_PERSISTENT_DIRS = | |
| ImmutableSet.of( | |
| MAC_INDEX_FILE, | |
| SandboxStash.SANDBOX_STASH_BASE, | |
| SandboxStash.TEMPORARY_SANDBOX_STASH_BASE, | |
| AsynchronousTreeDeleter.MOVED_TRASH_DIR); | |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | |
| /** Tracks whether we are issuing the very first build within this Bazel server instance. */ | |
| private static boolean firstBuild = true; | |
| /** Environment for the running command. */ | |
| @Nullable private CommandEnvironment env; | |
| /** Path to the location of the sandboxes. */ | |
| @Nullable private Path sandboxBase; | |
| /** | |
| * Collection of spawn runner instantiated during the executor setup. | |
| * | |
| * <p>We need this information to clean up the heavy subdirectories of the sandbox base on build | |
| * completion but to avoid wiping the whole sandbox base itself, which could be problematic across | |
| * builds. | |
| */ | |
| private final Set<SpawnRunner> spawnRunners = new HashSet<>(); | |
| /** | |
| * Handler to process expensive tree deletions, potentially outside of the critical path. | |
| * | |
| * <p>Sandboxing creates one separate tree for each action, and this tree is used to run the | |
| * action commands in. These trees are disjoint for all actions and have unique identifiers. | |
| * Therefore, there is no need for their deletion (which can be very expensive) to happen in the | |
| * critical path -- so if the user so wishes, we process those deletions asynchronously. | |
| */ | |
| // Package-private for testing | |
| @Nullable TreeDeleter treeDeleter; | |
| /** | |
| * 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(String commandName) { | |
| return commandName.equals("build") | |
| ? 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.getSandboxBase().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(); | |
| if (OS.getCurrent() == OS.DARWIN) { | |
| // Don't resolve symlinks on macOS: See https://github.com/bazelbuild/bazel/issues/13766 | |
| return fileSystem.getPath(options.getSandboxBase()).getRelative(dirName); | |
| } | |
| Path resolvedSandboxBase = | |
| fileSystem.getPath(options.getSandboxBase()).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 registerSpawnStrategies( | |
| SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env) | |
| throws AbruptExitException, InterruptedException { | |
| checkNotNull(env, "env not initialized; was beforeCommand called?"); | |
| try { | |
| setup(env, registryBuilder); | |
| } catch (IOException e) { | |
| throw new AbruptExitException( | |
| DetailedExitCode.of( | |
| FailureDetail.newBuilder() | |
| .setMessage(String.format("Failed to initialize sandbox: %s", e.getMessage())) | |
| .setSandbox( | |
| Sandbox.newBuilder().setCode(Sandbox.Code.INITIALIZATION_FAILURE).build()) | |
| .build()), | |
| e); | |
| } | |
| } | |
| /** | |
| * Returns true if windows-sandbox should be used for this build. | |
| * | |
| * <p>Returns true if requested in ["auto", "yes"] and binary is valid. Throws an error if state | |
| * is "yes" and binary is not valid. | |
| * | |
| * @param requested whether windows-sandbox use was requested or not | |
| * @param binary path of the windows-sandbox binary to use, can be absolute or relative path | |
| * @return true if windows-sandbox can and should be used; false otherwise | |
| * @throws IOException if there are problems trying to determine the status of windows-sandbox | |
| */ | |
| private static boolean shouldUseWindowsSandbox( | |
| TriState requested, PathFragment binary, ImmutableMap<String, String> clientEnv) | |
| throws IOException { | |
| return switch (requested) { | |
| case AUTO -> WindowsSandboxUtil.isAvailable(binary, clientEnv); | |
| case NO -> false; | |
| case YES -> { | |
| if (!WindowsSandboxUtil.isAvailable(binary, clientEnv)) { | |
| throw new IOException( | |
| "windows-sandbox explicitly requested but \"" | |
| + binary | |
| + "\" could not be found or is not valid"); | |
| } | |
| yield true; | |
| } | |
| }; | |
| } | |
| private void setup(CommandEnvironment cmdEnv, SpawnStrategyRegistry.Builder builder) | |
| throws IOException, InterruptedException { | |
| SandboxOptions options = checkNotNull(env.getOptions().getOptions(SandboxOptions.class)); | |
| sandboxBase = computeSandboxBase(options, env); | |
| Path trashBase = sandboxBase.getRelative(AsynchronousTreeDeleter.MOVED_TRASH_DIR); | |
| // 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.getSandboxDebug(); | |
| // If there happens to be any live tree deleter from a previous build and it's different than | |
| // the one we want now, leave it alone (i.e. don't attempt to wait for pending deletions). Its | |
| // deletions shouldn't overlap any new directories we create during this build (because the | |
| // identifiers in the subdirectories will be different). | |
| if (options.getAsyncTreeDeleteIdleThreads() == 0) { | |
| if (!(treeDeleter instanceof SynchronousTreeDeleter)) { | |
| treeDeleter = new SynchronousTreeDeleter(); | |
| } | |
| } else { | |
| if (!(treeDeleter instanceof AsynchronousTreeDeleter treeDeleter) | |
| || !treeDeleter.getTrashBase().equals(trashBase)) { | |
| if (treeDeleter != null) { | |
| treeDeleter.shutdown(); | |
| } | |
| treeDeleter = new AsynchronousTreeDeleter(trashBase); | |
| firstBuild = true; | |
| } | |
| } | |
| // Whenever a build starts, ensure the tree deleter runs in single-threaded mode so it | |
| // does not compete with the active build for CPU and I/O resources. | |
| if (treeDeleter instanceof AsynchronousTreeDeleter asyncTreeDeleter) { | |
| asyncTreeDeleter.setThreads(1); | |
| } | |
| try (SilentCloseable c = Profiler.instance().profile("SandboxStash.initialize")) { | |
| SandboxStash.initialize(env.getWorkspaceName(), sandboxBase, options, treeDeleter); | |
| } | |
| // SpawnExecutionPolicy#getId returns unique base directories for each sandboxed action during | |
| // the life of a Bazel server instance so we don't need to worry about stale directories from | |
| // previous builds. However, on the very first build of an instance of the server, we must | |
| // wipe old contents to avoid reusing stale directories. | |
| if (firstBuild && sandboxBase.exists()) { | |
| cleanSandboxBaseOnFirstBuild(sandboxBase, trashBase, options.getAsyncTreeDeleteIdleThreads()); | |
| } | |
| firstBuild = false; | |
| sandboxBase.createDirectoryAndParents(); | |
| trashBase.createDirectory(); | |
| PathFragment windowsSandboxPath = PathFragment.create(options.getWindowsSandboxPath()); | |
| boolean windowsSandboxSupported; | |
| try (SilentCloseable c = Profiler.instance().profile("shouldUseWindowsSandbox")) { | |
| windowsSandboxSupported = | |
| shouldUseWindowsSandbox( | |
| options.getUseWindowsSandbox(), windowsSandboxPath, cmdEnv.getClientEnv()); | |
| } | |
| Duration timeoutKillDelay = | |
| cmdEnv | |
| .getOptions() | |
| .getOptions(LocalExecutionOptions.class) | |
| .getLocalSigkillGraceSecondsDuration(); | |
| boolean processWrapperSupported = ProcessWrapperSandboxedSpawnRunner.isSupported(cmdEnv); | |
| boolean linuxSandboxSupported = LinuxSandboxedSpawnRunner.isSupported(cmdEnv); | |
| boolean darwinSandboxSupported = DarwinSandboxedSpawnRunner.isSupported(cmdEnv); | |
| ExecutionOptions executionOptions = | |
| checkNotNull(cmdEnv.getOptions().getOptions(ExecutionOptions.class)); | |
| // 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 = | |
| new ProcessWrapperSandboxedSpawnRunner(cmdEnv, sandboxBase, treeDeleter); | |
| spawnRunners.add(spawnRunner); | |
| builder.registerStrategy( | |
| new ProcessWrapperSandboxedStrategy(spawnRunner, executionOptions), | |
| "sandboxed", | |
| "processwrapper-sandbox"); | |
| } | |
| if (options.getEnableDockerSandbox()) { | |
| // 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.getDockerImage(); | |
| boolean useCustomizedImages = options.getDockerUseCustomizedImages(); | |
| SpawnRunner spawnRunner = | |
| new DockerSandboxedSpawnRunner( | |
| cmdEnv, pathToDocker, sandboxBase, defaultImage, useCustomizedImages, treeDeleter); | |
| spawnRunners.add(spawnRunner); | |
| builder.registerStrategy( | |
| new DockerSandboxedStrategy(spawnRunner, executionOptions), "docker"); | |
| } | |
| } else if (options.getDockerVerbose()) { | |
| 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 = | |
| LinuxSandboxedStrategy.create( | |
| cmdEnv, sandboxBase, timeoutKillDelay, treeDeleter, options); | |
| spawnRunners.add(spawnRunner); | |
| builder.registerStrategy( | |
| new LinuxSandboxedStrategy(spawnRunner, executionOptions), "sandboxed", "linux-sandbox"); | |
| } | |
| // This is the preferred sandboxing strategy on macOS. | |
| if (darwinSandboxSupported) { | |
| SpawnRunner spawnRunner = new DarwinSandboxedSpawnRunner(cmdEnv, sandboxBase, treeDeleter); | |
| spawnRunners.add(spawnRunner); | |
| builder.registerStrategy( | |
| new DarwinSandboxedStrategy(spawnRunner, executionOptions), | |
| "sandboxed", | |
| "darwin-sandbox"); | |
| } | |
| if (windowsSandboxSupported) { | |
| SpawnRunner spawnRunner = | |
| new WindowsSandboxedSpawnRunner(cmdEnv, timeoutKillDelay, windowsSandboxPath); | |
| spawnRunners.add(spawnRunner); | |
| builder.registerStrategy( | |
| new WindowsSandboxedStrategy(spawnRunner, executionOptions), | |
| "sandboxed", | |
| "windows-sandbox"); | |
| } | |
| if (processWrapperSupported | |
| || linuxSandboxSupported | |
| || darwinSandboxSupported | |
| || windowsSandboxSupported) { | |
| // This makes the "sandboxed" strategy the default Spawn strategy, unless it is | |
| // overridden by a later BlazeModule. | |
| builder.setDefaultStrategies(ImmutableList.of("sandboxed")); | |
| } | |
| } | |
| @Nullable | |
| 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) { | |
| // Intentionally ignored. | |
| } | |
| } | |
| return null; | |
| } | |
| @Subscribe | |
| public void cleanStarting(@SuppressWarnings("unused") CleanStartingEvent event) { | |
| if (sandboxBase != null) { | |
| SandboxStash.clean(treeDeleter, sandboxBase); | |
| } | |
| } | |
| // Package-private for testing | |
| void cleanSandboxBaseOnFirstBuild(Path sandboxBase, Path trashBase, int idleThreads) | |
| throws IOException { | |
| if (idleThreads > 0 && treeDeleter instanceof AsynchronousTreeDeleter asyncDeleter) { | |
| asyncDeleter.setThreads(idleThreads); | |
| } | |
| try (SilentCloseable c = Profiler.instance().profile("clean sandbox on first build")) { | |
| if (trashBase.exists()) { | |
| // Delete stale trash from a previous server instance. | |
| Path staleTrash = getStaleTrashDir(trashBase); | |
| trashBase.renameTo(staleTrash); | |
| trashBase.createDirectory(); | |
| for (Dirent dirent : staleTrash.readdir(Symlinks.NOFOLLOW)) { | |
| Path childPath = staleTrash.getChild(dirent.getName()); | |
| if (dirent.getType() == Dirent.Type.DIRECTORY) { | |
| treeDeleter.deleteTree(childPath); | |
| } else { | |
| childPath.delete(); | |
| } | |
| } | |
| staleTrash.delete(); | |
| } else { | |
| trashBase.createDirectory(); | |
| } | |
| // We can delete other dirs asynchronously (if the flag is on). | |
| for (Dirent dirent : sandboxBase.readdir(Symlinks.NOFOLLOW)) { | |
| Path childPath = sandboxBase.getChild(dirent.getName()); | |
| if (childPath.getBaseName().equals(AsynchronousTreeDeleter.MOVED_TRASH_DIR)) { | |
| continue; | |
| } | |
| if (childPath.getBaseName().equals(SandboxHelpers.INACCESSIBLE_HELPER_DIR)) { | |
| childPath.deleteTree(); | |
| } else if (dirent.getType() == Dirent.Type.DIRECTORY) { | |
| treeDeleter.deleteTree(childPath); | |
| } else { | |
| childPath.delete(); | |
| } | |
| } | |
| } catch (IOException e) { | |
| // We have observed asynchronous deletion failing when running Bazel under Docker, see | |
| // #21719. Different RUN commands with `bazel build` will write to different layers in the | |
| // docker image. The overlay filesystem is different and the renaming of the directories | |
| // that we need to do for asynchronous deletion will fail. When that happens we fall back to | |
| // synchronous deletion here. | |
| logger.atWarning().withCause(e).log( | |
| "Asynchronous sandbox deletion failed (likely due to overlayfs/container filesystem" | |
| + " layers, see https://github.com/bazelbuild/bazel/issues/21719); falling back to" | |
| + " synchronous deletion of %s", | |
| sandboxBase); | |
| if (treeDeleter != null) { | |
| treeDeleter.shutdown(); | |
| if (idleThreads > 0) { | |
| treeDeleter = new AsynchronousTreeDeleter(trashBase); | |
| } | |
| } | |
| sandboxBase.deleteTree(); | |
| } finally { | |
| if (idleThreads > 0 && treeDeleter instanceof AsynchronousTreeDeleter asyncDeleter) { | |
| asyncDeleter.setThreads(1); | |
| } | |
| } | |
| } | |
| /** | |
| * Removes any top-level entries in sandboxBase that are not in SANDBOX_BASE_PERSISTENT_DIRS or | |
| * expected inaccessible helper files/directories. This cleans up leftover sandbox directories | |
| * (e.g. linux-sandbox) from previous builds that used --sandbox_debug or different execution | |
| * strategies. | |
| */ | |
| // Package-private for testing | |
| static void cleanSandboxBaseTopOnlyContainsPersistentDirs( | |
| Path sandboxBase, @Nullable TreeDeleter treeDeleter) throws IOException { | |
| for (Dirent dirent : sandboxBase.readdir(Symlinks.NOFOLLOW)) { | |
| String name = dirent.getName(); | |
| if (SANDBOX_BASE_PERSISTENT_DIRS.contains(name) | |
| || name.equals(SandboxHelpers.INACCESSIBLE_HELPER_DIR) | |
| || name.equals(SandboxHelpers.INACCESSIBLE_HELPER_FILE)) { | |
| continue; | |
| } | |
| Path childPath = sandboxBase.getChild(name); | |
| if (dirent.getType() == Dirent.Type.DIRECTORY) { | |
| if (treeDeleter != null) { | |
| treeDeleter.deleteTree(childPath); | |
| } else { | |
| childPath.deleteTree(); | |
| } | |
| } else { | |
| childPath.delete(); | |
| } | |
| } | |
| } | |
| /** | |
| * Checks that sandboxBase only contains persistent directories, logging a warning if unexpected | |
| * entries are found. | |
| */ | |
| // Package-private for testing | |
| static void checkSandboxBaseTopOnlyContainsPersistentDirs(Path sandboxBase) { | |
| try { | |
| List<String> directoryEntries = | |
| sandboxBase.getDirectoryEntries().stream() | |
| .map(Path::getBaseName) | |
| .collect(toCollection(ArrayList::new)); | |
| // If sandbox initialization failed in-between creating the inaccessible dir/file and adding | |
| // the Linux sandboxing strategy to spawnRunners, then the sandbox base will be in a bad | |
| // state. We check for that here and clean up. | |
| if (directoryEntries.contains(SandboxHelpers.INACCESSIBLE_HELPER_DIR)) { | |
| Path inaccessibleHelperDir = sandboxBase.getChild(SandboxHelpers.INACCESSIBLE_HELPER_DIR); | |
| inaccessibleHelperDir.chmod(0700); | |
| directoryEntries.remove(SandboxHelpers.INACCESSIBLE_HELPER_DIR); | |
| inaccessibleHelperDir.deleteTree(); | |
| } | |
| if (directoryEntries.contains(SandboxHelpers.INACCESSIBLE_HELPER_FILE)) { | |
| Path inaccessibleHelperFile = sandboxBase.getChild(SandboxHelpers.INACCESSIBLE_HELPER_FILE); | |
| directoryEntries.remove(SandboxHelpers.INACCESSIBLE_HELPER_FILE); | |
| inaccessibleHelperFile.delete(); | |
| } | |
| ImmutableList<String> unexpectedEntries = | |
| directoryEntries.stream() | |
| .filter(entry -> !SANDBOX_BASE_PERSISTENT_DIRS.contains(entry)) | |
| .collect(toImmutableList()); | |
| if (!unexpectedEntries.isEmpty()) { | |
| StringBuilder message = new StringBuilder("Found unexpected entries in sandbox base: "); | |
| Joiner.on(", ").appendTo(message, unexpectedEntries); | |
| logger.atWarning().log("%s", message); | |
| } | |
| } catch (IOException e) { | |
| logger.atWarning().withCause(e).log("Failed to clean up sandbox base %s", sandboxBase); | |
| } | |
| } | |
| @Override | |
| public void afterCommand() { | |
| checkNotNull(env, "env not initialized; was beforeCommand called?"); | |
| SandboxOptions options = env.getOptions().getOptions(SandboxOptions.class); | |
| int asyncTreeDeleteThreads = options != null ? options.getAsyncTreeDeleteIdleThreads() : 0; | |
| // If asynchronous deletions were requested, they may still be ongoing so let them be: trying | |
| // to delete the base tree synchronously could fail as we can race with those other deletions, | |
| // and scheduling an asynchronous deletion could race with future builds. | |
| if (asyncTreeDeleteThreads > 0 && treeDeleter instanceof AsynchronousTreeDeleter treeDeleter) { | |
| treeDeleter.setThreads(asyncTreeDeleteThreads); | |
| } | |
| // `treeDeleter` might not be an AsynchronousTreeDeleter if the user changed the option but | |
| // then interrupted the build before the start of the execution phase. But that's OK, there | |
| // will be nothing new to delete. See #13240. | |
| if (shouldCleanupSandboxBase) { | |
| boolean cleanupSucceeded = false; | |
| try { | |
| checkNotNull(sandboxBase, "shouldCleanupSandboxBase implies sandboxBase has been set"); | |
| for (SpawnRunner spawnRunner : spawnRunners) { | |
| spawnRunner.cleanupSandboxBase(sandboxBase, treeDeleter); | |
| Path runnerDir = sandboxBase.getChild(spawnRunner.getName()); | |
| if (runnerDir.exists()) { | |
| if (treeDeleter != null) { | |
| treeDeleter.deleteTree(runnerDir); | |
| } else { | |
| runnerDir.deleteTree(); | |
| } | |
| } | |
| } | |
| cleanSandboxBaseTopOnlyContainsPersistentDirs(sandboxBase, treeDeleter); | |
| cleanupSucceeded = true; | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| } catch (IOException e) { | |
| env.getReporter() | |
| .handle(Event.warn("Failed to delete contents of sandbox " + sandboxBase + ": " + e)); | |
| } | |
| shouldCleanupSandboxBase = false; | |
| if (cleanupSucceeded) { | |
| checkSandboxBaseTopOnlyContainsPersistentDirs(sandboxBase); | |
| } | |
| // We intentionally keep sandboxBase around, without resetting it to null, in case we have | |
| // asynchronous deletions going on. In that case, we'd still want to retry this during | |
| // shutdown. | |
| } | |
| spawnRunners.clear(); | |
| env.getEventBus().unregister(this); | |
| env = null; | |
| } | |
| private void commonShutdown() { | |
| // Try to clean up as much garbage as possible, if there happens to be any. This will delay | |
| // server termination but it's the nice thing to do. If the user gets impatient, they can always | |
| // kill us again. | |
| if (sandboxBase != null) { | |
| SandboxStash.clean(treeDeleter, sandboxBase); | |
| } | |
| if (treeDeleter != null) { | |
| try { | |
| treeDeleter.shutdown(); | |
| } finally { | |
| treeDeleter = null; // Avoid potential reexecution if we crash. | |
| } | |
| } | |
| } | |
| @Override | |
| public void blazeShutdown() { | |
| commonShutdown(); | |
| } | |
| @Override | |
| public void blazeShutdownOnCrash(DetailedExitCode exitCode) { | |
| commonShutdown(); | |
| } | |
| private Path getStaleTrashDir(Path trashBase) throws IOException { | |
| int i = 0; | |
| while (trashBase.getParentDirectory().getChild("stale-trash-" + i++).exists()) { | |
| ; | |
| } | |
| return trashBase.getParentDirectory().getChild("stale-trash-" + --i); | |
| } | |
| } |