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