aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java261
1 files changed, 261 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java b/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
new file mode 100644
index 0000000000..70a355f378
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
@@ -0,0 +1,261 @@
+// 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 static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+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.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.List;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/** A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point. */
+final class RealSandboxfsProcess implements SandboxfsProcess {
+ private static final Logger log = Logger.getLogger(RealSandboxfsProcess.class.getName());
+
+ /** 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;
+
+ /**
+ * Writer with which to send data to the sandboxfs instance. Null only after {@link #destroy()}
+ * has been invoked.
+ */
+ private @Nullable BufferedWriter processStdIn;
+
+ /**
+ * Reader with which to receive data from the sandboxfs instance. Null only after
+ * {@link #destroy()} has been invoked.
+ */
+ private @Nullable BufferedReader processStdOut;
+
+ /**
+ * 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
+ */
+ private RealSandboxfsProcess(Path mountPoint, Subprocess process) {
+ this.mountPoint = mountPoint;
+
+ this.process = process;
+ this.processStdIn = new BufferedWriter(
+ new OutputStreamWriter(process.getOutputStream(), UTF_8));
+ this.processStdOut = new BufferedReader(
+ new InputStreamReader(process.getInputStream(), UTF_8));
+
+ 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 #map(List)}.
+ *
+ * @param binary path to the sandboxfs binary
+ * @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(Path binary, Path mountPoint, Path logFile) throws IOException {
+ log.info("Mounting sandboxfs (" + binary + ") onto " + mountPoint);
+
+ // TODO(jmmv): Before starting a sandboxfs serving instance, we must query the current version
+ // of sandboxfs and check if we support its communication protocol.
+
+ ImmutableList.Builder<String> argvBuilder = ImmutableList.builder();
+
+ argvBuilder.add(binary.getPathString());
+
+ // 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://julio.meroh.net/2017/10/fighting-execs-sandboxfs-macos.html
+ argvBuilder.add(OS.getCurrent() == OS.DARWIN ? "--allow=other" : "--allow=self");
+
+ // 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 = new RealSandboxfsProcess(mountPoint, process);
+ // TODO(jmmv): We should have a better mechanism to wait for sandboxfs to start successfully but
+ // sandboxfs currently provides no interface to do so. Just try to push an empty configuration
+ // and see if it works.
+ try {
+ sandboxfs.reconfigure("[]\n\n");
+ } catch (IOException e) {
+ destroyProcess(process);
+ 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();
+ }
+
+ /**
+ * Destroys a process and waits for it to exit.
+ *
+ * @param process the process to destroy.
+ */
+ // TODO(jmmv): This is adapted from Worker.java. Should probably replace both with a new variant
+ // of Uninterruptibles.callUninterruptibly that takes a lambda instead of a callable.
+ private static void destroyProcess(Subprocess process) {
+ process.destroy();
+
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ process.waitFor();
+ return;
+ } catch (InterruptedException ie) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void destroy() {
+ if (shutdownHook != null) {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ shutdownHook = null;
+ }
+
+ if (processStdIn != null) {
+ try {
+ processStdIn.close();
+ } catch (IOException e) {
+ log.warning("Failed to close sandboxfs's stdin pipe: " + e);
+ }
+ processStdIn = null;
+ }
+
+ if (processStdOut != null) {
+ try {
+ processStdOut.close();
+ } catch (IOException e) {
+ log.warning("Failed to close sandboxfs's stdout pipe: " + e);
+ }
+ processStdOut = null;
+ }
+
+ if (process != null) {
+ destroyProcess(process);
+ process = null;
+ }
+ }
+
+ /**
+ * Pushes a new configuration to sandboxfs and waits for acceptance.
+ *
+ * @param config the configuration chunk to push to sandboxfs
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ private synchronized void reconfigure(String config) throws IOException {
+ checkNotNull(processStdIn, "sandboxfs already has been destroyed");
+ processStdIn.write(config);
+ processStdIn.flush();
+
+ checkNotNull(processStdOut, "sandboxfs has already been destroyed");
+ String done = processStdOut.readLine();
+ if (done == null) {
+ throw new IOException("premature end of output from sandboxfs");
+ }
+ if (!done.equals("Done")) {
+ throw new IOException("received unknown string from sandboxfs: " + done + "; expected Done");
+ }
+ }
+
+ @Override
+ public void map(List<Mapping> mappings) throws IOException {
+ Function<Mapping, String> formatMapping =
+ (mapping) -> String.format(
+ "{\"Map\": {\"Mapping\": \"%s\", \"Target\": \"%s\", \"Writable\": %s}}",
+ mapping.path(), mapping.target(), mapping.writable() ? "true" : "false");
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("[\n");
+ sb.append(mappings.stream().map(formatMapping).collect(Collectors.joining(",\n")));
+ sb.append("]\n\n");
+ reconfigure(sb.toString());
+ }
+
+ @Override
+ public void unmap(PathFragment mapping) throws IOException {
+ reconfigure(String.format("[{\"Unmap\": \"%s\"}]\n\n", mapping));
+ }
+}