Prevent synchronous cleanupSandboxBase from blocking on interrupted commands

When `shouldCleanupSandboxBase` is true or during abrupt command termination,
`cleanupSandboxBase()` synchronously deletes directories. By having
`SpawnRunner.cleanupSandboxBase()` declare `throws InterruptedException` and throw
on `Thread.currentThread().isInterrupted()` during directory traversal,
`SandboxModule.afterCommand()` catches the interruption and skips blocking
synchronous directory tree deletions and `checkSandboxBaseTopOnlyContainsPersistentDirs()`
validations on Ctrl-C.

Part of https://github.com/bazelbuild/bazel/issues/19533

PiperOrigin-RevId: 971406665
Change-Id: I7328b1d5e8cac3088cdb8816e20515e972d66f3f
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
index e78a2a7..9ca0804 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
@@ -409,8 +409,10 @@
    *     entries
    * @param treeDeleter scheduler for tree deletions
    * @throws IOException if there are problems deleting the entries
+   * @throws InterruptedException if the cleanup is interrupted
    */
-  default void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter) throws IOException {}
+  default void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter)
+      throws IOException, InterruptedException {}
 
   /**
    * Returns a {@link SpawnResult.Builder} prepopulated with the runner name and the spawn digest.
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
index 7c0134a..c69456c 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
@@ -451,12 +451,17 @@
   }
 
   @Override
-  public void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter) throws IOException {
+  public void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter)
+      throws IOException, InterruptedException {
     Path root = sandboxBase.getChild(getName());
     if (root.exists()) {
       for (Path child : root.getDirectoryEntries()) {
+        if (Thread.currentThread().isInterrupted()) {
+          throw new InterruptedException();
+        }
         treeDeleter.deleteTree(child);
       }
+      root.delete();
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index adbed03..43137a9 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -523,7 +523,8 @@
   }
 
   @Override
-  public void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter) throws IOException {
+  public void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter)
+      throws IOException, InterruptedException {
     VirtualCgroup.deleteInstance();
     // Delete the inaccessible files synchronously, bypassing the treeDeleter. They are only a
     // couple of files that can be deleted fast, and ensuring they are gone at the end of every
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 45acb85..bc7a5e4 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
@@ -461,15 +461,15 @@
         checkNotNull(sandboxBase, "shouldCleanupSandboxBase implies sandboxBase has been set");
         for (SpawnRunner spawnRunner : spawnRunners) {
           spawnRunner.cleanupSandboxBase(sandboxBase, treeDeleter);
-          sandboxBase.getChild(spawnRunner.getName()).delete();
         }
+        shouldCleanupSandboxBase = false;
+        checkSandboxBaseTopOnlyContainsPersistentDirs(sandboxBase);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
       } catch (IOException e) {
         env.getReporter()
             .handle(Event.warn("Failed to delete contents of sandbox " + sandboxBase + ": " + e));
       }
-      shouldCleanupSandboxBase = false;
-
-      checkSandboxBaseTopOnlyContainsPersistentDirs(sandboxBase);
       // We intentionally keep sandboxBase around, without resetting it to null, in case we have
       // asynchronous deletions going on. In that case, we'd still want to retry this during
       // shutdown.