Skip to main content

shep_core/
signals.rs

1//! The signals `shep signal` may name.
2//!
3//! A grammar of its own, next to [`KillSignal`](crate::config::KillSignal)'s
4//! four: that one is what a Flockfile's `kill_signal` may say and the stop
5//! ladder delivers; this one is a nudge an operator hands a running app,
6//! with no ladder and no escalation.
7//!
8//! No `as_raw`: signal numbers are not portable (`SIGUSR1` is 10 on Linux,
9//! 30 on macOS), and shep-core has no libc to ask.
10//!
11//! `SIGSTOP` parses to nothing: a `SIGSTOP`ed sheep still reads `online`
12//! everywhere shep reports state. `SIGCONT` is accepted, for the way back.
13
14/// A signal `shep signal` may name.
15///
16/// Nine, not every signal on the platform: each is something an operator
17/// plausibly means to say to an application. Nothing here is a signal shep
18/// would deliver on the kernel's behalf (`SIGSEGV`, `SIGBUS`, `SIGPIPE` and
19/// the rest are the kernel's to send).
20///
21/// Exhaustive, not `#[non_exhaustive]`: a caller matching on all nine
22/// should get a compile error the day a tenth arrives, not a silent
23/// wildcard arm.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum OperatorSignal {
26    /// `SIGHUP`: hang up, the near-universal "re-read your configuration".
27    Hup,
28    /// `SIGINT`: interrupt, what Ctrl-C sends.
29    Int,
30    /// `SIGQUIT`: quit, core-dumping by default. Several runtimes dump every
31    /// thread's stack on it instead.
32    Quit,
33    /// `SIGTERM`: the polite stop. Sending it here bypasses the stop ladder
34    /// entirely: shep does not start a `kill_timeout`, does not escalate, and
35    /// does not mark the sheep stopped. Use `shep stop` for a stop.
36    Term,
37    /// `SIGUSR1`: user-defined signal 1.
38    Usr1,
39    /// `SIGUSR2`: user-defined signal 2, the one several runtimes reserve for
40    /// a graceful restart.
41    Usr2,
42    /// `SIGWINCH`: terminal resized. Harmless to nearly everything, which is
43    /// what makes it the signal to test a wiring with.
44    Winch,
45    /// `SIGCONT`: continue a stopped process.
46    Cont,
47    /// `SIGKILL`: unblockable, immediate. The restart policy will see the
48    /// exit as any other unexpected one and act on it: an app with
49    /// `autorestart` on comes back.
50    Kill,
51}
52
53impl OperatorSignal {
54    /// Every spelling this grammar accepts, canonical form, in the order a
55    /// refusal lists them.
56    ///
57    /// Public because it is rendered into the refusal an operator reads and
58    /// into `shep signal --help`; a second hand-written list in either place
59    /// is one free to drift.
60    pub const ACCEPTED: [&'static str; 9] = [
61        "SIGHUP", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGCONT",
62        "SIGKILL",
63    ];
64
65    /// Parses one signal name, case-insensitively, with or without the `SIG`
66    /// prefix. `None` for anything else, including a raw number: a number
67    /// means different signals on different platforms, and shep will not
68    /// guess which one an operator meant.
69    #[must_use]
70    pub fn parse(name: &str) -> Option<Self> {
71        match name.to_ascii_uppercase().as_str() {
72            "SIGHUP" | "HUP" => Some(Self::Hup),
73            "SIGINT" | "INT" => Some(Self::Int),
74            "SIGQUIT" | "QUIT" => Some(Self::Quit),
75            "SIGTERM" | "TERM" => Some(Self::Term),
76            "SIGUSR1" | "USR1" => Some(Self::Usr1),
77            "SIGUSR2" | "USR2" => Some(Self::Usr2),
78            "SIGWINCH" | "WINCH" => Some(Self::Winch),
79            "SIGCONT" | "CONT" => Some(Self::Cont),
80            "SIGKILL" | "KILL" => Some(Self::Kill),
81            _ => None,
82        }
83    }
84
85    /// The canonical name, always `SIG`-prefixed and uppercase.
86    #[must_use]
87    pub fn as_str(self) -> &'static str {
88        match self {
89            Self::Hup => "SIGHUP",
90            Self::Int => "SIGINT",
91            Self::Quit => "SIGQUIT",
92            Self::Term => "SIGTERM",
93            Self::Usr1 => "SIGUSR1",
94            Self::Usr2 => "SIGUSR2",
95            Self::Winch => "SIGWINCH",
96            Self::Cont => "SIGCONT",
97            Self::Kill => "SIGKILL",
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    /// The list is what a refusal prints, so a name advertised but not
107    /// parsed sends an operator in a circle.
108    #[test]
109    fn every_accepted_name_round_trips_through_parse() {
110        for name in OperatorSignal::ACCEPTED {
111            let parsed = OperatorSignal::parse(name)
112                .unwrap_or_else(|| panic!("`{name}` is advertised but not parsed"));
113            assert_eq!(parsed.as_str(), name);
114        }
115    }
116
117    /// Both accepted for the reason `KillSignal` accepts both: an operator
118    /// types what `kill -l` prints, the bare form.
119    #[test]
120    fn the_prefix_and_the_case_are_both_optional() {
121        assert_eq!(OperatorSignal::parse("hup"), Some(OperatorSignal::Hup));
122        assert_eq!(OperatorSignal::parse("SigUsr1"), Some(OperatorSignal::Usr1));
123        assert_eq!(OperatorSignal::parse("WINCH"), Some(OperatorSignal::Winch));
124    }
125
126    /// The one real, spellable, deliverable signal this grammar refuses: a
127    /// stopped sheep still reads `online` everywhere shep reports state.
128    #[test]
129    fn sigstop_is_refused_because_the_shepherd_could_not_report_it() {
130        assert_eq!(OperatorSignal::parse("SIGSTOP"), None);
131        assert_eq!(OperatorSignal::parse("stop"), None);
132    }
133
134    /// `SIGSEGV` is the shape that matters: a real signal, plausibly typed,
135    /// that shep has no business delivering on an operator's behalf.
136    #[test]
137    fn a_name_outside_the_table_does_not_parse() {
138        assert_eq!(OperatorSignal::parse("SIGSEGV"), None);
139        assert_eq!(OperatorSignal::parse(""), None);
140        assert_eq!(OperatorSignal::parse("9"), None);
141    }
142
143    /// The two exist for different jobs and may differ, but the
144    /// operator-facing set being narrower than the config-facing one would
145    /// mean an operator cannot name a signal shep itself sends.
146    #[test]
147    fn every_kill_signal_name_is_also_an_operator_signal() {
148        for name in crate::config::KillSignal::ACCEPTED {
149            assert!(
150                OperatorSignal::parse(name).is_some(),
151                "`{name}` is a kill_signal but not an operator signal"
152            );
153        }
154    }
155}