// Copyright 2017 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.actions;

import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.shell.TerminationStatus;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.vfs.Path;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.ByteString;
import java.io.InputStream;
import java.time.Instant;
import java.util.Locale;
import javax.annotation.Nullable;

/** The result of a {@link Spawn}'s execution. */
@SuppressWarnings("GoodTime") // Use ints instead of Durations to improve build time (cl/505728570)
public interface SpawnResult {

  int POSIX_TIMEOUT_EXIT_CODE = /*SIGNAL_BASE=*/ 128 + /*SIGALRM=*/ 14;

  /** The status of the attempted Spawn execution. */
  enum Status {
    /** Subprocess executed successfully, and returned a zero exit code. */
    SUCCESS,

    /** Subprocess executed successfully, but returned a non-zero exit code. */
    NON_ZERO_EXIT(true),

    /** Subprocess execution timed out. */
    TIMEOUT(true),

    /**
     * The subprocess ran out of memory. On Linux systems, the kernel may kill processes in
     * low-memory situations, and this status is intended to report such a case back to Bazel.
     */
    OUT_OF_MEMORY(true),

    /**
     * Subprocess did not execute, it's not the user's fault, and the error is not catastrophic. If
     * keep_going is enabled then Bazel will try to continue the build, possibly will attempt to
     * rerun the same spawn, and possibly will attempt to run other actions.
     */
    EXECUTION_FAILED,

    /**
     * Subprocess did not execute, it's not the user's fault, and the error is catastrophic. Bazel
     * will not rerun this spawn. Bazel will attempt to not run other actions (regardless of whether
     * keep_going is enabled).
     */
    EXECUTION_FAILED_CATASTROPHICALLY,

    /**
     * Subprocess did not execute, it may be the user's fault, and the error is not catastrophic.
     * The user may be able to fix it. For example, a remote system may have denied the execution
     * due to too many inputs or too large inputs.
     */
    EXECUTION_DENIED(true),

    /**
     * Subprocess did not execute, it may be the user's fault, and the error is catastrophic. The
     * user may be able to prevent it from reoccurring. For example, an input file's contents may
     * have been modified by the user intra-build.
     */
    EXECUTION_DENIED_CATASTROPHICALLY(true),

    /**
     * The result of the remotely executed Spawn could not be retrieved due to errors in the remote
     * caching layer.
     */
    REMOTE_CACHE_FAILED;

    private final boolean isUserError;

    Status(boolean isUserError) {
      this.isUserError = isUserError;
    }

    Status() {
      this(false);
    }

    public boolean isConsideredUserError() {
      return isUserError;
    }
  }

  /**
   * Returns whether the spawn was actually run, regardless of the exit code. I.e., returns {@code
   * true} if {@link #status} is any of {@link Status#SUCCESS}, {@link Status#NON_ZERO_EXIT}, {@link
   * Status#TIMEOUT} or {@link Status#OUT_OF_MEMORY}.
   *
   * <p>Returns false if there were errors that prevented the spawn from being run, such as network
   * errors, missing local files, errors setting up sandboxing, etc.
   */
  default boolean setupSuccess() {
    Status status = status();
    return status == Status.SUCCESS
        || status == Status.NON_ZERO_EXIT
        || status == Status.TIMEOUT
        || status == Status.OUT_OF_MEMORY;
  }

  /**
   * Returns true if the status was {@link Status#EXECUTION_FAILED_CATASTROPHICALLY} or {@link
   * Status#EXECUTION_DENIED_CATASTROPHICALLY}.
   */
  default boolean isCatastrophe() {
    return status() == Status.EXECUTION_FAILED_CATASTROPHICALLY
        || status() == Status.EXECUTION_DENIED_CATASTROPHICALLY;
  }

  /** Returns the status of the attempted Spawn execution. */
  Status status();

  /**
   * Returns the exit code of the subprocess if the subprocess was executed.
   *
   * <p>Returns zero if {@link #status} returns {@link Status#SUCCESS}.
   *
   * <p>Returns non-zero if {@link #status} returns {@link Status#NON_ZERO_EXIT} or {@link
   * Status#OUT_OF_MEMORY}.
   *
   * <p>Returns 128 + 14 (corresponding to the Unix signal SIGALRM) if {@link #status} returns
   * {@link Status#TIMEOUT}.
   *
   * <p>Otherwise, the returned value is not meaningful.
   */
  // TODO(mschaller): clean up all uses of this method when {@code !this.setupSuccess()}
  int exitCode();

