[sandbox] Add experimental flags for windows sandbox

Closes #8785.

PiperOrigin-RevId: 256653464
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
index 199568a..3bf345a 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
@@ -100,7 +100,6 @@
    *
    * @param binary path to the sandboxfs binary that will later be used in the {@link #mount} call.
    * @return true if the binary looks good, false otherwise
-   * @throws IOException if there is a problem trying to start the subprocess
    */
   static boolean isAvailable(PathFragment binary) {
     Subprocess process;
@@ -112,7 +111,7 @@
               .redirectErrorStream(true)
               .start();
     } catch (IOException e) {
-      log.warning("sandboxfs binary at " + binary + " seems to be missing; got error " + e);
+      log.warning("sandboxfs binary at " + binary + " seems to be missing; got error: " + e);
       return false;
     }
 
@@ -121,14 +120,14 @@
       ByteStreams.copy(process.getInputStream(), outErrBytes);
     } catch (IOException e) {
       try {
-        outErrBytes.write(("Failed to read stdout: " + e).getBytes());
+        outErrBytes.write(("Failed to read stdout: " + e).getBytes("UTF-8"));
       } catch (IOException e2) {
         // Should not really have happened. There is nothing we can do.
       }
     }
     String outErr = outErrBytes.toString().replaceFirst("\n$", "");
 
-    int exitCode = waitForProcess(process);
+    int exitCode = SandboxHelpers.waitForProcess(process);
     if (exitCode == 0) {
       // TODO(jmmv): Validate the version number and ensure we support it. Would be nice to reuse
       // the DottedVersion logic from the Apple rules.
@@ -226,31 +225,6 @@
   }
 
   /**
-   * Waits for a process to terminate.
-   *
-   * @param process the process to wait for
-   * @return the exit code of the terminated process
-   */
-  private static int waitForProcess(Subprocess process) {
-    boolean interrupted = false;
-    try {
-      while (true) {
-        try {
-          process.waitFor();
-          break;
-        } catch (InterruptedException ie) {
-          interrupted = true;
-        }
-      }
-    } finally {
-      if (interrupted) {
-        Thread.currentThread().interrupt();
-      }
-    }
-    return process.exitValue();
-  }
-
-  /**
    * Destroys a process and waits for it to exit.
    *
    * @param process the process to destroy
@@ -259,7 +233,7 @@
   // of Uninterruptibles.callUninterruptibly that takes a lambda instead of a callable.
   private static void destroyProcess(Subprocess process) {
     process.destroy();
-    waitForProcess(process);
+    SandboxHelpers.waitForProcess(process);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
index 48caa71..672be69 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.actions.cache.VirtualActionInput.EmptyActionInput;
 import com.google.devtools.build.lib.analysis.test.TestConfiguration;
 import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
+import com.google.devtools.build.lib.shell.Subprocess;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.common.options.OptionsParsingResult;
@@ -160,4 +161,29 @@
         .testArguments
         .contains("--wrapper_script_flag=--debug");
   }
+
+  /**
+   * Waits for a process to terminate.
+   *
+   * @param process the process to wait for
+   * @return the exit code of the terminated process
+   */
+  static int waitForProcess(Subprocess process) {
+    boolean interrupted = false;
+    try {
+      while (true) {
+        try {
+          process.waitFor();
+          break;
+        } catch (InterruptedException ie) {
+          interrupted = true;
+        }
+      }
+    } finally {
+      if (interrupted) {
+        Thread.currentThread().interrupt();
+      }
+    }
+    return process.exitValue();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index e8ce079..578b34e 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -146,12 +146,11 @@
   /**
    * Returns true if sandboxfs should be used for this build.
    *
-   * <p>If the user set the use of sandboxfs as optional, this only returns true if the configured
-   * sandboxfs binary is present and valid. If the user requested the use of sandboxfs as mandatory,
-   * this throws an error if the binary is not valid.
+   * <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 sandboxfs use was requested or not
-   * @param binary path of the sandboxfs binary to use
+   * @param binary path of the sandboxfs binary to use, can be absolute or relative path
    * @return true if sandboxfs can and should be used; false otherwise
    * @throws IOException if there are problems trying to determine the status of sandboxfs
    */
@@ -175,6 +174,38 @@
     throw new IllegalStateException("Not reachable");
   }
 
