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