  /**
   * A detailed representation of what failed if {@link #status} is not {@link Status#SUCCESS}, and
   * {@code null} otherwise.
   */
  @Nullable
  FailureDetail failureDetail();

  /**
   * Returns the host name of the executor or {@code null}. This information is intended for
   * debugging purposes, especially for remote execution systems. Remote caches usually do not store
   * the original host name, so this is generally {@code null} for cache hits.
   */
  @Nullable
  String getExecutorHostName();

  /**
   * Returns the name of the SpawnRunner that executed the spawn. It should always be defined,
   * unless isCacheHit is true, in which case the spawn was not actually run.
   */
  String getRunnerName();

  /** Returns optional details about the runner. */
  String getRunnerSubtype();

  /**
   * Returns the start time for the {@link Spawn}'s execution.
   *
   * @return the measurement, or null in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  @Nullable
  Instant getStartTime();

  /**
   * Returns the wall time taken by the {@link Spawn}'s execution.
   *
   * @return the measurement, or 0 in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  int getWallTimeInMs();

  /**
   * Returns the user time taken by the {@link Spawn}'s execution.
   *
   * @return the measurement, or 0 in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  int getUserTimeInMs();

  /**
   * Returns the system time taken by the {@link Spawn}'s execution.
   *
   * @return the measurement, or 0 in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  int getSystemTimeInMs();

  /**
   * Returns the number of block output operations during the {@link Spawn}'s execution.
   *
   * @return the measurement, or null in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  @Nullable
  Long getNumBlockOutputOperations();

  /**
   * Returns the number of block input operations during the {@link Spawn}'s execution.
   *
   * @return the measurement, or null in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  @Nullable
  Long getNumBlockInputOperations();

  /**
   * Returns the number of involuntary context switches during the {@link Spawn}'s execution.
   *
   * @return the measurement, or null in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  @Nullable
  Long getNumInvoluntaryContextSwitches();

  /**
   * Returns the memory in Kilobytes used during the {@link Spawn}'s execution. The spawn memory
   * based on the maximum resident set size during command execution.
   *
   * @return the measurement, or null in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  // TODO(b/181317827) implement for windows systems.
  @Nullable
  Long getMemoryInKb();

  SpawnMetrics getMetrics();

  /** Returns whether the spawn result was a cache hit. */
  boolean isCacheHit();

  /** Returns an optional custom failure message for the result. */
  default String getFailureMessage() {
    return "";
  }

  /**
   * Returns a {@link Spawn}'s output in-memory, if supported and available.
   *
   * <p>This behavior may be triggered with {@link
   * ExecutionRequirements#REMOTE_EXECUTION_INLINE_OUTPUTS}.
   */
  @Nullable
  default InputStream getInMemoryOutput(ActionInput output) {
    return null;
  }

  String getDetailMessage(
      String message,
      boolean catastrophe,
      boolean forciblyRunRemotely);

  /** Returns a file path to the action metadata log. */
  @Nullable
  MetadataLog getActionMetadataLog();

  /** Whether the spawn result was obtained through remote strategy. */
  boolean wasRemote();

  /** A unique identifier for the spawn. */
  @AutoValue
  @Immutable
  public abstract class Digest {
    public abstract String getHash();

    public abstract Long getSizeBytes();

    public static Digest of(String hash, Long sizeBytes) {
      return new AutoValue_SpawnResult_Digest(hash, sizeBytes);
    }
  }

  @Nullable
  default Digest getDigest() {
    return null;
  }

  /** Basic implementation of {@link SpawnResult}. */
  @Immutable
  @ThreadSafe
  final class SimpleSpawnResult implements SpawnResult {
    private final int exitCode;
    private final Status status;
    @Nullable private final FailureDetail failureDetail;
    private final String executorHostName;
    private final String runnerName;
    private final String runnerSubtype;
    private final SpawnMetrics spawnMetrics;
    @Nullable private final Instant startTime;
    private final int wallTimeInMs;
    private final int userTimeInMs;
    private final int systemTimeInMs;
    @Nullable private final Long numBlockOutputOperations;
    @Nullable private final Long numBlockInputOperations;
    @Nullable private final Long numInvoluntaryContextSwitches;
    @Nullable private final Long memoryKb;
    @Nullable private final MetadataLog actionMetadataLog;
    private final boolean cacheHit;
    private final String failureMessage;

    // Invariant: Either both have a value or both are null.
    @Nullable private final ActionInput inMemoryOutputFile;
    @Nullable private final ByteString inMemoryContents;

    private final boolean remote;
    @Nullable private final Digest digest;