+  /**
+   * 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 boolean shouldUseWindowsSandbox(TriState requested, PathFragment binary)
+      throws IOException {
+    switch (requested) {
+      case AUTO:
+        return WindowsSandboxUtil.isAvailable(binary);
+
+      case NO:
+        return false;
+
+      case YES:
+        if (!WindowsSandboxUtil.isAvailable(binary)) {
+          throw new IOException(
+              "windows-sandbox explicitly requested but \""
+                  + binary
+                  + "\" could not be found or is not valid");
+        }
+        return true;
+    }
+    throw new IllegalStateException("Not reachable");
+  }
+
   private void setup(CommandEnvironment cmdEnv, ExecutorBuilder builder)
       throws IOException {
     SandboxOptions options = checkNotNull(env.getOptions().getOptions(SandboxOptions.class));
@@ -240,6 +271,12 @@
       }
     }
 
+    PathFragment windowsSandboxPath = PathFragment.create(options.windowsSandboxPath);
+    boolean useWindowsSandbox;
+    try (SilentCloseable c = Profiler.instance().profile("shouldUseWindowsSandbox")) {
+      useWindowsSandbox = shouldUseWindowsSandbox(options.useWindowsSandbox, windowsSandboxPath);
+    }
+
     Duration timeoutKillDelay =
         cmdEnv.getOptions().getOptions(LocalExecutionOptions.class).getLocalSigkillGraceSeconds();
 
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
index ed745d2..5e5c85a 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
@@ -212,6 +212,29 @@
               + "this on their own and should be removed once all such rules are fixed.")
   public boolean sandboxfsMapSymlinkTargets;
 
+  @Option(
+      name = "experimental_use_windows_sandbox",
+      converter = TriStateConverter.class,
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "Use Windows sandbox to run actions. "
+              + "If \"yes\", the binary provided by --experimental_windows_sandbox_path must be "
+              + "valid and correspond to a supported version of sandboxfs. If \"auto\", the binary "
+              + "may be missing or not compatible.")
+  public TriState useWindowsSandbox;
+
+  @Option(
+      name = "experimental_windows_sandbox_path",
+      defaultValue = "BazelSandbox.exe",
+      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "Path to the Windows sandbox binary to use when --experimental_use_windows_sandbox is"
+              + " true. If a bare name, use the first binary of that name found in the PATH.")
+  public String windowsSandboxPath;
+
   public ImmutableSet<Path> getInaccessiblePaths(FileSystem fs) {
     List<Path> inaccessiblePaths = new ArrayList<>();
     for (String path : sandboxBlockPath) {
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/WindowsSandboxUtil.java b/src/main/java/com/google/devtools/build/lib/sandbox/WindowsSandboxUtil.java
new file mode 100644
index 0000000..4b8af84
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/WindowsSandboxUtil.java
@@ -0,0 +1,199 @@
+// Copyright 2019 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.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.shell.Subprocess;
+import com.google.devtools.build.lib.shell.SubprocessBuilder;
+import com.google.devtools.build.lib.shell.SubprocessBuilder.StreamAction;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/** Utility functions for the {@code windows-sandbox}. */
+public final class WindowsSandboxUtil {
+  private static final Logger log = Logger.getLogger(WindowsSandboxUtil.class.getName());
+
+  /**
+   * Checks if the given Windows sandbox binary is available and is valid.
+   *
+   * @param binary path to the Windows sandbox binary
+   * @return true if the binary looks good, false otherwise
+   */
+  public static boolean isAvailable(PathFragment binary) {
+    Subprocess process;
+    try {
+      process =
+          new SubprocessBuilder()
+              .setArgv(binary.getPathString(), "-h")
+              .setStdout(StreamAction.STREAM)
+              .redirectErrorStream(true)
+              .setWorkingDirectory(new File("."))
+              .start();
+    } catch (IOException e) {
+      log.warning("Windows sandbox binary at " + binary + " seems to be missing; got error: " + e);
+      return false;
+    }
+
+    ByteArrayOutputStream outErrBytes = new ByteArrayOutputStream();
+    try {
+      ByteStreams.copy(process.getInputStream(), outErrBytes);
+    } catch (IOException e) {
+      try {
+        outErrBytes.write(("Failed to read stdout: " + e).getBytes(StandardCharsets.UTF_8));
+      } catch (IOException e2) {
+        // Should not really have happened. There is nothing we can do.
+      }
+    }
+    String outErr = outErrBytes.toString().replaceFirst("\n$", "");
+
+    int exitCode = SandboxHelpers.waitForProcess(process);
+    if (exitCode == 0) {
+      // TODO(rongjiecomputer): Validate the version number and ensure we support it. Would be nice
+      // to reuse
+      // the DottedVersion logic from the Apple rules.
+      return true;
+    } else {
+      log.warning(
+          "Windows sandbox binary at "
+              + binary
+              + " returned non-zero exit code "
+              + exitCode
+              + " and output "
+              + outErr);
+      return false;
+    }
+  }
+
+  /** Returns a new command line builder for the {@code windows-sandbox} tool. */
+  public static CommandLineBuilder commandLineBuilder(
+      PathFragment windowsSandboxPath, List<String> commandArguments) {
+    return new CommandLineBuilder(windowsSandboxPath, commandArguments);
+  }
+
+  /**
+   * A builder class for constructing the full command line to run a command using the {@code
+   * windows-sandbox} tool.
+   */
+  public static class CommandLineBuilder {
+    private final PathFragment windowsSandboxPath;
+    private Path workingDirectory;
+    private Duration timeout;
+    private Duration killDelay;
+    private Path stdoutPath;
+    private Path stderrPath;
+    private Set<Path> writableFilesAndDirectories = ImmutableSet.of();
+    private boolean useDebugMode = false;
+    private List<String> commandArguments = ImmutableList.of();
+
+    private CommandLineBuilder(PathFragment windowsSandboxPath, List<String> commandArguments) {
+      this.windowsSandboxPath = windowsSandboxPath;
+      this.commandArguments = commandArguments;
+    }
+
+    /** Sets the working directory to use, if any. */
+    public CommandLineBuilder setWorkingDirectory(Path workingDirectory) {
+      this.workingDirectory = workingDirectory;
+      return this;
+    }
+
+    /** Sets the timeout for the command run using the {@code windows-sandbox} tool. */
+    public CommandLineBuilder setTimeout(Duration timeout) {
+      this.timeout = timeout;
+      return this;
+    }
+
+    /**
+     * Sets the kill delay for commands run using the {@code windows-sandbox} tool that exceed their
+     * timeout.
+     */
+    public CommandLineBuilder setKillDelay(Duration killDelay) {
+      this.killDelay = killDelay;
+      return this;
+    }
+
+    /** Sets the path to use for redirecting stdout, if any. */
+    public CommandLineBuilder setStdoutPath(Path stdoutPath) {
+      this.stdoutPath = stdoutPath;
+      return this;
+    }
+
+    /** Sets the path to use for redirecting stderr, if any. */
+    public CommandLineBuilder setStderrPath(Path stderrPath) {
+      this.stderrPath = stderrPath;
+      return this;
+    }
+
+    /** Sets the files or directories to make writable for the sandboxed process, if any. */
+    public CommandLineBuilder setWritableFilesAndDirectories(
+        Set<Path> writableFilesAndDirectories) {
+      this.writableFilesAndDirectories = writableFilesAndDirectories;
+      return this;
+    }
+
+    /** Sets whether to enable debug mode (e.g. to print debugging messages). */
+    public CommandLineBuilder setUseDebugMode(boolean useDebugMode) {
+      this.useDebugMode = useDebugMode;
+      return this;
+    }
+
+    /**
+     * Builds the command line to invoke a specific command using the {@code windows-sandbox} tool.
+     */
+    public ImmutableList<String> build() {
+      Preconditions.checkNotNull(this.windowsSandboxPath, "windowsSandboxPath is required");
+      Preconditions.checkState(!this.commandArguments.isEmpty(), "commandArguments are required");
+
+      ImmutableList.Builder<String> commandLineBuilder = ImmutableList.builder();
+
+      commandLineBuilder.add(windowsSandboxPath.getPathString());
+      if (workingDirectory != null) {
+        commandLineBuilder.add("-W", workingDirectory.getPathString());
+      }
+      if (timeout != null) {
+        commandLineBuilder.add("-T", Long.toString(timeout.getSeconds()));
+      }
+      if (killDelay != null) {
+        commandLineBuilder.add("-t", Long.toString(killDelay.getSeconds()));
+      }
+      if (stdoutPath != null) {
+        commandLineBuilder.add("-l", stdoutPath.getPathString());
+      }
+      if (stderrPath != null) {
+        commandLineBuilder.add("-L", stderrPath.getPathString());
+      }
+      for (Path writablePath : writableFilesAndDirectories) {
+        commandLineBuilder.add("-w", writablePath.getPathString());
+      }
+      if (useDebugMode) {
+        commandLineBuilder.add("-D");
+      }
+      commandLineBuilder.add("--");
+      commandLineBuilder.addAll(commandArguments);
+
+      return commandLineBuilder.build();
+    }
+  }
+}