sandbox: Fix an issue where an IOException happening during SandboxExecRoot#copyOutputs could hide an earlier ExecException from SandboxRunner#run in the Darwin and Linux sandbox strategies.

--
MOS_MIGRATED_REVID=134273806
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
index 6b5c3ed..d02ffc2 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
@@ -21,28 +21,94 @@
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.SandboxedSpawnActionContext;
 import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
 
 /** Abstract common ancestor for sandbox strategies implementing the common parts. */
 abstract class SandboxStrategy implements SandboxedSpawnActionContext {
 
+  private final BuildRequest buildRequest;
   private final BlazeDirectories blazeDirs;
+  private final Path execRoot;
+  private final ExecutorService backgroundWorkers;
   private final boolean verboseFailures;
   private final SandboxOptions sandboxOptions;
   private final SpawnHelpers spawnHelpers;
 
   public SandboxStrategy(
-      BlazeDirectories blazeDirs, boolean verboseFailures, SandboxOptions sandboxOptions) {
+      BuildRequest buildRequest,
+      BlazeDirectories blazeDirs,
+      ExecutorService backgroundWorkers,
+      boolean verboseFailures,
+      SandboxOptions sandboxOptions) {
+    this.buildRequest = buildRequest;
     this.blazeDirs = blazeDirs;
+    this.execRoot = blazeDirs.getExecRoot();
+    this.backgroundWorkers = Preconditions.checkNotNull(backgroundWorkers);
     this.verboseFailures = verboseFailures;
     this.sandboxOptions = sandboxOptions;
     this.spawnHelpers = new SpawnHelpers(blazeDirs.getExecRoot());
   }
 
+  protected void runSpawn(
+      Spawn spawn,
+      ActionExecutionContext actionExecutionContext,
+      Map<String, String> spawnEnvironment,
+      SandboxExecRoot sandboxExecRoot,
+      Set<PathFragment> outputs,
+      SandboxRunner runner,
+      AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles)
+      throws ExecException, InterruptedException {
+    try {
+      runner.run(
+          spawn.getArguments(),
+          spawnEnvironment,
+          actionExecutionContext.getFileOutErr(),
+          Spawns.getTimeoutSeconds(spawn),
+          SandboxHelpers.shouldAllowNetwork(buildRequest, spawn));
+    } finally {
+      if (writeOutputFiles != null
+          && !writeOutputFiles.compareAndSet(null, SandboxStrategy.class)) {
+        Thread.currentThread().interrupt();
+      } else {
+        try {
+          // We copy the outputs even when the command failed, otherwise StandaloneTestStrategy
+          // won't be able to get the test logs of a failed test. (We should probably do this in
+          // some better way.)
+          sandboxExecRoot.copyOutputs(execRoot, outputs);
+        } catch (IOException e) {
+          // Catch the IOException and turn it into an error message, otherwise this might hide an
+          // exception thrown during runner.run earlier.
+          actionExecutionContext
+              .getExecutor()
+              .getEventHandler()
+              .handle(
+                  Event.error(
+                      "I/O exception while extracting output artifacts from sandboxed execution: "
+                          + e));
+        }
+      }
+      if (!sandboxOptions.sandboxDebug) {
+        SandboxHelpers.lazyCleanup(backgroundWorkers, runner);
+      }
+    }
+
+    if (Thread.interrupted()) {
+      throw new InterruptedException();
+    }
+  }
+
   /** Gets the list of directories that the spawn will assume to be writable. */
   protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) {
     Builder<Path> writableDirs = ImmutableSet.builder();