Encode CleanCommand failures with FailureDetails

RELNOTES: None.
PiperOrigin-RevId: 312355942
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
index 3007d8e..7421a9f 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
@@ -221,9 +221,7 @@
     skyframeExecutor.resetEvaluator();
   }
 
-  /**
-   * Removes in-memory caches.
-   */
+  /** Removes in-memory and on-disk action caches. */
   public void clearCaches() throws IOException {
     if (actionCache != null) {
       actionCache.clear();
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
index edb7c54..ecb1915 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.runtime.commands;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.analysis.NoBuildEvent;
@@ -25,9 +26,12 @@
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.CleanCommand.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.CommandBuilder;
-import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.InterruptedFailureDetails;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.ProcessUtils;
 import com.google.devtools.build.lib.util.ShellEscaper;
@@ -160,15 +164,15 @@
               .getOptions(BuildRequestOptions.class)
               .getSymlinkPrefix(env.getRuntime().getProductName());
       return actuallyClean(env, env.getOutputBase(), cleanOptions.expunge, async, symlinkPrefix);
-    } catch (IOException e) {
+    } catch (CleanException e) {
       env.getReporter().handle(Event.error(e.getMessage()));
-      return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
-    } catch (CommandException | ExecException e) {
-      env.getReporter().handle(Event.error(e.getMessage()));
-      return BlazeCommandResult.exitCode(ExitCode.RUN_FAILURE);
+      return BlazeCommandResult.failureDetail(e.getFailureDetail());
     } catch (InterruptedException e) {
-      env.getReporter().handle(Event.error("clean interrupted"));
-      return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED);
+      String message = "clean interrupted";
+      env.getReporter().handle(Event.error(message));
+      return BlazeCommandResult.detailedExitCode(
+          InterruptedFailureDetails.detailedExitCode(
+              message, FailureDetails.Interrupted.Code.CLEAN_COMMAND));
     }
   }
 
@@ -203,25 +207,36 @@
 
   private BlazeCommandResult actuallyClean(
       CommandEnvironment env, Path outputBase, boolean expunge, boolean async, String symlinkPrefix)
