Skip to main content

command_stream/
signal.rs

1//! Signal delivery and the signal exit-code convention.
2//!
3//! Both runners stop processes the same way, so the signal vocabulary lives in
4//! one place instead of being duplicated per runner:
5//!
6//!   * [`ProcessRunner::kill`](crate::ProcessRunner::kill) /
7//!     [`ProcessRunner::kill_with`](crate::ProcessRunner::kill_with)
8//!   * [`OutputStream::kill`](crate::OutputStream::kill) /
9//!     [`OutputStream::kill_with`](crate::OutputStream::kill_with)
10//!
11//! The model mirrors the JavaScript implementation:
12//!
13//!   1. The requested signal is delivered to the child **and** its process
14//!      group, so grandchildren spawned by a shell are stopped too. The group
15//!      is skipped for a child that shares the caller's terminal, which stays
16//!      in the caller's process group by design so that CTRL+C keeps reaching
17//!      it.
18//!   2. The child is given a grace period ([`DEFAULT_KILL_GRACE_MS`]) to run its
19//!      own signal handler and exit on its own terms.
20//!   3. If it is still alive when the grace period expires, `SIGKILL` follows,
21//!      so a process that ignores the signal still terminates.
22//!   4. The reported exit code is the conventional `128 + signal` value
23//!      ([`signal_exit_code`]).
24//!
25//! A grace period of zero collapses steps 1 to 3 into `SIGKILL` alone: any work
26//! between the requested signal and the escalation is a window the child can be
27//! scheduled in, so delivering it first would make "no grace" a race rather
28//! than a guarantee. The exit code still reflects the signal that was asked
29//! for.
30
31/// Default signal used to stop a process when no explicit signal is given.
32///
33/// Mirrors the JavaScript `killSignal` default.
34pub const DEFAULT_KILL_SIGNAL: &str = "SIGTERM";
35
36/// Default grace period (in milliseconds) between the requested signal and the
37/// forceful `SIGKILL` escalation.
38///
39/// Mirrors the JavaScript `killGrace` default. It is what makes a graceful
40/// shutdown possible: without it the child is killed before its own handler
41/// gets to run.
42pub const DEFAULT_KILL_GRACE_MS: u64 = 100;
43
44/// Map a signal name to its numeric value.
45///
46/// Unknown names fall back to `SIGTERM`, matching the JavaScript
47/// implementation's behavior for unrecognized signals.
48///
49/// ```
50/// use command_stream::signal::signal_number;
51///
52/// assert_eq!(signal_number("SIGINT"), 2);
53/// assert_eq!(signal_number("SIGKILL"), 9);
54/// assert_eq!(signal_number("SIGTERM"), 15);
55/// ```
56pub fn signal_number(signal: &str) -> i32 {
57    match signal {
58        "SIGHUP" => 1,
59        "SIGINT" => 2,
60        "SIGQUIT" => 3,
61        "SIGKILL" => 9,
62        "SIGUSR1" => 10,
63        "SIGUSR2" => 12,
64        "SIGTERM" => 15,
65        _ => 15,
66    }
67}
68
69/// The exit code reported for a process stopped with `signal`, following the
70/// conventional `128 + signal` mapping used by POSIX shells.
71///
72/// ```
73/// use command_stream::signal::signal_exit_code;
74///
75/// assert_eq!(signal_exit_code("SIGINT"), 130); // CTRL+C
76/// assert_eq!(signal_exit_code("SIGTERM"), 143);
77/// assert_eq!(signal_exit_code("SIGKILL"), 137);
78/// ```
79pub fn signal_exit_code(signal: &str) -> i32 {
80    128 + signal_number(signal)
81}
82
83/// Who a signal is delivered to.
84///
85/// Only the runner that spawned the child knows which of these applies, so it
86/// is stated rather than discovered: a child spawned with `process_group(0)`
87/// leads its own group, and a child left in the caller's group does not.
88// Windows has neither signals nor process groups, so the distinction only ever
89// narrows to `ProcessAndGroup` there and the other variant is genuinely unused.
90#[cfg_attr(not(unix), allow(dead_code))]
91#[derive(Clone, Copy, PartialEq, Eq, Debug)]
92pub(crate) enum Delivery {
93    /// The process alone, for a child sharing the caller's process group.
94    ProcessOnly,
95    /// The process and the group it leads, which is what reaches grandchildren.
96    ProcessAndGroup,
97}
98
99/// Send a signal to a process and, when it leads one, its process group.
100///
101/// Delivery to the group (negative pid) is what reaches grandchildren, e.g. the
102/// real command behind a `sh -c` wrapper. It must not be attempted for a child
103/// left in the caller's group, where `-pid` would name a group we do not own -
104/// at best a non-existent one, at worst an unrelated group that reused the
105/// number.
106///
107/// Group leadership is passed in rather than looked up with `getpgid` because
108/// the leader is usually dead by the time the group is signalled: the first
109/// signal kills the `sh` wrapper, and the escalation follows a grace period
110/// later. Linux answers `getpgid` for a zombie, but macOS does not - its
111/// `proc_find` skips zombies, so the lookup failed with `ESRCH` and the group,
112/// including the still-running grandchild, was never signalled at all.
113///
114/// Both deliveries are best effort: the process may already have exited, which
115/// is not an error for a caller that only wants it stopped.
116#[cfg(unix)]
117pub(crate) fn send_signal_to_process(pid: u32, signal: &str, delivery: Delivery) {
118    use nix::sys::signal::{kill, Signal};
119    use nix::unistd::Pid;
120
121    let sig = match signal {
122        "SIGHUP" => Signal::SIGHUP,
123        "SIGINT" => Signal::SIGINT,
124        "SIGQUIT" => Signal::SIGQUIT,
125        "SIGKILL" => Signal::SIGKILL,
126        "SIGUSR1" => Signal::SIGUSR1,
127        "SIGUSR2" => Signal::SIGUSR2,
128        "SIGTERM" => Signal::SIGTERM,
129        _ => Signal::SIGTERM,
130    };
131
132    // Signal the whole process group (negative pid) first, so grandchildren are
133    // reached even if the group leader dies on the signal we send it next.
134    if delivery == Delivery::ProcessAndGroup {
135        let _ = kill(Pid::from_raw(-(pid as i32)), sig);
136    }
137    // Signal the process itself.
138    let _ = kill(Pid::from_raw(pid as i32), sig);
139}
140
141/// On non-Unix platforms there is no signal delivery, and no process groups to
142/// deliver to; the forceful `start_kill()` escalation in the caller handles
143/// termination.
144#[cfg(not(unix))]
145pub(crate) fn send_signal_to_process(_pid: u32, _signal: &str, _delivery: Delivery) {}