| // Copyright 2018 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 com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.devtools.build.lib.shell.Subprocess; | |
| import com.google.devtools.build.lib.shell.SubprocessBuilder; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.versioning.GnuVersionParser; | |
| import com.google.devtools.build.lib.versioning.ParseException; | |
| import com.google.devtools.build.lib.versioning.SemVer; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import java.io.IOException; | |
| import java.util.logging.Logger; | |
| import javax.annotation.Nullable; | |
| /** | |
| * A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point. | |
| * | |
| * <p>This class implements common code to generalize the interactions with sandboxfs, but delegates | |
| * to its subclassess once the version of sandboxfs in use has been determined. The subclasses | |
| * implement logic specific to each version to provide compatibility with the different versions of | |
| * sandboxfs that the user might have installed. | |
| */ | |
| abstract class RealSandboxfsProcess implements SandboxfsProcess { | |
| private static final Logger log = Logger.getLogger(RealSandboxfsProcess.class.getName()); | |
| /** | |
| * Contains the {@code --allow} flag to pass to sandboxfs. | |
| * | |
| * <p>On macOS, we need to allow users other than self to access the sandboxfs instance. This is | |
| * necessary because macOS's amfid, which runs as root, has to have access to the binaries within | |
| * the sandbox in order to validate signatures. See: | |
| * http://jmmv.dev/2017/10/fighting-execs-sandboxfs-macos.html | |
| */ | |
| @VisibleForTesting | |
| static final String ALLOW_FLAG = OS.getCurrent() == OS.DARWIN ? "--allow=other" : "--allow=self"; | |
| /** Directory on which the sandboxfs is serving. */ | |
| private final Path mountPoint; | |
| /** | |
| * Process handle to the sandboxfs instance. Null only after {@link #destroy()} has been invoked. | |
| */ | |
| private @Nullable Subprocess process; | |
| /** | |
| * Shutdown hook to stop the sandboxfs instance on abrupt termination. Null only after | |
| * {@link #destroy()} has been invoked. | |
| */ | |
| private @Nullable Thread shutdownHook; | |
| /** | |
| * Initializes a new sandboxfs process instance. | |
| * | |
| * @param process process handle for the already-running sandboxfs instance | |
| */ | |
| RealSandboxfsProcess(Path mountPoint, Subprocess process) { | |
| this.mountPoint = mountPoint; | |
| this.process = process; | |
| this.shutdownHook = | |
| new Thread( | |
| () -> { | |
| try { | |
| this.destroy(); | |
| } catch (Exception e) { | |
| log.warning("Failed to destroy running sandboxfs instance; mount point may have " | |
| + "been left behind: " + e); | |
| } | |
| }); | |
| Runtime.getRuntime().addShutdownHook(shutdownHook); | |
| } | |
| /** | |
| * Mounts a new sandboxfs instance. | |
| * | |
| * <p>The root of the file system instance is left unmapped which means that it remains as | |
| * read-only throughout the lifetime of this instance. Writable subdirectories can later be mapped | |
| * via {@link #createSandbox}. | |
| * | |
| * @param binary path to the sandboxfs binary. This is a {@link PathFragment} and not a {@link | |
| * Path} because we want to support "bare" (non-absolute) names for the location of the | |
| * sandboxfs binary; such names are automatically looked for in the {@code PATH}. | |
| * @param mountPoint directory on which to mount the sandboxfs instance | |
| * @param logFile path to the file that will receive all sandboxfs logging output | |
| * @return a new handle that represents the running process | |
| * @throws IOException if there is a problem starting the process | |
| */ | |
| static SandboxfsProcess mount(PathFragment binary, Path mountPoint, Path logFile) | |
| throws IOException { | |
| log.info("Mounting sandboxfs (" + binary + ") onto " + mountPoint); | |
| GnuVersionParser<SemVer> parser = new GnuVersionParser<>("sandboxfs", SemVer::parse); | |
| SemVer version; | |
| try { | |
| version = parser.fromProgram(binary); | |
| } catch (IOException | ParseException e) { | |
| throw new IOException("Failed to get sandboxfs version from " + binary, e); | |
| } | |
| ImmutableList.Builder<String> argvBuilder = ImmutableList.builder(); | |
| argvBuilder.add(binary.getPathString()); | |
| argvBuilder.add(ALLOW_FLAG); | |
| // TODO(jmmv): Pass flags to enable sandboxfs' debugging support (--listen_address and --debug) | |
| // when requested by the user via --sandbox_debug. Tricky because we have to figure out how to | |
| // deal with port numbers (which sandboxfs can autoassign, but doesn't currently promise a way | |
| // to tell us back what it picked). | |
| argvBuilder.add(mountPoint.getPathString()); | |
| SubprocessBuilder processBuilder = new SubprocessBuilder(); | |
| processBuilder.setArgv(argvBuilder.build()); | |
| processBuilder.setStderr(logFile.getPathFile()); | |
| processBuilder.setEnv(ImmutableMap.of( | |
| // sandboxfs may need to locate fusermount depending on the FUSE implementation so pass the | |
| // PATH to the subprocess (which we assume is sufficient). | |
| "PATH", System.getenv("PATH"))); | |
| Subprocess process = processBuilder.start(); | |
| RealSandboxfsProcess sandboxfs; | |
| if (version.compareTo(SemVer.from(0, 2)) >= 0) { | |
| sandboxfs = new RealSandboxfs02Process(mountPoint, process); | |
| } else { | |
| sandboxfs = new RealSandboxfs01Process(mountPoint, process); | |
| } | |
| try { | |
| // Create an empty sandbox to ensure sandboxfs is successfully serving. | |
| sandboxfs.createSandbox("empty", (mapper) -> {}); | |
| } catch (IOException e) { | |
| process.destroyAndWait(); | |
| throw new IOException("sandboxfs failed to start", e); | |
| } | |
| return sandboxfs; | |
| } | |
| @Override | |
| public Path getMountPoint() { | |
| return mountPoint; | |
| } | |
| @Override | |
| public boolean isAlive() { | |
| return process != null && !process.finished(); | |
| } | |
| @Override | |
| public synchronized void destroy() { | |
| if (shutdownHook != null) { | |
| Runtime.getRuntime().removeShutdownHook(shutdownHook); | |
| shutdownHook = null; | |
| } | |
| if (process != null) { | |
| try { | |
| process.getOutputStream().close(); | |
| } catch (IOException e) { | |
| log.warning("Failed to close sandboxfs's stdin pipe: " + e); | |
| } | |
| try { | |
| process.getInputStream().close(); | |
| } catch (IOException e) { | |
| log.warning("Failed to close sandboxfs's stdout pipe: " + e); | |
| } | |
| process.destroyAndWait(); | |
| process = null; | |
| } | |
| } | |
| } |