#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ExitStatus {
exit_code: u32,
signal: Option<u32>,
}
impl ExitStatus {
#[must_use]
pub const fn with_exit_code(exit_code: u32) -> Self {
Self {
exit_code,
signal: None,
}
}
#[must_use]
pub const fn with_signal(signal: u32) -> Self {
Self {
exit_code: 1,
signal: Some(signal),
}
}
#[must_use]
pub const fn success(&self) -> bool {
self.signal.is_none() && self.exit_code == 0
}
#[must_use]
pub const fn exit_code(&self) -> u32 {
self.exit_code
}
#[must_use]
pub const fn signal(&self) -> Option<u32> {
self.signal
}
pub(crate) fn from_portable(status: portable_pty::ExitStatus) -> Self {
let signal = status.signal().and_then(signal_name_to_number);
Self {
exit_code: status.exit_code(),
signal,
}
}
}
fn signal_name_to_number(name: &str) -> Option<u32> {
let trimmed = name.trim();
let upper = trimmed.to_ascii_uppercase();
let stripped = upper.strip_prefix("SIG").unwrap_or(&upper);
SIGNAL_TABLE
.iter()
.find(|entry| entry.aliases.iter().any(|a| *a == stripped || *a == upper))
.map(|entry| entry.number)
}
struct SignalEntry {
aliases: &'static [&'static str],
number: u32,
}
static SIGNAL_TABLE: &[SignalEntry] = &[
SignalEntry {
aliases: &["HUP", "HANGUP"],
number: 1,
},
SignalEntry {
aliases: &["INT", "INTERRUPT"],
number: 2,
},
SignalEntry {
aliases: &["QUIT"],
number: 3,
},
SignalEntry {
aliases: &["KILL", "KILLED"],
number: 9,
},
SignalEntry {
aliases: &["USR1", "USER DEFINED SIGNAL 1"],
number: 10,
},
SignalEntry {
aliases: &["SEGV", "SEGMENTATION FAULT"],
number: 11,
},
SignalEntry {
aliases: &["USR2", "USER DEFINED SIGNAL 2"],
number: 12,
},
SignalEntry {
aliases: &["PIPE", "BROKEN PIPE"],
number: 13,
},
SignalEntry {
aliases: &["ALRM", "ALARM CLOCK"],
number: 14,
},
SignalEntry {
aliases: &["TERM", "TERMINATED"],
number: 15,
},
SignalEntry {
aliases: &["CONT", "CONTINUED"],
number: 18,
},
SignalEntry {
aliases: &["STOP", "STOPPED", "STOPPED (SIGNAL)"],
number: 19,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn portable_pty_clean_exit_decodes_to_no_signal() {
let upstream = portable_pty::ExitStatus::with_exit_code(0);
let wrapped = ExitStatus::from_portable(upstream);
assert_eq!(wrapped.exit_code(), 0);
assert_eq!(wrapped.signal(), None);
}
#[test]
fn portable_pty_named_signal_decodes_to_number() {
let upstream = portable_pty::ExitStatus::with_signal("Interrupt");
let wrapped = ExitStatus::from_portable(upstream);
assert_eq!(wrapped.signal(), Some(2));
assert_eq!(wrapped.exit_code(), 1);
}
#[test]
fn portable_pty_unknown_description_falls_back_to_no_signal() {
let upstream = portable_pty::ExitStatus::with_signal("Bus error");
let wrapped = ExitStatus::from_portable(upstream);
assert_eq!(wrapped.signal(), None);
assert_eq!(wrapped.exit_code(), 1);
}
#[test]
fn constructors_round_trip() {
let s = ExitStatus::with_exit_code(42);
assert_eq!(s.exit_code(), 42);
assert_eq!(s.signal(), None);
let s = ExitStatus::with_signal(15);
assert_eq!(s.exit_code(), 1);
assert_eq!(s.signal(), Some(15));
}
}