    SimpleSpawnResult(Builder builder) {
      this.exitCode = builder.exitCode;
      this.status = Preconditions.checkNotNull(builder.status);
      this.failureDetail = builder.failureDetail;
      this.executorHostName = builder.executorHostName;
      this.runnerName = builder.runnerName;
      this.runnerSubtype = builder.runnerSubtype;
      this.spawnMetrics =
          builder.spawnMetrics != null
              ? builder.spawnMetrics
              : SpawnMetrics.forLocalExecution(builder.wallTimeInMs);
      this.startTime = builder.startTime;
      this.wallTimeInMs = builder.wallTimeInMs;
      this.userTimeInMs = builder.userTimeInMs;
      this.systemTimeInMs = builder.systemTimeInMs;
      this.numBlockOutputOperations = builder.numBlockOutputOperations;
      this.numBlockInputOperations = builder.numBlockInputOperations;
      this.numInvoluntaryContextSwitches = builder.numInvoluntaryContextSwitches;
      this.memoryKb = builder.memoryInKb;
      this.cacheHit = builder.cacheHit;
      this.failureMessage = builder.failureMessage;
      this.inMemoryOutputFile = builder.inMemoryOutputFile;
      this.inMemoryContents = builder.inMemoryContents;
      this.actionMetadataLog = builder.actionMetadataLog;
      this.remote = builder.remote;
      this.digest = builder.digest;
    }

    @Override
    public int exitCode() {
      return exitCode;
    }

    @Override
    public Status status() {
      return status;
    }

    @Override
    @Nullable
    public FailureDetail failureDetail() {
      return failureDetail;
    }

    @Override
    public String getExecutorHostName() {
      return executorHostName;
    }

    @Override
    public String getRunnerName() {
      return runnerName;
    }

    @Override
    public String getRunnerSubtype() {
      return runnerSubtype;
    }

    @Override
    public SpawnMetrics getMetrics() {
      return spawnMetrics;
    }

    @Override
    public Instant getStartTime() {
      return startTime;
    }

    @Override
    public int getWallTimeInMs() {
      return wallTimeInMs;
    }

    @Override
    public int getUserTimeInMs() {
      return userTimeInMs;
    }

    @Override
    public int getSystemTimeInMs() {
      return systemTimeInMs;
    }

    @Override
    public Long getNumBlockOutputOperations() {
      return numBlockOutputOperations;
    }

    @Override
    public Long getNumBlockInputOperations() {
      return numBlockInputOperations;
    }

    @Override
    public Long getNumInvoluntaryContextSwitches() {
      return numInvoluntaryContextSwitches;
    }

    @Override
    public Long getMemoryInKb() {
      return memoryKb;
    }

    @Override
    public boolean isCacheHit() {
      return cacheHit;
    }

    @Override
    public String getFailureMessage() {
      return failureMessage;
    }

    @Override
    public String getDetailMessage(
        String message,
        boolean catastrophe,
        boolean forciblyRunRemotely) {
      TerminationStatus status = new TerminationStatus(
          exitCode(), status() == Status.TIMEOUT);
      String reason = "(" + status.toShortString() + ")"; // e.g. "(Exit 1)"
      String explanation = Strings.isNullOrEmpty(message) ? "" : ": " + message;

      if (status() == Status.TIMEOUT) {
        // 0 wall time means no measurement
        if (getWallTimeInMs() != 0) {
          explanation +=
              String.format(
                  Locale.US,
                  " (failed due to timeout after %.2f seconds.)",
                  getWallTimeInMs() / 1000.0);
        } else {
          explanation += " (failed due to timeout.)";
        }
      } else if (status() == Status.OUT_OF_MEMORY) {
        explanation += " (Remote action was terminated due to Out of Memory.)";
      }
      if (status() != Status.TIMEOUT && forciblyRunRemotely) {
        explanation += " Action tagged as local was forcibly run remotely and failed - it's "
            + "possible that the action simply doesn't work remotely";
      }
      return reason + explanation;
    }

    @Nullable
    @Override
    public InputStream getInMemoryOutput(ActionInput output) {
      if (inMemoryOutputFile != null && inMemoryOutputFile.equals(output)) {
        return inMemoryContents.newInput();
      }
      return null;
    }

    @Override
    public MetadataLog getActionMetadataLog() {
      return actionMetadataLog;
    }

    @Override
    public boolean wasRemote() {
      return remote;
    }

    @Override
    public Digest getDigest() {
      return digest;
    }
  }