-      throws IOException, CommandException, ExecException,
-          InterruptedException {
+      throws CleanException, InterruptedException {
     BlazeRuntime runtime = env.getRuntime();
     String workspaceDirectory = env.getWorkspace().getBaseName();
     if (env.getOutputService() != null) {
-      env.getOutputService().clean();
+      try {
+        env.getOutputService().clean();
+      } catch (ExecException e) {
+        throw new CleanException(Code.OUTPUT_SERVICE_CLEAN_FAILURE, e);
+      }
     }
-    env.getBlazeWorkspace().clearCaches();
+    try {
+      env.getBlazeWorkspace().clearCaches();
+    } catch (IOException e) {
+      throw new CleanException(Code.ACTION_CACHE_CLEAN_FAILURE, e);
+    }
     if (expunge && !async) {
       logger.atInfo().log("Expunging...");
       runtime.prepareForAbruptShutdown();
       // Close java.log.
       LogManager.getLogManager().reset();
       // Close the default stdout/stderr.
-      if (FileDescriptor.out.valid()) {
-        new FileOutputStream(FileDescriptor.out).close();
-      }
-      if (FileDescriptor.err.valid()) {
-        new FileOutputStream(FileDescriptor.err).close();
+      try {
+        if (FileDescriptor.out.valid()) {
+          new FileOutputStream(FileDescriptor.out).close();
+        }
+        if (FileDescriptor.err.valid()) {
+          new FileOutputStream(FileDescriptor.err).close();
+        }
+      } catch (IOException e) {
+        throw new CleanException(Code.OUT_ERR_CLOSE_FAILURE, e);
       }
       // Close the redirected stdout/stderr.
       System.out.close();
@@ -231,12 +246,22 @@
       // and links right before we exit. Once the lock file is gone there will
       // be a small possibility of a server race if a client is waiting, but
       // all significant files will be gone by then.
-      outputBase.deleteTreesBelow();
-      outputBase.deleteTree();
+      try {
+        outputBase.deleteTreesBelow();
+        outputBase.deleteTree();
+      } catch (IOException e) {
+        throw new CleanException(Code.OUTPUT_BASE_DELETE_FAILURE, e);
+      }
     } else if (expunge && async) {
       logger.atInfo().log("Expunging asynchronously...");
       runtime.prepareForAbruptShutdown();
-      asyncClean(env, outputBase, "Output base");
+      try {
+        asyncClean(env, outputBase, "Output base");
+      } catch (IOException e) {
+        throw new CleanException(Code.OUTPUT_BASE_TEMP_MOVE_FAILURE, e);
+      } catch (CommandException e) {
+        throw new CleanException(Code.ASYNC_OUTPUT_BASE_DELETE_FAILURE, e);
+      }
     } else {
       logger.atInfo().log("Output cleaning...");
       env.getBlazeWorkspace().resetEvaluator();
@@ -244,9 +269,19 @@
       if (execroot.exists()) {
         logger.atFinest().log("Cleaning %s%s", execroot, async ? " asynchronously..." : "");
         if (async) {
-          asyncClean(env, execroot, "Output tree");
+          try {
+            asyncClean(env, execroot, "Output tree");
+          } catch (IOException e) {
+            throw new CleanException(Code.EXECROOT_TEMP_MOVE_FAILURE, e);
+          } catch (CommandException e) {
+            throw new CleanException(Code.ASYNC_EXECROOT_DELETE_FAILURE, e);
+          }
         } else {
-          execroot.deleteTreesBelow();
+          try {
+            execroot.deleteTreesBelow();
+          } catch (IOException e) {
+            throw new CleanException(Code.EXECROOT_DELETE_FAILURE, e);
+          }
         }
       }
     }
@@ -266,4 +301,20 @@
     System.gc();
     return BlazeCommandResult.success();
   }
+
+  private static class CleanException extends Exception {
+    private final FailureDetails.CleanCommand.Code detailedCode;
+
+    private CleanException(FailureDetails.CleanCommand.Code detailedCode, Exception e) {
+      super(Strings.nullToEmpty(e.getMessage()), e);
+      this.detailedCode = detailedCode;
+    }
+
+    private FailureDetail getFailureDetail() {
+      return FailureDetail.newBuilder()
+          .setMessage(getMessage())
+          .setCleanCommand(FailureDetails.CleanCommand.newBuilder().setCode(detailedCode))
+          .build();
+    }
+  }
 }
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 3cc2cc3..2db09ec 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -118,6 +118,7 @@
     TestCommand test_command = 140;
     ActionQuery action_query = 141;
     TargetPatterns target_patterns = 142;
+    CleanCommand clean_command = 144;
   }
 
   reserved 102; // For internal use
@@ -145,6 +146,7 @@
     AFTER_QUERY = 10 [(metadata) = { exit_code: 8 }];
     FETCH_COMMAND = 17 [(metadata) = { exit_code: 8 }];
     SYNC_COMMAND = 18 [(metadata) = { exit_code: 8 }];
+    CLEAN_COMMAND = 20 [(metadata) = { exit_code: 8 }];
     reserved 1 to 3; // For internal use
     reserved 11 to 16; // For internal use
     reserved 19; // For internal use
@@ -616,3 +618,20 @@
 
   Code code = 1;
 }
+
+message CleanCommand {
+  enum Code {
+    CLEAN_COMMAND_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    OUTPUT_SERVICE_CLEAN_FAILURE = 1 [(metadata) = { exit_code: 6 }];
+    ACTION_CACHE_CLEAN_FAILURE = 2 [(metadata) = { exit_code: 36 }];
+    OUT_ERR_CLOSE_FAILURE = 3 [(metadata) = { exit_code: 36 }];
+    OUTPUT_BASE_DELETE_FAILURE = 4 [(metadata) = { exit_code: 36 }];
+    OUTPUT_BASE_TEMP_MOVE_FAILURE = 5 [(metadata) = { exit_code: 36 }];
+    ASYNC_OUTPUT_BASE_DELETE_FAILURE = 6 [(metadata) = { exit_code: 6 }];
+    EXECROOT_DELETE_FAILURE = 7 [(metadata) = { exit_code: 36 }];
+    EXECROOT_TEMP_MOVE_FAILURE = 8 [(metadata) = { exit_code: 36 }];
+    ASYNC_EXECROOT_DELETE_FAILURE = 9 [(metadata) = { exit_code: 6 }];
+  }
+
+  Code code = 1;
+}