Skip to main content

cabin_core/
process.rs

1//! Shared classification of a child process's exit status.
2//!
3//! Cabin spawns external tools (formatters, linters, the C/C++
4//! toolchain) and needs to report *how* they exited without leaking
5//! platform-specific `ExitStatus` details into its own error and
6//! report types. [`ExitStatusKind`] is that stable, serializable-free
7//! summary; [`exit_status_kind`] derives it once at the spawn site.
8
9/// Stringified exit-status kind preserved so the orchestration layer
10/// can decide whether to display an exit code or a signal.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ExitStatusKind {
13    /// The process exited normally with this code.
14    Code(i32),
15    /// The process was terminated by a signal (Unix only).
16    Signal(String),
17    /// The process exited with neither a code nor a signal;
18    /// preserved as a fallback only.
19    Unknown,
20}
21
22impl std::fmt::Display for ExitStatusKind {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            ExitStatusKind::Code(c) => write!(f, "{c}"),
26            ExitStatusKind::Signal(s) => write!(f, "signal {s}"),
27            ExitStatusKind::Unknown => write!(f, "<unknown>"),
28        }
29    }
30}
31
32/// Classify a finished [`std::process::ExitStatus`] into an
33/// [`ExitStatusKind`], preferring the exit code and falling back to the
34/// terminating signal on Unix.
35pub fn exit_status_kind(status: std::process::ExitStatus) -> ExitStatusKind {
36    if let Some(code) = status.code() {
37        return ExitStatusKind::Code(code);
38    }
39    #[cfg(unix)]
40    {
41        use std::os::unix::process::ExitStatusExt;
42        if let Some(sig) = status.signal() {
43            return ExitStatusKind::Signal(sig.to_string());
44        }
45    }
46    ExitStatusKind::Unknown
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn display_renders_each_variant() {
55        assert_eq!(ExitStatusKind::Code(0).to_string(), "0");
56        assert_eq!(ExitStatusKind::Code(-1).to_string(), "-1");
57        assert_eq!(
58            ExitStatusKind::Signal("11".to_owned()).to_string(),
59            "signal 11"
60        );
61        assert_eq!(ExitStatusKind::Unknown.to_string(), "<unknown>");
62    }
63
64    #[cfg(unix)]
65    #[test]
66    fn classifies_unix_wait_statuses() {
67        use std::os::unix::process::ExitStatusExt;
68        use std::process::ExitStatus;
69        // POSIX wait status encoding: the exit code lives in the
70        // high byte, a terminating signal in the low bits.
71        assert_eq!(
72            exit_status_kind(ExitStatus::from_raw(0)),
73            ExitStatusKind::Code(0)
74        );
75        assert_eq!(
76            exit_status_kind(ExitStatus::from_raw(2 << 8)),
77            ExitStatusKind::Code(2)
78        );
79        assert_eq!(
80            exit_status_kind(ExitStatus::from_raw(9)),
81            ExitStatusKind::Signal("9".to_owned())
82        );
83        // A stopped status (low byte 0x7f) carries neither an exit
84        // code nor a terminating signal: the fallback kicks in.
85        assert_eq!(
86            exit_status_kind(ExitStatus::from_raw(0x7f)),
87            ExitStatusKind::Unknown
88        );
89    }
90
91    #[cfg(windows)]
92    #[test]
93    fn classifies_windows_exit_codes() {
94        use std::os::windows::process::ExitStatusExt;
95        use std::process::ExitStatus;
96        assert_eq!(
97            exit_status_kind(ExitStatus::from_raw(0)),
98            ExitStatusKind::Code(0)
99        );
100        assert_eq!(
101            exit_status_kind(ExitStatus::from_raw(2)),
102            ExitStatusKind::Code(2)
103        );
104    }
105}