#[derive(Debug)]
pub struct SignalGuardActive;
impl std::fmt::Display for SignalGuardActive {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("a signal guard is already active in this process");
}
}
impl std::error::Error for SignalGuardActive {}
pub fn is_guard_active_refusal(error: &io::Error) -> bool {
return error
.get_ref()
.is_some_and(|inner| { return inner.is::<SignalGuardActive>(); });
}
#[cfg(unix)]
use std::ffi::c_int;
use std::io;
use crate::start_retry::RetryStart;
use std::process::{Command, ExitStatus};
use crate::forwarding::ChildOutcome;
#[cfg(all(unix, not(target_os = "macos")))]
pub fn is_process_sender(code: c_int) -> bool {
return PROCESS_SENDER_CODES.contains(&code);
}
#[cfg(target_os = "macos")]
pub fn is_process_sender(_code: c_int) -> bool {
return true;
}
#[cfg(target_os = "linux")]
const PROCESS_SENDER_CODES: [c_int; 3] = [libc::SI_USER, libc::SI_QUEUE, libc::SI_TKILL];
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
const PROCESS_SENDER_CODES: [c_int; 2] = [libc::SI_USER, libc::SI_QUEUE];
#[cfg(unix)]
pub const RELAYED_SIGNALS: [c_int; 4] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT];
#[cfg(unix)]
static CHILD_PID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
#[cfg(unix)]
static PENDING: [std::sync::atomic::AtomicI32; 4] =
[const { std::sync::atomic::AtomicI32::new(0) }; 4];
#[cfg(unix)]
static GUARD_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(unix)]
#[repr(C)]
struct SignalHeader {
signal: c_int,
error: c_int,
code: c_int,
}
#[cfg(unix)]
extern "C" fn relay_handler(
signal: c_int,
info: *mut libc::siginfo_t,
_context: *mut std::ffi::c_void,
) {
if info.is_null() {
return;
}
let code: c_int = unsafe { (*info.cast::<SignalHeader>()).code };
if !is_process_sender(code) {
return;
}
let Some(slot) = RELAYED_SIGNALS.iter().position(|candidate| return *candidate == signal) else {
return;
};
PENDING[slot].store(signal, std::sync::atomic::Ordering::SeqCst);
relay_pending();
}
#[cfg(unix)]
fn relay_pending() {
let pid: i32 = CHILD_PID.load(std::sync::atomic::Ordering::SeqCst);
if pid <= 0 {
return;
}
for slot in &PENDING {
let signal: c_int = slot.swap(0, std::sync::atomic::Ordering::SeqCst);
if signal != 0 {
unsafe { libc::kill(pid, signal) };
}
}
}
#[cfg(unix)]
fn install_one(signal: c_int) -> io::Result<Option<libc::sigaction>> {
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
let mut previous: libc::sigaction = unsafe { std::mem::zeroed() };
let read: c_int = unsafe { libc::sigaction(signal, std::ptr::null(), &mut previous) };
if read != 0 {
return Err(io::Error::last_os_error());
}
if previous.sa_sigaction == libc::SIG_IGN {
return Ok(None);
}
action.sa_sigaction = relay_handler as *const () as libc::sighandler_t;
action.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART;
unsafe { libc::sigemptyset(&mut action.sa_mask) };
let set: c_int = unsafe { libc::sigaction(signal, &action, &mut previous) };
if set != 0 {
return Err(io::Error::last_os_error());
}
return Ok(Some(previous));
}
#[cfg(unix)]
fn restore(saved: &[(c_int, libc::sigaction)]) {
for (signal, previous) in saved {
unsafe { libc::sigaction(*signal, previous, std::ptr::null_mut()) };
}
}
#[cfg(target_os = "linux")]
fn wait_without_reaping(pid: libc::pid_t) -> io::Result<()> {
loop {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let result: c_int = unsafe {
libc::waitid(
libc::P_PID,
libc::id_t::try_from(pid).unwrap_or(0),
&mut info,
libc::WEXITED | libc::WNOWAIT,
)
};
if result == 0 {
return Ok(());
}
let error: io::Error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::EINTR) => continue,
Some(libc::ECHILD) => return Ok(()),
_ => return Err(error),
}
}
}
#[cfg(all(unix, not(target_os = "linux")))]
fn wait_without_reaping(_pid: libc::pid_t) -> io::Result<()> {
return Ok(());
}
#[cfg(unix)]
#[must_use = "the handlers last only while the guard lives"]
pub struct SignalGuard {
saved: Vec<(c_int, libc::sigaction)>,
}
#[cfg(unix)]
impl SignalGuard {
pub fn install() -> io::Result<Self> {
if GUARD_ACTIVE
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_err()
{
return Err(io::Error::other(SignalGuardActive));
}
let mut saved: Vec<(c_int, libc::sigaction)> = Vec::with_capacity(RELAYED_SIGNALS.len());
for signal in RELAYED_SIGNALS {
match install_one(signal) {
Ok(Some(previous)) => saved.push((signal, previous)),
Ok(None) => {}
Err(error) => {
restore(&saved);
GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
return Err(error);
}
}
}
return Ok(Self { saved });
}
pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
return run_relayed(command);
}
}
#[cfg(unix)]
fn run_relayed(command: &mut Command) -> io::Result<ChildOutcome> {
let mut child: std::process::Child = command.spawn_retrying()?;
let pid: libc::pid_t = libc::pid_t::try_from(child.id())
.map_err(|error| return io::Error::other(error))?;
CHILD_PID.store(pid, std::sync::atomic::Ordering::SeqCst);
relay_pending();
let waited: io::Result<()> = wait_without_reaping(pid);
#[cfg(target_os = "linux")]
CHILD_PID.store(0, std::sync::atomic::Ordering::SeqCst);
let reaped: io::Result<ExitStatus> = child.wait();
#[cfg(not(target_os = "linux"))]
CHILD_PID.store(0, std::sync::atomic::Ordering::SeqCst);
let status: ExitStatus = reaped?;
waited?;
return Ok(crate::forwarding::outcome_of(status));
}
#[cfg(unix)]
impl Drop for SignalGuard {
fn drop(&mut self) {
restore(&self.saved);
GUARD_ACTIVE.store(false, std::sync::atomic::Ordering::SeqCst);
}
}
#[cfg(not(unix))]
pub struct SignalGuard;
#[cfg(not(unix))]
impl SignalGuard {
pub fn install() -> io::Result<Self> {
return Ok(Self);
}
pub fn run_child(&self, command: &mut Command) -> io::Result<ChildOutcome> {
let mut child: std::process::Child = command.spawn_retrying()?;
let status: ExitStatus = child.wait()?;
return Ok(crate::forwarding::outcome_of(status));
}
}
#[cfg(test)]
#[path = "signal_relay_tests.rs"]
mod tests;