aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java261
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java128
-rw-r--r--src/test/java/com/google/devtools/build/lib/BUILD52
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java176
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java111
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcessTest.java52
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcessTest.java59
8 files changed, 836 insertions, 5 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
index 9d849a8fc1..daaf92a5e7 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
@@ -30,6 +30,8 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/standalone",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/common/options",
+ "//third_party:auto_value",
"//third_party:guava",
+ "//third_party:jsr305",
],
)
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));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java
new file mode 100644
index 0000000000..4b56ed3265
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java
@@ -0,0 +1,128 @@
+// 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.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.List;
+
+/** Interface to interact with a sandboxfs instance. */
+interface SandboxfsProcess {
+
+ /** Represents a single mapping within a sandboxfs file system. */
+ @AutoValue
+ abstract class Mapping {
+ /**
+ * Path within the sandbox. This looks like an absolute path but is treated as relative to the
+ * sandbox's root.
+ */
+ abstract PathFragment path();
+
+ /** Absolute path from the host's file system to map into the sandbox. */
+ abstract PathFragment target();
+
+ /** Whether the mapped path is writable or not. */
+ abstract boolean writable();
+
+ /** Constructs a new mapping builder. */
+ static Builder builder() {
+ return new AutoValue_SandboxfsProcess_Mapping.Builder();
+ }
+
+ /** Builder for a single mapping within a sandboxfs file system. */
+ @AutoValue.Builder
+ abstract static class Builder {
+ /**
+ * Sets the path within the sandbox on which this mapping will appear. This looks like an
+ * absolute path but is treated as relative to the sandbox's root.
+ *
+ * @param path absolute path rooted at the sandbox's mount point
+ * @return the builder instance
+ */
+ abstract Builder setPath(PathFragment path);
+
+ /**
+ * Sets the path to which this mapping refers. This is an absolute path into the host's
+ * file system.
+ *
+ * @param target absolute path into the host's file system
+ * @return the builder instance
+ */
+ abstract Builder setTarget(PathFragment target);
+
+ /**
+ * Sets whether this mapping is writable or not when accessed via the sandbox.
+ *
+ * @param writable whether the mapping is writable or not
+ * @return the builder instance
+ */
+ abstract Builder setWritable(boolean writable);
+
+ abstract Mapping autoBuild();
+
+ /**
+ * Constructs the mapping and validates field invariants.
+ *
+ * @return the constructed mapping.
+ */
+ public Mapping build() {
+ Mapping mapping = autoBuild();
+ checkState(mapping.path().isAbsolute(), "Mapping specifications are supposed to be "
+ + "absolute but %s is not", mapping.path());
+ checkState(mapping.target().isAbsolute(), "Mapping targets are supposed to be "
+ + "absolute but %s is not", mapping.target());
+ return mapping;
+ }
+ }
+ }
+
+ /** Returns the path to the sandboxfs's mount point. */
+ Path getMountPoint();
+
+ /** Returns true if the sandboxfs process is still alive. */
+ boolean isAlive();
+
+ /**
+ * Unmounts and stops the sandboxfs process.
+ *
+ * <p>This function must be idempotent because there can be a race between explicit calls during
+ * regular execution and calls from shutdown hooks.
+ */
+ void destroy();
+
+ /**
+ * Adds new mappings to the sandboxfs instance.
+ *
+ * @param mappings the collection of mappings to add, which must not have yet been previously
+ * mapped
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ void map(List<Mapping> mappings) throws IOException;
+
+ /**
+ * Removes a mapping from the sandboxfs instance.
+ *
+ * @param mapping the mapping to remove, which must have been previously mapped. This looks like
+ * an absolute path but is treated as relative to the sandbox's root.
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ void unmap(PathFragment mapping) throws IOException;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 9e871c9298..a11f48ac1a 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -1226,34 +1226,76 @@ java_test(
],
)
+java_library(
+ name = "sandboxfs-base-tests",
+ testonly = 1,
+ srcs = ["sandbox/BaseSandboxfsProcessTest.java"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib/sandbox",
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//src/test/java/com/google/devtools/build/lib:testutil",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
java_test(
name = "sandbox-tests",
- srcs = glob(["sandbox/*.java"]),
+ srcs = glob(
+ ["sandbox/*.java"],
+ exclude = [
+ "sandbox/BaseSandboxfsProcessTest.java",
+ "sandbox/RealSandboxfsProcessTest.java",
+ ],
+ ),
data = [":embedded_scripts"],
local = 1,
tags = ["no_windows"],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [
- ":actions_testutil",
":analysis_testutil",
":foundations_testutil",
":guava_junit_truth",
+ ":sandboxfs-base-tests",
":testutil",
"//src/main/java/com/google/devtools/build/lib:bazel-rules",
"//src/main/java/com/google/devtools/build/lib:build-base",
- "//src/main/java/com/google/devtools/build/lib:events",
"//src/main/java/com/google/devtools/build/lib:os_util",
"//src/main/java/com/google/devtools/build/lib:util",
"//src/main/java/com/google/devtools/build/lib/actions",
- "//src/main/java/com/google/devtools/build/lib/clock",
"//src/main/java/com/google/devtools/build/lib/sandbox",
- "//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/vfs",
+ "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
"//src/main/java/com/google/devtools/common/options",
],
)
java_test(
+ name = "sandboxfs-integration-tests",
+ srcs = ["sandbox/RealSandboxfsProcessTest.java"],
+ data = [":embedded_scripts"],
+ local = 1,
+ tags = [
+ # On macOS:
+ # sudo sysctl -w vfs.generic.osxfuse.tunables.allow_other=1
+ # Test requires:
+ # --test_env=SANDBOXFS=/path/to/sandboxfs
+ "manual",
+ "no-sandbox",
+ "no_windows",
+ ],
+ test_class = "com.google.devtools.build.lib.AllTests",
+ deps = [
+ ":sandboxfs-base-tests",
+ ":test_runner",
+ "//src/main/java/com/google/devtools/build/lib/sandbox",
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//third_party:junit4",
+ ],
+)
+
+java_test(
name = "standalone-tests",
srcs = glob(["standalone/*.java"]),
data = [":embedded_scripts"],
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java
new file mode 100644
index 0000000000..dd827761be
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java
@@ -0,0 +1,176 @@
+// 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.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.sandbox.SandboxfsProcess.Mapping;
+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 java.io.IOException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Common tests for all implementations of {@link SandboxfsProcess}.
+ *
+ * <p>Subclasses must define the provided hooks to configure the file system the tests run in
+ * (which can be real or virtual), and a mechanism to "mount" a sandboxfs instance.
+ *
+ * <p>Subclasses inherit and run all the tests in this class.
+ */
+abstract class BaseSandboxfsProcessTest {
+
+ /** Test-specific temporary directory and file system. */
+ protected Path tmpDir;
+
+ /** Hook to obtain the path to a test-specific temporary directory and file system. */
+ abstract Path newTmpDir() throws IOException;
+
+ /** Hook to mount a new test-specific sandboxfs instance. */
+ abstract SandboxfsProcess mount(Path mountPoint) throws IOException;
+
+ @Before
+ public void setUp() throws IOException {
+ tmpDir = newTmpDir();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ FileSystemUtils.deleteTreesBelow(tmpDir);
+ tmpDir = null;
+ }
+
+ @Test
+ public void testMount_MissingDirectory() throws IOException {
+ IOException expected = assertThrows(
+ IOException.class, () -> mount(tmpDir.getRelative("missing")));
+ assertThat(expected).hasMessageThat().matches(".*(/missing.*does not exist|failed to start).*");
+ }
+
+ @Test
+ public void testLifeCycle() throws IOException {
+ Path mountPoint = tmpDir.getRelative("mnt");
+ mountPoint.createDirectory();
+ SandboxfsProcess process = mount(mountPoint);
+ try {
+ assertThat(process.isAlive()).isTrue();
+ process.destroy();
+ assertThat(process.isAlive()).isFalse();
+ process.destroy();
+ assertThat(process.isAlive()).isFalse();
+ } finally {
+ process.destroy();
+ }
+ }
+
+ @Test
+ public void testReconfigure() throws IOException {
+ Path mountPoint = tmpDir.getRelative("mnt");
+ mountPoint.createDirectory();
+ SandboxfsProcess process = mount(mountPoint);
+ try {
+ // Start by ensuring the mount point is empty.
+ assertThat(mountPoint.getDirectoryEntries()).isEmpty();
+
+ // Create a file outside of the mount point to ensure it's not touched.
+ mountPoint.getRelative("../unrelated").createDirectory();
+
+ // Create twp mappings: one to be deleted and one to be kept around throughout the test.
+ Path keepMeFile = tmpDir.getRelative("one");
+ keepMeFile.getOutputStream().close();
+ Path oneFile = tmpDir.getRelative("one");
+ FileSystemUtils.writeContent(oneFile, UTF_8, "One test data");
+ process.map(
+ ImmutableList.of(
+ Mapping.builder()
+ .setPath(PathFragment.create("/keep-me"))
+ .setTarget(keepMeFile.asFragment())
+ .setWritable(false)
+ .build(),
+ Mapping.builder()
+ .setPath(PathFragment.create("/foo"))
+ .setTarget(oneFile.asFragment())
+ .setWritable(false)
+ .build()));
+ assertThat(
+ mountPoint.getDirectoryEntries())
+ .containsExactly(mountPoint.getRelative("foo"), mountPoint.getRelative("keep-me"));
+ assertThat(
+ FileSystemUtils.readContent(mountPoint.getRelative("foo"), UTF_8))
+ .isEqualTo("One test data");
+
+ // Replace the previous mapping and create a new one.
+ Path twoFile = tmpDir.getRelative("two");
+ FileSystemUtils.writeContent(twoFile, UTF_8, "Two test data");
+ Path bazFile = tmpDir.getRelative("baz");
+ FileSystemUtils.writeContent(bazFile, UTF_8, "Baz test data");
+ process.unmap(PathFragment.create("/foo"));
+ process.map(
+ ImmutableList.of(
+ Mapping.builder()
+ .setPath(PathFragment.create("/foo"))
+ .setTarget(twoFile.asFragment())
+ .setWritable(false)
+ .build(),
+ Mapping.builder()
+ .setPath(PathFragment.create("/bar"))
+ .setTarget(bazFile.asFragment())
+ .setWritable(true)
+ .build()));
+ assertThat(
+ mountPoint.getDirectoryEntries())
+ .containsExactly(mountPoint.getRelative("foo"), mountPoint.getRelative("bar"),
+ mountPoint.getRelative("keep-me"));
+ assertThat(
+ FileSystemUtils.readContent(mountPoint.getRelative("foo"), UTF_8))
+ .isEqualTo("Two test data");
+ assertThat(
+ FileSystemUtils.readContent(mountPoint.getRelative("bar"), UTF_8))
+ .isEqualTo("Baz test data");
+
+ // Replace all existing mappings, and try with a nested one.
+ Path longLink = tmpDir.getRelative("long/link");
+ longLink.getParentDirectory().createDirectoryAndParents();
+ longLink.createSymbolicLink(oneFile); // The target is irrelevant but must exist.
+ process.unmap(PathFragment.create("/foo"));
+ process.unmap(PathFragment.create("/bar"));
+ process.map(
+ ImmutableList.of(
+ Mapping.builder()
+ .setPath(PathFragment.create("/something/complex"))
+ .setTarget(longLink.asFragment())
+ .setWritable(false)
+ .build()));
+ assertThat(
+ mountPoint.getDirectoryEntries())
+ .containsExactly(mountPoint.getRelative("keep-me"), mountPoint.getRelative("something"));
+ assertThat(
+ FileSystemUtils.readContent(mountPoint.getRelative("something/complex"), UTF_8))
+ .isEqualTo("One test data");
+
+ // Ensure that files that should not have been touched throughout the test are still there.
+ assertThat(mountPoint.getRelative("keep-me").exists()).isTrue();
+ assertThat(mountPoint.getRelative("../unrelated").exists()).isTrue();
+ } finally {
+ process.destroy();
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java
new file mode 100644
index 0000000000..8a9c63b288
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java
@@ -0,0 +1,111 @@
+// 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.checkState;
+
+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 java.io.IOException;
+import java.util.List;
+
+/**
+ * A fake in-process sandboxfs implementation that uses symlinks on the Bazel file system API.
+ *
+ * <p>TODO(jmmv): It's possible that this could replace {@link SymlinkedSandboxedSpawn} altogether,
+ * simplifying all callers that need to perform a sandboxed spawn because they would all go through
+ * the sandboxfs worker interface. Evaluate this idea once we are confident enough that we won't
+ * just remove all sandboxfs support code.
+ */
+final class FakeSandboxfsProcess implements SandboxfsProcess {
+
+ /** File system on which the fake sandboxfs instance operates. */
+ private final FileSystem fileSystem;
+
+ /** Directory on which the sandboxfs is serving. */
+ private final PathFragment mountPoint;
+
+ /**
+ * Whether this "process" is valid or not. Used to better represent the workflow of a real
+ * sandboxfs subprocess.
+ */
+ private boolean alive = true;
+
+ /**
+ * Initializes a new sandboxfs process instance.
+ *
+ * <p>To better represent reality, this ensures that the mount point is present and valid.
+ *
+ * @param fileSystem file system on which the fake sandboxfs instance operates
+ * @param mountPoint directory on which the sandboxfs instance is serving
+ * @throws IOException if the mount point is missing
+ */
+ FakeSandboxfsProcess(FileSystem fileSystem, PathFragment mountPoint) throws IOException {
+ if (!fileSystem.getPath(mountPoint).exists()) {
+ throw new IOException("Mount point " + mountPoint + " does not exist");
+ } else if (!fileSystem.getPath(mountPoint).isDirectory()) {
+ throw new IOException("Mount point " + mountPoint + " is not a directory");
+ }
+
+ this.fileSystem = fileSystem;
+ this.mountPoint = mountPoint;
+ }
+
+ @Override
+ public Path getMountPoint() {
+ return fileSystem.getPath(mountPoint);
+ }
+
+ @Override
+ public synchronized boolean isAlive() {
+ return alive;
+ }
+
+ @Override
+ public synchronized void destroy() {
+ alive = false;
+ }
+
+ @Override
+ public synchronized void map(List<Mapping> mappings) throws IOException {
+ checkState(alive, "Cannot be called after destroy()");
+
+ for (Mapping mapping : mappings) {
+ checkState(mapping.path().isAbsolute(), "Mapping specifications are expected to be absolute"
+ + " but %s is not", mapping.path());
+ Path link = fileSystem.getPath(mountPoint).getRelative(mapping.path().toRelative());
+ link.getParentDirectory().createDirectoryAndParents();
+
+ if (!fileSystem.getPath(mapping.target()).exists()) {
+ // Not a requirement for the creation of a symbolic link but this reflects the behavior of
+ // the real sandboxfs.
+ throw new IOException("Target " + mapping.target() + " does not exist");
+ }
+
+ link.createSymbolicLink(fileSystem.getPath(mapping.target()));
+ }
+ }
+
+ @Override
+ public synchronized void unmap(PathFragment mapping) throws IOException {
+ checkState(alive, "Cannot be called after destroy()");
+
+ checkState(mapping.isAbsolute(), "Mapping specifications are expected to be absolute"
+ + " but %s is not", mapping);
+ FileSystemUtils.deleteTree(fileSystem.getPath(mountPoint).getRelative(mapping.toRelative()));
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcessTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcessTest.java
new file mode 100644
index 0000000000..f9d8c63eb9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcessTest.java
@@ -0,0 +1,52 @@
+// 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.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link FakeSandboxfsProcess}. */
+@RunWith(JUnit4.class)
+public class FakeSandboxfsProcessTest extends BaseSandboxfsProcessTest {
+
+ @Override
+ Path newTmpDir() throws IOException {
+ FileSystem fileSystem = new InMemoryFileSystem();
+ Path tmpDir = fileSystem.getPath("/tmp");
+ tmpDir.createDirectory();
+ return tmpDir;
+ }
+
+ @Override
+ SandboxfsProcess mount(Path mountPoint) throws IOException {
+ return new FakeSandboxfsProcess(mountPoint.getFileSystem(), mountPoint.asFragment());
+ }
+
+ @Test
+ public void testMount_NotADirectory() throws IOException {
+ tmpDir.getRelative("file").getOutputStream().close();
+ IOException expected = assertThrows(
+ IOException.class, () -> mount(tmpDir.getRelative("file")));
+ assertThat(expected).hasMessageThat().matches(".*/file.*not a directory");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcessTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcessTest.java
new file mode 100644
index 0000000000..ffce1d2539
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcessTest.java
@@ -0,0 +1,59 @@
+// 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 junit.framework.TestCase.fail;
+
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link RealSandboxfsProcess}. */
+@RunWith(JUnit4.class)
+public class RealSandboxfsProcessTest extends BaseSandboxfsProcessTest {
+
+ @Override
+ Path newTmpDir() {
+ String rawTmpDir = System.getenv("TEST_TMPDIR");
+ if (rawTmpDir == null) {
+ fail("Test requires TEST_TMPDIR to be defined in the environment");
+ }
+
+ FileSystem fileSystem = new JavaIoFileSystem();
+ Path tmpDir = fileSystem.getPath(rawTmpDir);
+ if (!tmpDir.isDirectory()) {
+ fail("TEST_TMPDIR must point to a directory");
+ }
+ return tmpDir;
+ }
+
+ @Override
+ SandboxfsProcess mount(Path mountPoint) throws IOException {
+ String rawSandboxfs = System.getenv("SANDBOXFS");
+ if (rawSandboxfs == null) {
+ fail("Test requires SANDBOXFS to be defined in the environment");
+ }
+
+ FileSystem fileSystem = new JavaIoFileSystem();
+ Path sandboxfs = fileSystem.getPath(rawSandboxfs);
+ if (!sandboxfs.isExecutable()) {
+ fail("SANDBOXFS must point to an executable binary");
+ }
+ return RealSandboxfsProcess.mount(sandboxfs, mountPoint, fileSystem.getPath("/dev/stderr"));
+ }
+}