indexbus_platform_ops/
process.rs

1//! Process-related operational helpers.
2//!
3//! **Platform support**
4//! - Unix: uses `kill(2)`.
5//! - Non-Unix: signaling APIs are best-effort no-ops.
6//!
7//! **Contracts**
8//! - [`crate::process::pid_is_alive`] is a best-effort probe and can race with process exit.
9//! - On Unix, `EPERM` is treated as "alive" for `pid_is_alive`.
10
11use crate::errors::{Error, Result};
12
13#[cfg(unix)]
14use std::sync::Arc;
15
16#[cfg(unix)]
17use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20/// A process signal that can be sent via [`send_signal`].
21pub enum Signal {
22    /// SIGTERM (request graceful shutdown).
23    Term,
24    /// SIGKILL (force termination).
25    Kill,
26}
27
28#[cfg(unix)]
29static SIGINT_STOP_FLAG: AtomicPtr<AtomicBool> = AtomicPtr::new(std::ptr::null_mut());
30
31#[cfg(unix)]
32extern "C" fn sigint_set_stop_flag(_signum: libc::c_int) {
33    // Keep the handler async-signal-safe: only do a raw atomic store.
34    let ptr = SIGINT_STOP_FLAG.load(Ordering::Relaxed);
35    if ptr.is_null() {
36        return;
37    }
38    unsafe {
39        (*ptr).store(true, Ordering::Relaxed);
40    }
41}
42
43/// Install a best-effort SIGINT (Ctrl-C) handler that sets `stop` to `true`.
44///
45/// This is intended for cooperative shutdown wiring in long-running processes.
46///
47/// Notes:
48/// - Unix only: on non-Unix targets this is a no-op that returns `Ok(())`.
49/// - This keeps the signal handler async-signal-safe by doing only an atomic store.
50/// - The stop flag is intentionally leaked to avoid use-after-free if SIGINT fires late.
51/// - This installs a single *process-global* stop flag. Calling it multiple times with
52///   different `Arc`s will leak one allocation per call; install once during startup.
53pub fn install_sigint_stop_flag(stop: Arc<AtomicBool>) -> Result<()> {
54    #[cfg(unix)]
55    {
56        let new_ptr = Arc::as_ptr(&stop) as *mut AtomicBool;
57
58        // If we're already installed for this exact flag, do nothing.
59        if SIGINT_STOP_FLAG.load(Ordering::SeqCst) == new_ptr {
60            return Ok(());
61        }
62
63        let handler = sigint_set_stop_flag as *const () as usize as libc::sighandler_t;
64        let prev = unsafe { libc::signal(libc::SIGINT, handler) };
65        if prev == libc::SIG_ERR {
66            return Err(Error::msg(format!(
67                "signal(SIGINT) failed: {} (errno={:?})",
68                std::io::Error::last_os_error(),
69                std::io::Error::last_os_error().raw_os_error()
70            )));
71        }
72
73        // Leak the stop flag to ensure the handler never writes to freed memory.
74        let _ = Arc::into_raw(stop);
75        SIGINT_STOP_FLAG.store(new_ptr, Ordering::SeqCst);
76        Ok(())
77    }
78
79    #[cfg(not(unix))]
80    {
81        let _ = stop;
82        Ok(())
83    }
84}
85
86#[cfg(unix)]
87fn kill_raw(pid: i32, sig: i32) -> std::io::Result<()> {
88    let rc = unsafe { libc::kill(pid, sig) };
89    if rc == 0 {
90        Ok(())
91    } else {
92        Err(std::io::Error::last_os_error())
93    }
94}
95
96#[cfg(unix)]
97fn pid_to_i32(pid: u32) -> Result<i32> {
98    i32::try_from(pid).map_err(|_| Error::msg(format!("pid {pid} out of range for i32")))
99}
100
101/// Best-effort check whether a PID exists.
102///
103/// On Unix this uses `kill(pid, 0)` and treats `EPERM` as alive.
104pub fn pid_is_alive(pid: u32) -> bool {
105    #[cfg(unix)]
106    {
107        let Ok(pid) = i32::try_from(pid) else {
108            return false;
109        };
110
111        match kill_raw(pid, 0) {
112            Ok(()) => true,
113            Err(e) => {
114                // If we don't have permission to signal it, it still exists.
115                matches!(e.raw_os_error(), Some(libc::EPERM))
116            }
117        }
118    }
119
120    #[cfg(not(unix))]
121    {
122        let _ = pid;
123        false
124    }
125}
126
127/// Send a Unix signal to the given PID.
128///
129/// On non-Unix targets this is a no-op that returns `Ok(())`.
130///
131/// # Errors
132/// On Unix, returns an error if the PID is out of range or if `kill(2)` fails.
133pub fn send_signal(pid: u32, signal: Signal) -> Result<()> {
134    #[cfg(unix)]
135    {
136        let pid_i32 = pid_to_i32(pid)?;
137        let sig = match signal {
138            Signal::Term => libc::SIGTERM,
139            Signal::Kill => libc::SIGKILL,
140        };
141
142        kill_raw(pid_i32, sig).map_err(|e| {
143            Error::msg(format!(
144                "kill(pid={pid}, sig={sig}) failed: {e} (errno={:?})",
145                e.raw_os_error()
146            ))
147        })
148    }
149
150    #[cfg(not(unix))]
151    {
152        let _ = (pid, signal);
153        Ok(())
154    }
155}
156
157/// Convenience wrapper for `SIGTERM`.
158pub fn signal_term(pid: u32) -> Result<()> {
159    send_signal(pid, Signal::Term)
160}
161
162/// Convenience wrapper for `SIGKILL`.
163pub fn signal_kill(pid: u32) -> Result<()> {
164    send_signal(pid, Signal::Kill)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn pid_is_alive_is_false_for_out_of_range_pid() {
173        assert!(!pid_is_alive(u32::MAX));
174    }
175
176    #[test]
177    fn send_signal_rejects_pid_out_of_i32_range() {
178        // This should fail during pid conversion, without sending a signal.
179        let err = send_signal(u32::MAX, Signal::Term).unwrap_err();
180        assert!(err.to_string().contains("out of range"));
181    }
182}