Skip to main content

rlmctl_core/
exit.rs

1//! How a command launched under limits ended, shared by the CLI and GUI.
2
3/// The conventional name of a signal number, or a generic description.
4pub fn signal_name(sig: i32) -> &'static str {
5    match sig {
6        1 => "SIGHUP",
7        2 => "SIGINT",
8        3 => "SIGQUIT",
9        4 => "SIGILL",
10        6 => "SIGABRT",
11        7 => "SIGBUS",
12        8 => "SIGFPE",
13        9 => "SIGKILL",
14        11 => "SIGSEGV",
15        13 => "SIGPIPE",
16        14 => "SIGALRM",
17        15 => "SIGTERM",
18        32..=64 => "a real-time signal",
19        _ => "an unknown signal",
20    }
21}
22
23/// (exit code for the caller, messages without a "rlm:" prefix)
24///
25/// A normal exit keeps its code; a signal death becomes 128 + the signal
26/// number, capped at 255; anything else is 1. Messages say which signal
27/// ended the command and whether the memory limit made the kernel kill
28/// processes in the cgroup.
29pub fn exit_report(
30    code: Option<i32>,
31    signal: Option<i32>,
32    oom_kills: u64,
33    memory_max: Option<u64>,
34) -> (u8, Vec<String>) {
35    let exit = code
36        .map(|c| (c & 0xff) as u8)
37        .or_else(|| signal.map(|s| (128 + s).clamp(0, 255) as u8))
38        .unwrap_or(1);
39    let mut messages = Vec::new();
40    if let Some(s) = signal {
41        messages.push(format!(
42            "the command was killed by signal {s} ({})",
43            signal_name(s)
44        ));
45    }
46    if oom_kills > 0 {
47        let limit = memory_max
48            .map(common::format_bytes)
49            .unwrap_or_else(|| "memory.max".to_string());
50        let noun = if oom_kills == 1 {
51            "process"
52        } else {
53            "processes"
54        };
55        messages.push(format!(
56            "the memory limit ({limit}) was reached; the kernel killed {oom_kills} {noun} in this cgroup"
57        ));
58    }
59    (exit, messages)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    const M150: u64 = 150 * 1024 * 1024;
66
67    #[test]
68    fn exit_codes_and_messages() {
69        assert_eq!(exit_report(Some(0), None, 0, None), (0, vec![]));
70        assert_eq!(exit_report(Some(3), None, 0, None), (3, vec![]));
71        let (code, msgs) = exit_report(None, Some(9), 1, Some(M150));
72        assert_eq!(code, 137);
73        assert_eq!(msgs[0], "the command was killed by signal 9 (SIGKILL)");
74        assert_eq!(
75            msgs[1],
76            "the memory limit (150.0M) was reached; the kernel killed 1 process in this cgroup"
77        );
78        let (code, msgs) = exit_report(Some(1), None, 2, Some(M150));
79        assert_eq!(code, 1);
80        assert_eq!(msgs, vec!["the memory limit (150.0M) was reached; the kernel killed 2 processes in this cgroup".to_string()]);
81        assert_eq!(exit_report(None, Some(15), 0, None).0, 143);
82        assert_eq!(exit_report(None, None, 0, None).0, 1);
83    }
84
85    #[test]
86    fn unknown_signals_have_a_generic_name() {
87        assert_eq!(signal_name(11), "SIGSEGV");
88        assert_eq!(signal_name(40), "a real-time signal");
89    }
90}