Skip to main content

mod_signal/
signal.rs

1//! Cross-platform [`Signal`] enum and bit-packed [`SignalSet`].
2
3use core::fmt;
4
5/// A platform-neutral signal identifier.
6///
7/// Variants map to their nearest platform equivalent. On Unix the
8/// mapping is direct (SIGTERM, SIGINT, etc.). On Windows the mapping
9/// is to Windows console control events:
10///
11/// | Variant      | Unix     | Windows             |
12/// | ------------ | -------- | ------------------- |
13/// | `Terminate`  | SIGTERM  | `CTRL_CLOSE_EVENT`  |
14/// | `Interrupt`  | SIGINT   | `CTRL_C_EVENT`      |
15/// | `Quit`       | SIGQUIT  | `CTRL_BREAK_EVENT`  |
16/// | `Hangup`     | SIGHUP   | `CTRL_SHUTDOWN_EVENT` |
17/// | `Pipe`       | SIGPIPE  | inert               |
18/// | `User1`      | SIGUSR1  | inert               |
19/// | `User2`      | SIGUSR2  | inert               |
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum Signal {
23    /// SIGTERM on Unix, `CTRL_CLOSE_EVENT` on Windows.
24    Terminate,
25    /// SIGINT (Ctrl+C) on Unix, `CTRL_C_EVENT` on Windows.
26    Interrupt,
27    /// SIGQUIT on Unix, `CTRL_BREAK_EVENT` on Windows.
28    Quit,
29    /// SIGHUP on Unix, `CTRL_SHUTDOWN_EVENT` on Windows.
30    Hangup,
31    /// SIGPIPE (Unix only; inert on Windows).
32    Pipe,
33    /// SIGUSR1 (Unix only; inert on Windows).
34    User1,
35    /// SIGUSR2 (Unix only; inert on Windows).
36    User2,
37}
38
39impl Signal {
40    /// All defined variants, in canonical order.
41    pub const ALL: [Self; 7] = [
42        Self::Terminate,
43        Self::Interrupt,
44        Self::Quit,
45        Self::Hangup,
46        Self::Pipe,
47        Self::User1,
48        Self::User2,
49    ];
50
51    /// Human-readable description used by `Display` and logging.
52    #[must_use]
53    pub const fn description(self) -> &'static str {
54        match self {
55            Self::Terminate => "Terminate (SIGTERM / CTRL_CLOSE_EVENT)",
56            Self::Interrupt => "Interrupt (SIGINT / CTRL_C_EVENT)",
57            Self::Quit => "Quit (SIGQUIT / CTRL_BREAK_EVENT)",
58            Self::Hangup => "Hangup (SIGHUP / CTRL_SHUTDOWN_EVENT)",
59            Self::Pipe => "Pipe (SIGPIPE, Unix only)",
60            Self::User1 => "User1 (SIGUSR1, Unix only)",
61            Self::User2 => "User2 (SIGUSR2, Unix only)",
62        }
63    }
64
65    /// Unix signal number for this variant. Returns `None` for
66    /// variants that have no canonical Unix number (none currently,
67    /// but the API reserves the right to add Windows-only variants).
68    #[must_use]
69    pub const fn unix_number(self) -> Option<i32> {
70        match self {
71            Self::Hangup => Some(1),
72            Self::Interrupt => Some(2),
73            Self::Quit => Some(3),
74            Self::User1 => Some(10),
75            Self::User2 => Some(12),
76            Self::Pipe => Some(13),
77            Self::Terminate => Some(15),
78        }
79    }
80
81    /// Returns `true` for variants that have no Windows analog.
82    #[must_use]
83    pub const fn is_unix_only(self) -> bool {
84        matches!(self, Self::Pipe | Self::User1 | Self::User2)
85    }
86
87    /// Returns `true` if installing a handler for this signal is
88    /// expected to succeed on the platform this binary is running on.
89    #[must_use]
90    pub const fn available_on_current_platform(self) -> bool {
91        if cfg!(unix) {
92            true
93        } else {
94            !self.is_unix_only()
95        }
96    }
97
98    pub(crate) const fn bit(self) -> u16 {
99        match self {
100            Self::Terminate => 1 << 0,
101            Self::Interrupt => 1 << 1,
102            Self::Quit => 1 << 2,
103            Self::Hangup => 1 << 3,
104            Self::Pipe => 1 << 4,
105            Self::User1 => 1 << 5,
106            Self::User2 => 1 << 6,
107        }
108    }
109}
110
111impl fmt::Display for Signal {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str(self.description())
114    }
115}
116
117/// A bit-packed set of [`Signal`] values.
118///
119/// `SignalSet` is `Copy` and const-constructible. The recommended
120/// constructors are [`SignalSet::graceful`] (the default for
121/// long-running services) and [`SignalSet::standard`].
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct SignalSet {
124    bits: u16,
125}
126
127impl SignalSet {
128    /// Bit mask covering every defined variant.
129    const ALL_BITS: u16 = 0b0111_1111;
130
131    /// Empty set.
132    #[must_use]
133    pub const fn empty() -> Self {
134        Self { bits: 0 }
135    }
136
137    /// All defined variants enabled. Inert variants on the current
138    /// platform are still represented but ignored by `install`.
139    #[must_use]
140    pub const fn all() -> Self {
141        Self {
142            bits: Self::ALL_BITS,
143        }
144    }
145
146    /// The recommended default: `Terminate | Interrupt | Hangup`.
147    #[must_use]
148    pub const fn graceful() -> Self {
149        Self::empty()
150            .with(Signal::Terminate)
151            .with(Signal::Interrupt)
152            .with(Signal::Hangup)
153    }
154
155    /// Maximum graceful coverage: `Terminate | Interrupt | Quit | Hangup`.
156    #[must_use]
157    pub const fn standard() -> Self {
158        Self::graceful().with(Signal::Quit)
159    }
160
161    /// Return a copy of `self` with `sig` enabled.
162    #[must_use]
163    pub const fn with(self, sig: Signal) -> Self {
164        Self {
165            bits: self.bits | sig.bit(),
166        }
167    }
168
169    /// Return a copy of `self` with `sig` disabled.
170    #[must_use]
171    pub const fn without(self, sig: Signal) -> Self {
172        Self {
173            bits: self.bits & !sig.bit(),
174        }
175    }
176
177    /// Check whether `sig` is enabled.
178    #[must_use]
179    pub const fn contains(self, sig: Signal) -> bool {
180        (self.bits & sig.bit()) != 0
181    }
182
183    /// `true` if the set has no signals enabled.
184    #[must_use]
185    pub const fn is_empty(self) -> bool {
186        self.bits == 0
187    }
188
189    /// Number of signals enabled in the set.
190    #[must_use]
191    pub const fn len(self) -> usize {
192        self.bits.count_ones() as usize
193    }
194
195    /// Iterate the enabled signals in canonical [`Signal::ALL`] order.
196    #[must_use]
197    pub const fn iter(&self) -> SignalSetIter {
198        SignalSetIter {
199            set: *self,
200            index: 0,
201        }
202    }
203}
204
205impl Default for SignalSet {
206    fn default() -> Self {
207        Self::graceful()
208    }
209}
210
211impl IntoIterator for SignalSet {
212    type Item = Signal;
213    type IntoIter = SignalSetIter;
214    fn into_iter(self) -> Self::IntoIter {
215        self.iter()
216    }
217}
218
219/// Iterator over the signals enabled in a [`SignalSet`].
220#[derive(Debug, Clone)]
221pub struct SignalSetIter {
222    set: SignalSet,
223    index: usize,
224}
225
226impl Iterator for SignalSetIter {
227    type Item = Signal;
228
229    fn next(&mut self) -> Option<Signal> {
230        while self.index < Signal::ALL.len() {
231            let sig = Signal::ALL[self.index];
232            self.index += 1;
233            if self.set.contains(sig) {
234                return Some(sig);
235            }
236        }
237        None
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn signal_display_matches_description() {
247        for s in Signal::ALL {
248            assert_eq!(format!("{s}"), s.description());
249        }
250    }
251
252    #[test]
253    fn signal_unix_number_round_trip() {
254        assert_eq!(Signal::Terminate.unix_number(), Some(15));
255        assert_eq!(Signal::Interrupt.unix_number(), Some(2));
256        assert_eq!(Signal::Hangup.unix_number(), Some(1));
257        assert_eq!(Signal::Quit.unix_number(), Some(3));
258        assert_eq!(Signal::Pipe.unix_number(), Some(13));
259        assert_eq!(Signal::User1.unix_number(), Some(10));
260        assert_eq!(Signal::User2.unix_number(), Some(12));
261    }
262
263    #[test]
264    fn is_unix_only_is_correct() {
265        assert!(Signal::Pipe.is_unix_only());
266        assert!(Signal::User1.is_unix_only());
267        assert!(Signal::User2.is_unix_only());
268        assert!(!Signal::Terminate.is_unix_only());
269        assert!(!Signal::Interrupt.is_unix_only());
270        assert!(!Signal::Quit.is_unix_only());
271        assert!(!Signal::Hangup.is_unix_only());
272    }
273
274    #[test]
275    fn set_empty_and_all() {
276        assert!(SignalSet::empty().is_empty());
277        assert_eq!(SignalSet::empty().len(), 0);
278        assert_eq!(SignalSet::all().len(), 7);
279    }
280
281    #[test]
282    fn set_graceful_contents() {
283        let g = SignalSet::graceful();
284        assert!(g.contains(Signal::Terminate));
285        assert!(g.contains(Signal::Interrupt));
286        assert!(g.contains(Signal::Hangup));
287        assert!(!g.contains(Signal::Quit));
288        assert!(!g.contains(Signal::Pipe));
289        assert_eq!(g.len(), 3);
290    }
291
292    #[test]
293    fn set_standard_contents() {
294        let s = SignalSet::standard();
295        assert!(s.contains(Signal::Quit));
296        assert_eq!(s.len(), 4);
297    }
298
299    #[test]
300    fn with_without_idempotent() {
301        let s = SignalSet::empty()
302            .with(Signal::Terminate)
303            .with(Signal::Terminate);
304        assert_eq!(s.len(), 1);
305        let s = s.without(Signal::Terminate).without(Signal::Terminate);
306        assert!(s.is_empty());
307    }
308
309    #[test]
310    fn iter_canonical_order() {
311        let s = SignalSet::all();
312        let v: Vec<Signal> = s.iter().collect();
313        assert_eq!(v, Signal::ALL.to_vec());
314    }
315
316    #[test]
317    fn default_is_graceful() {
318        assert_eq!(SignalSet::default(), SignalSet::graceful());
319    }
320}