aion-worker 0.27.1

Rust remote-worker SDK for executing Aion activities over the gRPC worker protocol.
Documentation
//! How a command's process ENDED, read from the status rather than assumed.
//!
//! A process that was killed by a signal exited with no status code at all.
//! Reporting a number for it — any number — tells an operator the program
//! chose to stop when in fact something stopped it, and reporting one fixed
//! number tells them WHICH signal when the code never knew. A segmentation
//! fault reported as "exited 137" sends an operator looking for an
//! out-of-memory kill that never happened.
//!
//! So the ending is read as what it is, and the number that rides beside it
//! follows the shell's `128 + signal` convention — the number an operator
//! reading a status already recognises — with the signal named in words
//! alongside, because the convention is a rendering of the signal and not a
//! substitute for knowing which one it was.

use std::process::ExitStatus;

/// How a finished process ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Ending {
    /// The process exited on its own and named this status code.
    Exited(i32),
    /// The process was ended by this signal and named no status code.
    Signalled(i32),
    /// The process named neither a status code nor a signal.
    ///
    /// Not reachable on any platform this crate can spawn on — a POSIX wait
    /// status is one or the other, and process-group containment refuses to
    /// spawn anywhere else — but a status this side cannot read is carried as
    /// its own arm rather than folded into an invented exit code.
    Unstated,
}

impl Ending {
    /// Read how `status` ended.
    #[must_use]
    pub(super) fn of(status: ExitStatus) -> Self {
        #[cfg(unix)]
        {
            use std::os::unix::process::ExitStatusExt as _;
            match (status.code(), status.signal()) {
                (Some(code), _) => Self::Exited(code),
                (None, Some(signal)) => Self::Signalled(signal),
                (None, None) => Self::Unstated,
            }
        }
        #[cfg(not(unix))]
        {
            match status.code() {
                Some(code) => Self::Exited(code),
                None => Self::Unstated,
            }
        }
    }

    /// Whether the process ended by exiting zero.
    #[must_use]
    pub(super) const fn succeeded(self) -> bool {
        matches!(self, Self::Exited(0))
    }

    /// The number this ending reports as an exit code.
    ///
    /// An exit is its own code. A signal death is `128 + signal`, the shell's
    /// convention, which is a rendering of [`Self::described`] rather than a
    /// replacement for it. An ending that stated nothing reports nothing:
    /// there is no honest number for a status that named neither.
    #[must_use]
    pub(super) const fn reported_code(self) -> Option<i32> {
        match self {
            Self::Exited(code) => Some(code),
            Self::Signalled(signal) => Some(128 + signal),
            Self::Unstated => None,
        }
    }

    /// What happened, in the words an operator needs.
    #[must_use]
    pub(super) fn described(self) -> String {
        match self {
            Self::Exited(code) => format!("exited {code}"),
            Self::Signalled(signal) => match signal_name(signal) {
                Some(name) => format!("was ended by signal {signal} ({name}) rather than exiting"),
                None => format!("was ended by signal {signal} rather than exiting"),
            },
            Self::Unstated => "ended without saying whether it exited or was signalled".to_owned(),
        }
    }
}

/// The conventional name of a signal number, when the host knows one.
#[cfg(unix)]
fn signal_name(signal: i32) -> Option<&'static str> {
    nix::sys::signal::Signal::try_from(signal)
        .ok()
        .map(nix::sys::signal::Signal::as_str)
}

/// Off Unix there are no signal numbers to name.
#[cfg(not(unix))]
const fn signal_name(_signal: i32) -> Option<&'static str> {
    None
}

#[cfg(test)]
mod tests {
    use super::Ending;

    #[test]
    fn an_exit_reports_its_own_code_and_says_it_exited() {
        assert_eq!(Ending::Exited(0).reported_code(), Some(0));
        assert!(Ending::Exited(0).succeeded());
        assert_eq!(Ending::Exited(3).reported_code(), Some(3));
        assert!(!Ending::Exited(3).succeeded());
        assert_eq!(Ending::Exited(3).described(), "exited 3");
    }

    /// THE DEFECT THIS FILE EXISTS FOR: two different signal deaths must not
    /// be reported as the same thing.
    #[test]
    fn two_different_signals_are_two_different_reports() {
        let segfault = Ending::Signalled(11);
        let killed = Ending::Signalled(9);
        assert_ne!(segfault.reported_code(), killed.reported_code());
        assert_ne!(segfault.described(), killed.described());
        assert_eq!(killed.reported_code(), Some(137));
        assert_eq!(segfault.reported_code(), Some(139));
        assert!(!segfault.succeeded());
        assert!(
            segfault.described().contains("signal 11"),
            "{}",
            segfault.described()
        );
    }

    #[cfg(unix)]
    #[test]
    fn a_signal_is_named_in_words_beside_its_number() {
        assert!(
            Ending::Signalled(11).described().contains("SIGSEGV"),
            "{}",
            Ending::Signalled(11).described()
        );
        assert!(
            Ending::Signalled(9).described().contains("SIGKILL"),
            "{}",
            Ending::Signalled(9).described()
        );
    }

    /// A status that named nothing gets no invented number.
    #[test]
    fn an_unstated_ending_reports_no_code() {
        assert_eq!(Ending::Unstated.reported_code(), None);
        assert!(!Ending::Unstated.succeeded());
    }

    /// Read off a REAL wait status, so the arms are not only asserted about
    /// values this test constructed.
    #[tokio::test]
    async fn a_real_process_that_exits_is_read_as_an_exit() -> Result<(), Box<dyn std::error::Error>>
    {
        let status = tokio::process::Command::new("sh")
            .arg("-c")
            .arg("exit 7")
            .status()
            .await?;
        assert_eq!(Ending::of(status), Ending::Exited(7));
        Ok(())
    }
}