  /** Builder class for {@link SpawnResult}. */
  final class Builder {
    private int exitCode;
    private Status status;
    private FailureDetail failureDetail;
    private String executorHostName;
    private String runnerName = "";
    private String runnerSubtype = "";
    private SpawnMetrics spawnMetrics;
    private Instant startTime;
    private int wallTimeInMs;
    private int userTimeInMs;
    private int systemTimeInMs;
    private Long numBlockOutputOperations;
    private Long numBlockInputOperations;
    private Long numInvoluntaryContextSwitches;
    private Long memoryInKb;
    private MetadataLog actionMetadataLog;
    private boolean cacheHit;
    private String failureMessage = "";

    // Invariant: Either both have a value or both are null.
    @Nullable private ActionInput inMemoryOutputFile;
    @Nullable private ByteString inMemoryContents;

    private boolean remote;
    private Digest digest;

    public SpawnResult build() {
      Preconditions.checkArgument(!runnerName.isEmpty());

      switch (status) {
        case SUCCESS:
          Preconditions.checkArgument(exitCode == 0, exitCode);
          Preconditions.checkArgument(failureDetail == null, failureDetail);
          break;
        case TIMEOUT:
          Preconditions.checkArgument(exitCode == POSIX_TIMEOUT_EXIT_CODE, exitCode);
          // Fall through.
        default:
          Preconditions.checkArgument(
              exitCode != 0,
              "Failed spawn with status %s had exit code 0 (%s %s)",
              status,
              failureMessage,
              failureDetail);
          Preconditions.checkArgument(
              failureDetail != null,
              "Failed spawn with status %s and exit code %s had no failure detail (%s)",
              status,
              exitCode,
              failureMessage);
          if (!status.isConsideredUserError()
              && ExitCode.BUILD_FAILURE.equals(DetailedExitCode.getExitCode(failureDetail))) {
            BugReport.sendBugReport(
                new IllegalStateException(
                    String.format(
                        "System error %s should not have failure detail %s with 'build failure'"
                            + " exit code (%s)",
                        status, failureDetail, failureMessage)));
          }
      }

      return new SimpleSpawnResult(this);
    }

    @CanIgnoreReturnValue
    public Builder setExitCode(int exitCode) {
      this.exitCode = exitCode;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setStatus(Status status) {
      this.status = status;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setFailureDetail(FailureDetail failureDetail) {
      this.failureDetail = failureDetail;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setExecutorHostname(String executorHostName) {
      this.executorHostName = executorHostName;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setRunnerName(String runnerName) {
      this.runnerName = runnerName;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setSpawnMetrics(SpawnMetrics spawnMetrics) {
      this.spawnMetrics = spawnMetrics;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setStartTime(Instant startTime) {
      this.startTime = startTime;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setWallTimeInMs(int wallTimeInMs) {
      this.wallTimeInMs = wallTimeInMs;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setUserTimeInMs(int userTimeInMs) {
      this.userTimeInMs = userTimeInMs;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setSystemTimeInMs(int systemTimeInMs) {
      this.systemTimeInMs = systemTimeInMs;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setNumBlockOutputOperations(long numBlockOutputOperations) {
      this.numBlockOutputOperations = numBlockOutputOperations;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setNumBlockInputOperations(long numBlockInputOperations) {
      this.numBlockInputOperations = numBlockInputOperations;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setNumInvoluntaryContextSwitches(long numInvoluntaryContextSwitches) {
      this.numInvoluntaryContextSwitches = numInvoluntaryContextSwitches;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setMemoryInKb(long memoryInKb) {
      this.memoryInKb = memoryInKb;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setCacheHit(boolean cacheHit) {
      this.cacheHit = cacheHit;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setFailureMessage(String failureMessage) {
      this.failureMessage = failureMessage;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setInMemoryOutput(ActionInput outputFile, ByteString contents) {
      this.inMemoryOutputFile = Preconditions.checkNotNull(outputFile);
      this.inMemoryContents = Preconditions.checkNotNull(contents);
      return this;
    }

    @CanIgnoreReturnValue
    Builder setActionMetadataLog(MetadataLog actionMetadataLog) {
      this.actionMetadataLog = actionMetadataLog;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setRemote(boolean remote) {
      this.remote = remote;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setDigest(Digest digest) {
      this.digest = digest;
      return this;
    }
  }

  /** A {@link Spawn}'s metadata name and {@link Path}. */
  final class MetadataLog {
    private final String name;
    private final Path filePath;

    public MetadataLog(String name, Path filePath) {
      this.name = name;
      this.filePath = filePath;
    }

    public String getName() {
      return this.name;
    }

    public Path getFilePath() {
      return this.filePath;
    }
  }
}
