| // Copyright 2014 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.shell; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.time.Duration; |
| import java.util.Optional; |
| |
| /** |
| * Represents the termination status of a command. {@link Process#waitFor} is |
| * not very precisely specified, so this class encapsulates the interpretation |
| * of values returned by it. |
| * |
| * Caveat: due to the lossy encoding, it's not always possible to accurately |
| * distinguish signal and exit cases. In particular, processes that exit with |
| * a value within the interval [129, 191] will be mistaken for having been |
| * terminated by a signal. |
| * |
| * Instances are immutable. |
| */ |
| public final class TerminationStatus { |
| |
| private final int waitResult; |
| private final boolean timedOut; |
| private final Optional<Duration> wallExecutionTime; |
| private final Optional<Duration> userExecutionTime; |
| private final Optional<Duration> systemExecutionTime; |
| |
| /** |
| * Values taken from the glibc strsignal(3) function. |
| */ |
| private static final String[] SIGNAL_STRINGS = { |
| null, |
| "Hangup", |
| "Interrupt", |
| "Quit", |
| "Illegal instruction", |
| "Trace/breakpoint trap", |
| "Aborted", |
| "Bus error", |
| "Floating point exception", |
| "Killed", |
| "User defined signal 1", |
| "Segmentation fault", |
| "User defined signal 2", |
| "Broken pipe", |
| "Alarm clock", |
| "Terminated", |
| "Stack fault", |
| "Child exited", |
| "Continued", |
| "Stopped (signal)", |
| "Stopped", |
| "Stopped (tty input)", |
| "Stopped (tty output)", |
| "Urgent I/O condition", |
| "CPU time limit exceeded", |
| "File size limit exceeded", |
| "Virtual timer expired", |
| "Profiling timer expired", |
| "Window changed", |
| "I/O possible", |
| "Power failure", |
| "Bad system call", |
| }; |
| |
| private static String getSignalString(int signum) { |
| return signum > 0 && signum < SIGNAL_STRINGS.length |
| ? SIGNAL_STRINGS[signum] |
| : "Signal " + signum; |
| } |
| |
| /** |
| * Construct a TerminationStatus instance from a Process waitFor code. |
| * |
| * @param waitResult the value returned by {@link java.lang.Process#waitFor}. |
| * @param timedOut whether the execution timed out |
| */ |
| public TerminationStatus(int waitResult, boolean timedOut) { |
| this(waitResult, timedOut, Optional.empty(), Optional.empty(), Optional.empty()); |
| } |
| |
| /** |
| * Construct a TerminationStatus instance from a Process waitFor code. |
| * |
| * <p>TerminationStatus objects are considered equal if they have the same waitResult. |
| * |
| * @param waitResult the value returned by {@link java.lang.Process#waitFor}. |
| * @param timedOut whether the execution timed out |
| * @param wallExecutionTime the wall execution time of the command, if available |
| * @param userExecutionTime the user execution time of the command, if available |
| * @param systemExecutionTime the system execution time of the command, if available |
| */ |
| public TerminationStatus( |
| int waitResult, |
| boolean timedOut, |
| Optional<Duration> wallExecutionTime, |
| Optional<Duration> userExecutionTime, |
| Optional<Duration> systemExecutionTime) { |
| this.waitResult = waitResult; |
| this.timedOut = timedOut; |
| this.wallExecutionTime = wallExecutionTime; |
| this.userExecutionTime = userExecutionTime; |
| this.systemExecutionTime = systemExecutionTime; |
| } |
| |
| /** |
| * Returns the exit code returned by the subprocess. |
| */ |
| public int getRawExitCode() { |
| return waitResult; |
| } |
| |
| /** |
| * Returns true iff the process exited with code 0. |
| */ |
| public boolean success() { |
| return exited() && getExitCode() == 0; |
| } |
| |
| // We're relying on undocumented behaviour of Process.waitFor, specifically |
| // that waitResult is the exit status when the process returns normally, or |
| // 128+signalnumber when the process is terminated by a signal. We further |
| // assume that value signal numbers fall in the interval [1, 63]. |
| @VisibleForTesting static final int SIGNAL_1 = 128 + 1; |
| @VisibleForTesting static final int SIGNAL_63 = 128 + 63; |
| @VisibleForTesting static final int SIGNAL_SIGABRT = 128 + 6; |
| @VisibleForTesting static final int SIGNAL_SIGKILL = 128 + 9; |
| @VisibleForTesting static final int SIGNAL_SIGBUS = 128 + 10; |
| @VisibleForTesting static final int SIGNAL_SIGTERM = 128 + 15; |
| |
| /** |
| * Returns true if the given exit code represents a crash. |
| * |
| * <p>This is a static function that processes a raw exit status because that's all the |
| * information that we have around in the single use case of this function. Propagating a {@link |
| * TerminationStatus} object to that point would be costly. If this function is needed for |
| * anything else, then this should be reevaluated. |
| */ |
| public static boolean crashed(int rawStatus) { |
| return rawStatus == SIGNAL_SIGABRT || rawStatus == SIGNAL_SIGBUS; |
| } |
| |
| /** Returns true iff the process exited normally. */ |
| public boolean exited() { |
| return !timedOut && (waitResult < SIGNAL_1 || waitResult > SIGNAL_63); |
| } |
| |
| /** Returns true if the process timed out. */ |
| public boolean timedOut() { |
| return timedOut; |
| } |
| |
| /** |
| * Returns the exit code of the subprocess. Undefined if exited() is false. |
| */ |
| public int getExitCode() { |
| if (!exited()) { |
| throw new IllegalStateException("getExitCode() not defined"); |
| } |
| return waitResult; |
| } |
| |
| /** |
| * Returns the number of the signal that terminated the process. Undefined |
| * if exited() returns true. |
| */ |
| public int getTerminatingSignal() { |
| if (exited() || timedOut) { |
| throw new IllegalStateException("getTerminatingSignal() not defined"); |
| } |
| return waitResult - SIGNAL_1 + 1; |
| } |
| |
| /** |
| * Returns the wall execution time. |
| * |
| * @return the measurement, or empty in case of execution errors or when the measurement is not |
| * implemented for the current platform |
| */ |
| public Optional<Duration> getWallExecutionTime() { |
| return wallExecutionTime; |
| } |
| |
| /** |
| * Returns the user execution time. |
| * |
| * @return the measurement, or empty in case of execution errors or when the measurement is not |
| * implemented for the current platform |
| */ |
| public Optional<Duration> getUserExecutionTime() { |
| return userExecutionTime; |
| } |
| |
| /** |
| * Returns the system execution time. |
| * |
| * @return the measurement, or empty in case of execution errors or when the measurement is not |
| * implemented for the current platform |
| */ |
| public Optional<Duration> getSystemExecutionTime() { |
| return systemExecutionTime; |
| } |
| |
| /** |
| * Returns a short string describing the termination status. |
| * e.g. "Exit 1" or "Hangup". |
| */ |
| public String toShortString() { |
| return exited() |
| ? "Exit " + getExitCode() |
| : (timedOut ? "Timeout" : getSignalString(getTerminatingSignal())); |
| } |
| |
| @Override |
| public String toString() { |
| if (exited()) { |
| return "Process exited with status " + getExitCode(); |
| } else if (timedOut) { |
| return "Timed out"; |
| } else { |
| return "Process terminated by signal " + getTerminatingSignal(); |
| } |
| } |
| |
| @Override |
| public int hashCode() { |
| return waitResult; |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| return other instanceof TerminationStatus |
| && ((TerminationStatus) other).waitResult == this.waitResult; |
| } |
| |
| /** Returns a new {@link TerminationStatus.Builder}. */ |
| public static Builder builder() { |
| return new TerminationStatus.Builder(); |
| } |
| |
| /** Builder for {@link TerminationStatus} objects. */ |
| public static class Builder { |
| // We use nullness here instead of Optional to avoid confusion between fields that may not |
| // yet have been set from fields that can legitimately hold an Optional value in the built |
| // object. |
| |
| private Integer waitResponse = null; |
| private Boolean timedOut = null; |
| |
| private Optional<Duration> wallExecutionTime = Optional.empty(); |
| private Optional<Duration> userExecutionTime = Optional.empty(); |
| private Optional<Duration> systemExecutionTime = Optional.empty(); |
| |
| /** Sets the value returned by {@link java.lang.Process#waitFor}. */ |
| @CanIgnoreReturnValue |
| public Builder setWaitResponse(int waitResponse) { |
| this.waitResponse = waitResponse; |
| return this; |
| } |
| |
| /** Sets whether the action timed out or not. */ |
| @CanIgnoreReturnValue |
| public Builder setTimedOut(boolean timedOut) { |
| this.timedOut = timedOut; |
| return this; |
| } |
| |
| /** Sets the wall execution time. */ |
| @CanIgnoreReturnValue |
| public Builder setWallExecutionTime(Duration wallExecutionTime) { |
| this.wallExecutionTime = Optional.of(wallExecutionTime); |
| return this; |
| } |
| |
| /** Sets the user execution time. */ |
| @CanIgnoreReturnValue |
| public Builder setUserExecutionTime(Duration userExecutionTime) { |
| this.userExecutionTime = Optional.of(userExecutionTime); |
| return this; |
| } |
| |
| /** Sets the system execution time. */ |
| @CanIgnoreReturnValue |
| public Builder setSystemExecutionTime(Duration systemExecutionTime) { |
| this.systemExecutionTime = Optional.of(systemExecutionTime); |
| return this; |
| } |
| |
| /** Builds a {@link TerminationStatus} object. */ |
| public TerminationStatus build() { |
| if (waitResponse == null) { |
| throw new IllegalStateException("waitResponse never set"); |
| } |
| if (timedOut == null) { |
| throw new IllegalStateException("timedOut never set"); |
| } |
| return new TerminationStatus( |
| waitResponse, timedOut, wallExecutionTime, userExecutionTime, systemExecutionTime); |
| } |
| } |
| } |