lean_ctx/shell/
exit_status.rs1use std::process::ExitStatus;
10
11pub fn exit_code(status: ExitStatus) -> i32 {
15 #[cfg(unix)]
16 {
17 use std::os::unix::process::ExitStatusExt;
18 if let Some(sig) = status.signal() {
19 return 128 + sig;
20 }
21 }
22 status.code().unwrap_or(1)
23}
24
25pub(crate) fn signal_name(code: i32) -> Option<&'static str> {
27 #[cfg(unix)]
28 {
29 let sig = code.checked_sub(128).filter(|s| *s > 0)?;
30 let name = match sig {
31 libc::SIGHUP => "SIGHUP",
32 libc::SIGINT => "SIGINT",
33 libc::SIGQUIT => "SIGQUIT",
34 libc::SIGILL => "SIGILL",
35 libc::SIGTRAP => "SIGTRAP",
36 libc::SIGABRT => "SIGABRT",
37 libc::SIGBUS => "SIGBUS",
38 libc::SIGFPE => "SIGFPE",
39 libc::SIGKILL => "SIGKILL",
40 libc::SIGUSR1 => "SIGUSR1",
41 libc::SIGSEGV => "SIGSEGV",
42 libc::SIGUSR2 => "SIGUSR2",
43 libc::SIGPIPE => "SIGPIPE",
44 libc::SIGALRM => "SIGALRM",
45 libc::SIGTERM => "SIGTERM",
46 libc::SIGXCPU => "SIGXCPU",
47 libc::SIGXFSZ => "SIGXFSZ",
48 _ => return None,
49 };
50 Some(name)
51 }
52 #[cfg(not(unix))]
53 {
54 let _ = code;
55 None
56 }
57}
58
59pub fn exit_marker(code: i32) -> Option<String> {
64 match code {
65 0 => None,
66 124 => Some("[exit:124 — command timed out]".to_string()),
67 _ => Some(match signal_name(code) {
68 Some(name) => format!("[exit:{code} — {name}]"),
69 None => format!("[exit:{code}]"),
70 }),
71 }
72}
73
74pub fn exit_footer(code: i32) -> String {
77 exit_marker(code).map_or_else(String::new, |m| format!("\n{m}"))
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn footer_for_success_is_empty() {
86 assert_eq!(exit_footer(0), "");
87 assert_eq!(exit_marker(0), None);
88 }
89
90 #[test]
91 fn footer_keeps_plain_and_timeout_formats() {
92 assert_eq!(exit_footer(1), "\n[exit:1]");
93 assert_eq!(exit_footer(2), "\n[exit:2]");
94 assert_eq!(exit_footer(124), "\n[exit:124 — command timed out]");
95 assert_eq!(exit_footer(128), "\n[exit:128]");
96 }
97
98 #[cfg(unix)]
99 #[test]
100 fn footer_names_signal_terminations() {
101 assert_eq!(exit_footer(130), "\n[exit:130 — SIGINT]");
102 assert_eq!(exit_footer(137), "\n[exit:137 — SIGKILL]");
103 assert_eq!(exit_footer(143), "\n[exit:143 — SIGTERM]");
104 assert_eq!(exit_marker(141).as_deref(), Some("[exit:141 — SIGPIPE]"));
105 }
106
107 #[cfg(unix)]
108 fn run_sh(script: &str) -> ExitStatus {
109 std::process::Command::new("sh")
110 .arg("-c")
111 .arg(script)
112 .status()
113 .expect("spawn sh")
114 }
115
116 #[cfg(unix)]
117 #[test]
118 fn exit_code_passes_normal_codes_through() {
119 assert_eq!(exit_code(run_sh("exit 0")), 0);
120 assert_eq!(exit_code(run_sh("exit 3")), 3);
121 }
122
123 #[cfg(unix)]
125 #[test]
126 fn exit_code_maps_signal_termination_to_128_plus_signal() {
127 assert_eq!(exit_code(run_sh("kill -TERM $$")), 143);
128 assert_eq!(exit_code(run_sh("kill -KILL $$")), 137);
129 }
130
131 #[cfg(unix)]
132 #[test]
133 fn exit_code_from_raw_wait_status() {
134 use std::os::unix::process::ExitStatusExt;
135 assert_eq!(exit_code(ExitStatus::from_raw(libc::SIGTERM)), 143);
137 assert_eq!(exit_code(ExitStatus::from_raw(2 << 8)), 2);
139 }
140}