Skip to main content

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(all(unix, not(target_os = "macos")))]
91pub fn is_process_sender(code: c_int) -> bool {
92    return PROCESS_SENDER_CODES.contains(&code);
93}
94
95/// What:
96///  Whether a signal counts as sent by a process; on macOS every signal does.
97/// Why:
98///   The macOS kernel reports `si_code` 0 for `kill`, for a signal from a forked child and for a Ctrl-C written to a
99///  pseudo-terminal master (measured on Darwin arm64), so the code cannot tell a terminal sender from a process one.
100///  Relaying all of them is the safe side: a missed relay orphans the Git child,
101///  while a second `SIGINT`, `SIGTERM`, `SIGHUP` or `SIGQUIT` reaching Git is harmless.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// const fromProcess = (_siCode: number) => true;
106/// ```
107#[cfg(target_os = "macos")]
108pub fn is_process_sender(_code: c_int) -> bool {
109    return true;
110}
111
112/// Sender codes that mean a process sent the signal, on Linux.
113#[cfg(target_os = "linux")]
114const PROCESS_SENDER_CODES: [c_int; 3] = [libc::SI_USER, libc::SI_QUEUE, libc::SI_TKILL];
115
116/// Sender codes that mean a process sent the signal, on other Unix systems.
117#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
118const PROCESS_SENDER_CODES: [c_int; 2] = [libc::SI_USER, libc::SI_QUEUE];
119
120/// What:
121///  The signals the guard catches and relays.
122/// Why:
123///   The four signals that end an interactive or service wrapper in the incumbent's contract.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// const CAUGHT = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const;
128/// ```
129#[cfg(unix)]
130pub const RELAYED_SIGNALS: [c_int; 4] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT];
131
132/// What:
133///  PID of the Git child being waited for, or zero when none is.
134/// Why:
135///   The handler runs asynchronously, so it can only read state through atomics.
136#[cfg(unix)]
137static CHILD_PID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
138
139/// What:
140///  One slot per relayed signal, holding a signal number that has not been relayed yet, or zero.
141/// Why:
142///   A signal that arrives before the child exists is relayed as soon as its PID is recorded.
143#[cfg(unix)]
144static PENDING: [std::sync::atomic::AtomicI32; 4] =
145    [const { std::sync::atomic::AtomicI32::new(0) }; 4];
146
147/// What:
148///  Whether a guard is installed in this process.
149/// Why:
150///   Handlers and their saved dispositions are process-wide, so two guards would overwrite each other's record.
151#[cfg(unix)]
152static GUARD_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
153
154/// What:
155///  The first three fields of `siginfo_t` on every Unix system: signal number, error number and code.
156/// Why:
157///   The C struct's fields differ in name and union layout across systems; these three share one layout,
158///  and reading them through this view avoids the platform accessors.
159#[cfg(unix)]
160#[repr(C)]
161struct SignalHeader {
162    /// Signal number.
163    signal: c_int,
164    /// Error number, unused here.
165    error: c_int,
166    /// Sender code, `SI_USER` for `kill` and so on.
167    code: c_int,
168}
169
170/// What:
171///  The handler for each relayed signal.
172/// Why:
173///   Relaying inside the handler delivers the signal at once, even while the wrapper is blocked in a wait,
174///  where a flag would not wake it;
175///  `kill` is async-signal-safe, and the handler touches only atomics and `kill`.
176///
177/// In TS you'd write (pseudocode):
178/// ```ts
179/// process.on('SIGTERM', (info) => { if (info.fromProcess) child.kill('SIGTERM'); });
180/// ```
181#[cfg(unix)]
182extern "C" fn relay_handler(
183    signal: c_int,
184    info: *mut libc::siginfo_t,
185    _context: *mut std::ffi::c_void,
186) {
187    if info.is_null() {
188        return;
189    }
190    // SAFETY: with `SA_SIGINFO` the kernel passes a valid `siginfo_t`; its first three fields are the header.
191    let code: c_int = unsafe { (*info.cast::<SignalHeader>()).code };
192    if !is_process_sender(code) {
193        return;
194    }
195    let Some(slot) = RELAYED_SIGNALS.iter().position(|candidate| return *candidate == signal) else {
196        return;
197    };
198    PENDING[slot].store(signal, std::sync::atomic::Ordering::SeqCst);
199    relay_pending();
200}
201
202/// What:
203///  Send every pending signal to the recorded child, claiming each slot so it is sent once.
204/// Why:
205///   Both the handler and the waiting code call this; the atomic swap decides which one sends a given signal.
206///
207/// In TS you'd write (pseudocode):
208/// ```ts
209/// for (const signal of pending.drain()) child.kill(signal);
210/// ```
211#[cfg(unix)]
212fn relay_pending() {
213    let pid: i32 = CHILD_PID.load(std::sync::atomic::Ordering::SeqCst);
214    if pid <= 0 {
215        return;
216    }
217    for slot in &PENDING {
218        let signal: c_int = slot.swap(0, std::sync::atomic::Ordering::SeqCst);
219        if signal != 0 {
220            // SAFETY: `kill` sends one signal to a PID this module recorded; a failure is ignored because the child may have ended.
221            unsafe { libc::kill(pid, signal) };
222        }
223    }
224}
225
226/// What:
227///  Install one relaying handler, returning the disposition it replaced.
228///  A signal the parent ignores stays ignored, so `None` is returned and nothing is saved.
229/// Why:
230///   Git inherits ignored signals, so replacing an ignore with a handler would change Git's environment.
231///
232/// In TS you'd write (pseudocode):
233/// ```ts
234/// if (process.listenerCount(signal) === 0) install(signal);
235/// ```
236#[cfg(unix)]
237fn install_one(signal: c_int) -> io::Result<Option<libc::sigaction>> {
238    // SAFETY: a zeroed `sigaction` is a valid starting value; every field used below is set first.
239    let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
240    // SAFETY: as above; `previous` receives the current disposition.
241    let mut previous: libc::sigaction = unsafe { std::mem::zeroed() };
242    // SAFETY: a null new action only reads the current disposition into `previous`.
243    let read: c_int = unsafe { libc::sigaction(signal, std::ptr::null(), &mut previous) };
244    if read != 0 {
245        return Err(io::Error::last_os_error());
246    }
247    if previous.sa_sigaction == libc::SIG_IGN {
248        return Ok(None);
249    }
250    action.sa_sigaction = relay_handler as *const () as libc::sighandler_t;
251    action.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART;
252    // SAFETY: `sigemptyset` initializes the mask in place.
253    unsafe { libc::sigemptyset(&mut action.sa_mask) };
254    // SAFETY: `action` is fully initialized; the old disposition is written to `previous`.
255    let set: c_int = unsafe { libc::sigaction(signal, &action, &mut previous) };
256    if set != 0 {
257        return Err(io::Error::last_os_error());
258    }
259    return Ok(Some(previous));
260}
261
262/// What:
263///  Put back each saved disposition.
264/// Why:
265///   Restoring is the drop's job and also the rollback when an installation fails midway.
266///
267/// In TS you'd write (pseudocode):
268/// ```ts
269/// for (const [signal, previous] of saved) process.removeListener(signal, previous);
270/// ```
271#[cfg(unix)]
272fn restore(saved: &[(c_int, libc::sigaction)]) {
273    for (signal, previous) in saved {
274        // SAFETY: `previous` is a disposition this module read earlier; restoring it reads no memory.
275        unsafe { libc::sigaction(*signal, previous, std::ptr::null_mut()) };
276    }
277}
278
279/// What:
280///  Wait for the child to exit without reaping it, so its PID still names it.
281/// Why:
282///   Clearing the recorded PID before reaping stops a relay to a PID that the system may have reused.
283///  `waitid` with `WNOWAIT` reports the exit and leaves the child waitable.
284///
285/// In TS you'd write (pseudocode):
286/// ```ts
287/// await waitForExitWithoutReaping(pid);
288/// ```
289#[cfg(target_os = "linux")]
290fn wait_without_reaping(pid: libc::pid_t) -> io::Result<()> {
291    loop {
292        // SAFETY: `waitid` writes into the zeroed buffer this function owns, and only for a child it names.
293        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
294        // SAFETY: `P_PID` names one child; `WEXITED | WNOWAIT` only observes its exit.
295        let result: c_int = unsafe {
296            libc::waitid(
297                libc::P_PID,
298                libc::id_t::try_from(pid).unwrap_or(0),
299                &mut info,
300                libc::WEXITED | libc::WNOWAIT,
301            )
302        };
303        if result == 0 {
304            return Ok(());
305        }
306        let error: io::Error = io::Error::last_os_error();
307        match error.raw_os_error() {
308            Some(libc::EINTR) => continue,
309            Some(libc::ECHILD) => return Ok(()),
310            _ => return Err(error),
311        }
312    }
313}
314
315/// What:
316///  Non-Linux Unix placeholder for the wait that leaves the child unreaped.
317/// Why:
318///   `waitid` is not in the `libc` crate for these systems; the window it closes is documented on the module.
319#[cfg(all(unix, not(target_os = "linux")))]
320fn wait_without_reaping(_pid: libc::pid_t) -> io::Result<()> {
321    return Ok(());
322}
323
324/// What:
325///  Owns the relaying handlers for its lifetime.
326///  Install one guard, run the Git child through it, then drop it after cleanup.
327/// Why:
328///   Dropping it restores the previous dispositions, so a signal after cleanup behaves as it did before.
329///
330/// In TS you'd write (pseudocode):
331/// ```ts
332/// using guard = SignalGuard.install(); const outcome = await guard.runChild(command);
333/// ```
334#[cfg(unix)]
335#[must_use = "the handlers last only while the guard lives"]
336pub struct SignalGuard {
337    /// Dispositions to restore on drop, one per signal that was replaced.
338    saved: Vec<(c_int, libc::sigaction)>,
339}
340
341/// Methods over an installed guard.
342#[cfg(unix)]
343impl SignalGuard {
344    /// What:
345    ///  Install the relaying handlers.
346    ///  Fails when another guard is already active in this process.
347    /// Why:
348    ///   The handlers are process-wide, so a second guard would record the first one's handler as the old disposition.
349    ///
350    /// In TS you'd write (pseudocode):
351    /// ```ts
352    /// static install(): SignalGuard // throws when already installed
353    /// ```
354    pub fn install() -> io::Result<Self> {
355        if GUARD_ACTIVE
356            .compare_exchange(
357                false,
358                true,
359                std::sync::atomic::Ordering::SeqCst,
360                std::sync::atomic::Ordering::SeqCst,
361            )
362            .is_err()
363        {
364            return Err(io::Error::other(SignalGuardActive));
365        }
366        // `mut` lets each installed disposition be recorded as it is replaced.
367        let mut saved: Vec<(c_int, libc::sigaction)> = Vec::with_capacity(RELAYED_SIGNALS.len());
368        for signal in RELAYED_SIGNALS {
369            match install_one(signal) {
370                Ok(Some(previous)) => saved.push((signal, previous)),
371                Ok(None) => {}
372                Err(error) => {
373                    restore(&saved);
374                    GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
375                    return Err(error);
376                }
377            }
378        }
379        return Ok(Self { saved });
380    }
381
382    /// What:
383    ///  Start `command`, wait for it while relaying signals, and return how it ended.
384    ///  The guard does not exit the process; the caller releases its locks and removes its state, then exits.
385    /// Why:
386    ///   Signals that arrived before the child started are relayed as soon as it has a PID.
387    ///
388    /// In TS you'd write (pseudocode):
389    /// ```ts
390    /// async runChild(command: SpawnOptions): Promise<ChildOutcome>
391    /// ```
392    pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
393        return run_relayed(command);
394    }
395}
396
397/// Spawns the child, then relays any signal that was caught before the child existed.
398///
399/// A handler that runs before the spawn keeps its signal in `PENDING` and finds no child.
400/// The PID is published before the drain, so that signal is relayed here, not lost.
401#[cfg(unix)]
402fn run_relayed(command: &mut Command) -> io::Result<ChildOutcome> {
403    let mut child: std::process::Child = command.spawn_retrying()?;
404    let pid: libc::pid_t = libc::pid_t::try_from(child.id())
405        .map_err(|error| return io::Error::other(error))?;
406    CHILD_PID.store(pid, std::sync::atomic::Ordering::SeqCst);
407    relay_pending();
408    let waited: io::Result<()> = wait_without_reaping(pid);
409    // Linux has waited for the child's end without reaping it, so the PID cannot be recycled yet and is cleared first.
410    #[cfg(target_os = "linux")]
411    CHILD_PID.store(0, std::sync::atomic::Ordering::SeqCst);
412    let reaped: io::Result<ExitStatus> = child.wait();
413    // Elsewhere `wait_without_reaping` returns at once, so the PID must stay published through the blocking wait
414    //  or a signal arriving during it would find no child; the recycled-PID window is the limit named on the module.
415    #[cfg(not(target_os = "linux"))]
416    CHILD_PID.store(0, std::sync::atomic::Ordering::SeqCst);
417    let status: ExitStatus = reaped?;
418    waited?;
419    return Ok(super::forwarding::outcome_of(status));
420}
421
422/// Restore the previous dispositions and release the process-wide slot.
423#[cfg(unix)]
424impl Drop for SignalGuard {
425    /// What:
426    ///  Restore the saved dispositions and clear the active flag.
427    /// Why:
428    ///   The next guard, or the process default after the guard, must see the original handlers.
429    ///
430    /// In TS you'd write (pseudocode):
431    /// ```ts
432    /// [Symbol.dispose]() { restore(saved); }
433    /// ```
434    fn drop(&mut self) {
435        restore(&self.saved);
436        GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
437    }
438}
439
440/// What:
441///  Non-Unix guard: no signal relay exists, so the child runs and the wrapper waits.
442/// Why:
443///   Callers keep one API; Windows ends a console child differently and is not covered by these controls.
444// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
445#[cfg(not(unix))]
446pub struct SignalGuard;
447
448/// Methods over the non-Unix guard.
449// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
450#[cfg(not(unix))]
451impl SignalGuard {
452    /// What:
453    ///  Return a guard that relays nothing.
454    /// Why:
455    ///   Keeps the call site identical on every platform.
456    ///
457    /// In TS you'd write (pseudocode):
458    /// ```ts
459    /// static install(): SignalGuard
460    /// ```
461    pub fn install() -> io::Result<Self> {
462        return Ok(Self);
463    }
464
465    /// What:
466    ///  Start `command` and wait for it.
467    /// Why:
468    ///   Without relaying, the wait is the whole job.
469    ///
470    /// In TS you'd write (pseudocode):
471    /// ```ts
472    /// async runChild(command): Promise<ChildOutcome>
473    /// ```
474    pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
475        let mut child: std::process::Child = command.spawn_retrying()?;
476        let status: ExitStatus = child.wait()?;
477        return Ok(super::forwarding::outcome_of(status));
478    }
479}
480
481/// Signal controls live beside the guard.
482#[cfg(test)]
483#[path = "signal_relay_tests.rs"]
484mod tests;