git_policy_cli/signal_relay.rs
1//! What:
2//! Catching `SIGINT`, `SIGTERM`, `SIGHUP` and `SIGQUIT` while the wrapper waits for a Git child,
3//! relaying a signal to that child only when a process sent it,
4//! and reporting how the child ended.
5//! `SignalGuard` owns the handlers for its lifetime and restores the previous dispositions on drop.
6//! Why:
7//! A wrapper that dies on a signal leaves its commit transaction for recovery and Git running;
8//! one that catches the signal can end Git, release its locks and remove the transaction first.
9//! A signal from the terminal already reached Git as part of the foreground process group,
10//! so relaying it again would deliver it twice.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! process.once('SIGTERM', () => { if (!fromTerminal) child.kill('SIGTERM'); });
15//! ```
16//!
17//! Integration (for the commit path and wrapped commands):
18//! ```ts
19//! // const guard = SignalGuard.install(); const outcome = guard.runChild(command); // cleanup; then exit(exitCode(outcome))
20//! ```
21//!
22//! Limits:
23//! Handlers are process-wide, so one guard may be active at a time;
24//! `SA_RESTART` keeps other system calls from failing with `EINTR`.
25//! Only Linux reads the child's exit without reaping it, so the PID is cleared before reaping there;
26//! elsewhere a signal that arrives in the instant between the child's exit and its reaping
27//! could reach a recycled PID.
28
29/// What:
30/// The typed reason a second signal guard is refused while one is active.
31/// Why:
32/// Callers that wait for the guard to be free must recognize this refusal by its type, not by its wording
33/// (issue 1346).
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// class SignalGuardActiveError extends Error {}
38/// ```
39#[derive(Debug)]
40pub struct SignalGuardActive;
41
42/// The wording of the refusal.
43impl std::fmt::Display for SignalGuardActive {
44 /// Writes the wording of the refusal.
45 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 return formatter.write_str("a signal guard is already active in this process");
47 }
48}
49
50impl std::error::Error for SignalGuardActive {}
51
52/// What:
53/// Whether an installation failure is the refusal of a second guard.
54/// Why:
55/// Only this refusal clears by itself when the other holder lets go; every other failure is final.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// error instanceof SignalGuardActiveError
60/// ```
61pub fn is_guard_active_refusal(error: &io::Error) -> bool {
62 return error
63 .get_ref()
64 .is_some_and(|inner| { return inner.is::<SignalGuardActive>(); });
65}
66
67/// `c_int` is the C integer type signal numbers and status codes use.
68#[cfg(unix)]
69use std::ffi::c_int;
70/// `io` is the input and output error type the spawn and wait calls return.
71use std::io;
72/// Starts that ride out a short shortage of processes.
73use super::start_retry::RetryStart;
74/// `Command` builds the child, and `ExitStatus` is how it ended.
75use std::process::{Command, ExitStatus};
76
77/// The Git child's end, with the exit-code mapping the wrapper's exit uses.
78use super::forwarding::ChildOutcome;
79
80/// What:
81/// Whether the kernel recorded a signal as sent by a process rather than by the terminal or the kernel.
82/// Why:
83/// `si_code` values `SI_USER`, `SI_QUEUE` and (Linux) `SI_TKILL` come from `kill`, `sigqueue` and `tkill`;
84/// a terminal interrupt carries `SI_KERNEL`.
85///
86/// In TS you'd write (pseudocode):
87/// ```ts
88/// const fromProcess = info.si_code === SI_USER || info.si_code === SI_QUEUE;
89/// ```
90#[cfg(unix)]
91pub fn is_process_sender(code: c_int) -> bool {
92 return PROCESS_SENDER_CODES.contains(&code);
93}
94
95/// Sender codes that mean a process sent the signal, on Linux.
96#[cfg(target_os = "linux")]
97const PROCESS_SENDER_CODES: [c_int; 3] = [libc::SI_USER, libc::SI_QUEUE, libc::SI_TKILL];
98
99/// Sender codes that mean a process sent the signal, on other Unix systems.
100#[cfg(all(unix, not(target_os = "linux")))]
101const PROCESS_SENDER_CODES: [c_int; 2] = [libc::SI_USER, libc::SI_QUEUE];
102
103/// What:
104/// The signals the guard catches and relays.
105/// Why:
106/// The four signals that end an interactive or service wrapper in the incumbent's contract.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// const CAUGHT = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const;
111/// ```
112#[cfg(unix)]
113pub const RELAYED_SIGNALS: [c_int; 4] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT];
114
115/// What:
116/// PID of the Git child being waited for, or zero when none is.
117/// Why:
118/// The handler runs asynchronously, so it can only read state through atomics.
119#[cfg(unix)]
120static CHILD_PID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
121
122/// What:
123/// One slot per relayed signal, holding a signal number that has not been relayed yet, or zero.
124/// Why:
125/// A signal that arrives before the child exists is relayed as soon as its PID is recorded.
126#[cfg(unix)]
127static PENDING: [std::sync::atomic::AtomicI32; 4] =
128 [const { std::sync::atomic::AtomicI32::new(0) }; 4];
129
130/// What:
131/// Whether a guard is installed in this process.
132/// Why:
133/// Handlers and their saved dispositions are process-wide, so two guards would overwrite each other's record.
134#[cfg(unix)]
135static GUARD_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
136
137/// What:
138/// The first three fields of `siginfo_t` on every Unix system: signal number, error number and code.
139/// Why:
140/// The C struct's fields differ in name and union layout across systems; these three share one layout,
141/// and reading them through this view avoids the platform accessors.
142#[cfg(unix)]
143#[repr(C)]
144struct SignalHeader {
145 /// Signal number.
146 signal: c_int,
147 /// Error number, unused here.
148 error: c_int,
149 /// Sender code, `SI_USER` for `kill` and so on.
150 code: c_int,
151}
152
153/// What:
154/// The handler for each relayed signal.
155/// Why:
156/// Relaying inside the handler delivers the signal at once, even while the wrapper is blocked in a wait,
157/// where a flag would not wake it;
158/// `kill` is async-signal-safe, and the handler touches only atomics and `kill`.
159///
160/// In TS you'd write (pseudocode):
161/// ```ts
162/// process.on('SIGTERM', (info) => { if (info.fromProcess) child.kill('SIGTERM'); });
163/// ```
164#[cfg(unix)]
165extern "C" fn relay_handler(
166 signal: c_int,
167 info: *mut libc::siginfo_t,
168 _context: *mut std::ffi::c_void,
169) {
170 if info.is_null() {
171 return;
172 }
173 // SAFETY: with `SA_SIGINFO` the kernel passes a valid `siginfo_t`; its first three fields are the header.
174 let code: c_int = unsafe { (*info.cast::<SignalHeader>()).code };
175 if !is_process_sender(code) {
176 return;
177 }
178 let Some(slot) = RELAYED_SIGNALS.iter().position(|candidate| return *candidate == signal) else {
179 return;
180 };
181 PENDING[slot].store(signal, std::sync::atomic::Ordering::SeqCst);
182 relay_pending();
183}
184
185/// What:
186/// Send every pending signal to the recorded child, claiming each slot so it is sent once.
187/// Why:
188/// Both the handler and the waiting code call this; the atomic swap decides which one sends a given signal.
189///
190/// In TS you'd write (pseudocode):
191/// ```ts
192/// for (const signal of pending.drain()) child.kill(signal);
193/// ```
194#[cfg(unix)]
195fn relay_pending() {
196 let pid: i32 = CHILD_PID.load(std::sync::atomic::Ordering::SeqCst);
197 if pid <= 0 {
198 return;
199 }
200 for slot in &PENDING {
201 let signal: c_int = slot.swap(0, std::sync::atomic::Ordering::SeqCst);
202 if signal != 0 {
203 // SAFETY: `kill` sends one signal to a PID this module recorded; a failure is ignored because the child may have ended.
204 unsafe { libc::kill(pid, signal) };
205 }
206 }
207}
208
209/// What:
210/// Install one relaying handler, returning the disposition it replaced.
211/// A signal the parent ignores stays ignored, so `None` is returned and nothing is saved.
212/// Why:
213/// Git inherits ignored signals, so replacing an ignore with a handler would change Git's environment.
214///
215/// In TS you'd write (pseudocode):
216/// ```ts
217/// if (process.listenerCount(signal) === 0) install(signal);
218/// ```
219#[cfg(unix)]
220fn install_one(signal: c_int) -> io::Result<Option<libc::sigaction>> {
221 // SAFETY: a zeroed `sigaction` is a valid starting value; every field used below is set first.
222 let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
223 // SAFETY: as above; `previous` receives the current disposition.
224 let mut previous: libc::sigaction = unsafe { std::mem::zeroed() };
225 // SAFETY: a null new action only reads the current disposition into `previous`.
226 let read: c_int = unsafe { libc::sigaction(signal, std::ptr::null(), &mut previous) };
227 if read != 0 {
228 return Err(io::Error::last_os_error());
229 }
230 if previous.sa_sigaction == libc::SIG_IGN {
231 return Ok(None);
232 }
233 action.sa_sigaction = relay_handler as *const () as libc::sighandler_t;
234 action.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART;
235 // SAFETY: `sigemptyset` initializes the mask in place.
236 unsafe { libc::sigemptyset(&mut action.sa_mask) };
237 // SAFETY: `action` is fully initialized; the old disposition is written to `previous`.
238 let set: c_int = unsafe { libc::sigaction(signal, &action, &mut previous) };
239 if set != 0 {
240 return Err(io::Error::last_os_error());
241 }
242 return Ok(Some(previous));
243}
244
245/// What:
246/// Put back each saved disposition.
247/// Why:
248/// Restoring is the drop's job and also the rollback when an installation fails midway.
249///
250/// In TS you'd write (pseudocode):
251/// ```ts
252/// for (const [signal, previous] of saved) process.removeListener(signal, previous);
253/// ```
254#[cfg(unix)]
255fn restore(saved: &[(c_int, libc::sigaction)]) {
256 for (signal, previous) in saved {
257 // SAFETY: `previous` is a disposition this module read earlier; restoring it reads no memory.
258 unsafe { libc::sigaction(*signal, previous, std::ptr::null_mut()) };
259 }
260}
261
262/// What:
263/// Wait for the child to exit without reaping it, so its PID still names it.
264/// Why:
265/// Clearing the recorded PID before reaping stops a relay to a PID that the system may have reused.
266/// `waitid` with `WNOWAIT` reports the exit and leaves the child waitable.
267///
268/// In TS you'd write (pseudocode):
269/// ```ts
270/// await waitForExitWithoutReaping(pid);
271/// ```
272#[cfg(target_os = "linux")]
273fn wait_without_reaping(pid: libc::pid_t) -> io::Result<()> {
274 loop {
275 // SAFETY: `waitid` writes into the zeroed buffer this function owns, and only for a child it names.
276 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
277 // SAFETY: `P_PID` names one child; `WEXITED | WNOWAIT` only observes its exit.
278 let result: c_int = unsafe {
279 libc::waitid(
280 libc::P_PID,
281 libc::id_t::try_from(pid).unwrap_or(0),
282 &mut info,
283 libc::WEXITED | libc::WNOWAIT,
284 )
285 };
286 if result == 0 {
287 return Ok(());
288 }
289 let error: io::Error = io::Error::last_os_error();
290 match error.raw_os_error() {
291 Some(libc::EINTR) => continue,
292 Some(libc::ECHILD) => return Ok(()),
293 _ => return Err(error),
294 }
295 }
296}
297
298/// What:
299/// Non-Linux Unix placeholder for the wait that leaves the child unreaped.
300/// Why:
301/// `waitid` is not in the `libc` crate for these systems; the window it closes is documented on the module.
302#[cfg(all(unix, not(target_os = "linux")))]
303fn wait_without_reaping(_pid: libc::pid_t) -> io::Result<()> {
304 return Ok(());
305}
306
307/// What:
308/// Owns the relaying handlers for its lifetime.
309/// Install one guard, run the Git child through it, then drop it after cleanup.
310/// Why:
311/// Dropping it restores the previous dispositions, so a signal after cleanup behaves as it did before.
312///
313/// In TS you'd write (pseudocode):
314/// ```ts
315/// using guard = SignalGuard.install(); const outcome = await guard.runChild(command);
316/// ```
317#[cfg(unix)]
318#[must_use = "the handlers last only while the guard lives"]
319pub struct SignalGuard {
320 /// Dispositions to restore on drop, one per signal that was replaced.
321 saved: Vec<(c_int, libc::sigaction)>,
322}
323
324/// Methods over an installed guard.
325#[cfg(unix)]
326impl SignalGuard {
327 /// What:
328 /// Install the relaying handlers.
329 /// Fails when another guard is already active in this process.
330 /// Why:
331 /// The handlers are process-wide, so a second guard would record the first one's handler as the old disposition.
332 ///
333 /// In TS you'd write (pseudocode):
334 /// ```ts
335 /// static install(): SignalGuard // throws when already installed
336 /// ```
337 pub fn install() -> io::Result<Self> {
338 if GUARD_ACTIVE
339 .compare_exchange(
340 false,
341 true,
342 std::sync::atomic::Ordering::SeqCst,
343 std::sync::atomic::Ordering::SeqCst,
344 )
345 .is_err()
346 {
347 return Err(io::Error::other(SignalGuardActive));
348 }
349 // `mut` lets each installed disposition be recorded as it is replaced.
350 let mut saved: Vec<(c_int, libc::sigaction)> = Vec::with_capacity(RELAYED_SIGNALS.len());
351 for signal in RELAYED_SIGNALS {
352 match install_one(signal) {
353 Ok(Some(previous)) => saved.push((signal, previous)),
354 Ok(None) => {}
355 Err(error) => {
356 restore(&saved);
357 GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
358 return Err(error);
359 }
360 }
361 }
362 return Ok(Self { saved });
363 }
364
365 /// What:
366 /// Start `command`, wait for it while relaying signals, and return how it ended.
367 /// The guard does not exit the process; the caller releases its locks and removes its state, then exits.
368 /// Why:
369 /// Signals that arrived before the child started are relayed as soon as it has a PID.
370 ///
371 /// In TS you'd write (pseudocode):
372 /// ```ts
373 /// async runChild(command: SpawnOptions): Promise<ChildOutcome>
374 /// ```
375 pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
376 return run_relayed(command);
377 }
378}
379
380/// Spawns the child, then relays any signal that was caught before the child existed.
381///
382/// A handler that runs before the spawn keeps its signal in `PENDING` and finds no child.
383/// The PID is published before the drain, so that signal is relayed here, not lost.
384#[cfg(unix)]
385fn run_relayed(command: &mut Command) -> io::Result<ChildOutcome> {
386 let mut child: std::process::Child = command.spawn_retrying()?;
387 let pid: libc::pid_t = libc::pid_t::try_from(child.id())
388 .map_err(|error| return io::Error::other(error))?;
389 CHILD_PID.store(pid, std::sync::atomic::Ordering::SeqCst);
390 relay_pending();
391 let waited: io::Result<()> = wait_without_reaping(pid);
392 CHILD_PID.store(0, std::sync::atomic::Ordering::SeqCst);
393 let status: ExitStatus = child.wait()?;
394 waited?;
395 return Ok(super::forwarding::outcome_of(status));
396}
397
398/// Restore the previous dispositions and release the process-wide slot.
399#[cfg(unix)]
400impl Drop for SignalGuard {
401 /// What:
402 /// Restore the saved dispositions and clear the active flag.
403 /// Why:
404 /// The next guard, or the process default after the guard, must see the original handlers.
405 ///
406 /// In TS you'd write (pseudocode):
407 /// ```ts
408 /// [Symbol.dispose]() { restore(saved); }
409 /// ```
410 fn drop(&mut self) {
411 restore(&self.saved);
412 GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
413 }
414}
415
416/// What:
417/// Non-Unix guard: no signal relay exists, so the child runs and the wrapper waits.
418/// Why:
419/// Callers keep one API; Windows ends a console child differently and is not covered by these controls.
420// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
421#[cfg(not(unix))]
422pub struct SignalGuard;
423
424/// Methods over the non-Unix guard.
425// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
426#[cfg(not(unix))]
427impl SignalGuard {
428 /// What:
429 /// Return a guard that relays nothing.
430 /// Why:
431 /// Keeps the call site identical on every platform.
432 ///
433 /// In TS you'd write (pseudocode):
434 /// ```ts
435 /// static install(): SignalGuard
436 /// ```
437 pub fn install() -> io::Result<Self> {
438 return Ok(Self);
439 }
440
441 /// What:
442 /// Start `command` and wait for it.
443 /// Why:
444 /// Without relaying, the wait is the whole job.
445 ///
446 /// In TS you'd write (pseudocode):
447 /// ```ts
448 /// async runChild(command): Promise<ChildOutcome>
449 /// ```
450 pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
451 let mut child: std::process::Child = command.spawn_retrying()?;
452 let status: ExitStatus = child.wait()?;
453 return Ok(super::forwarding::outcome_of(status));
454 }
455}
456
457/// Signal controls live beside the guard.
458#[cfg(test)]
459#[path = "signal_relay_tests.rs"]
460mod tests;