mothership 0.0.100

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
//! Parent death signal for BSD/macOS using kqueue
//!
//! Linux has `prctl(PR_SET_PDEATHSIG)` which automatically sends a signal
//! to a child when its parent dies. macOS and FreeBSD lack this syscall.
//!
//! This module provides equivalent functionality using kqueue's EVFILT_PROC
//! with NOTE_EXIT to monitor the parent process and send a signal when it exits.
//!
//! # Usage
//!
//! Call `setup_parent_death_signal()` early in a child process:
//!
//! ```ignore
//! // In child process after exec
//! let parent_pid = std::env::var("MS_PID").ok()
//!     .and_then(|s| s.parse().ok())
//!     .unwrap_or_else(|| unsafe { libc::getppid() });
//! setup_parent_death_signal(parent_pid, libc::SIGTERM);
//! ```

/// Set up parent death notification.
///
/// On Linux: Uses prctl(PR_SET_PDEATHSIG) - must be called after fork, before exec
/// On macOS/FreeBSD: Spawns a watcher thread using kqueue - can be called anytime
///
/// # Arguments
/// * `parent_pid` - PID of the parent process to monitor
/// * `signal` - Signal to send when parent exits (e.g., libc::SIGTERM)
///
/// # Returns
/// Ok(()) on success, Err on failure to set up monitoring
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
pub fn setup_parent_death_signal(parent_pid: i32, signal: i32) -> std::io::Result<()> {
    use std::thread;

    // Check if parent is still alive before setting up watcher
    // kill(pid, 0) returns 0 if process exists and we can signal it
    // returns -1 with EPERM if process exists but we can't signal it
    // returns -1 with ESRCH if process doesn't exist
    if unsafe { libc::kill(parent_pid, 0) } != 0 {
        let err = std::io::Error::last_os_error();
        if err.raw_os_error() == Some(libc::ESRCH) {
            return Err(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "parent process not found",
            ));
        }
        // EPERM means process exists but we can't signal it - that's fine for watching
    }

    thread::Builder::new()
        .name("parent-death-watcher".into())
        .spawn(move || {
            watch_parent_kqueue(parent_pid, signal);
        })?;

    Ok(())
}

/// Watch parent process using kqueue and send signal when it exits
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
fn watch_parent_kqueue(parent_pid: i32, signal: i32) {
    unsafe {
        let kq = libc::kqueue();
        if kq < 0 {
            return;
        }

        // Set up kevent to watch for parent exit
        let mut event: libc::kevent = std::mem::zeroed();
        event.ident = parent_pid as libc::uintptr_t;
        event.filter = libc::EVFILT_PROC;
        event.flags = libc::EV_ADD | libc::EV_ONESHOT;
        event.fflags = libc::NOTE_EXIT;
        event.data = 0;
        event.udata = std::ptr::null_mut();

        // Register the event
        let result = libc::kevent(kq, &event, 1, std::ptr::null_mut(), 0, std::ptr::null());
        if result < 0 {
            libc::close(kq);
            return;
        }

        // Wait for parent to exit (blocks indefinitely)
        let mut triggered: libc::kevent = std::mem::zeroed();
        let n = libc::kevent(kq, std::ptr::null(), 0, &mut triggered, 1, std::ptr::null());

        libc::close(kq);

        // Parent exited - send signal to self
        if n > 0 && (triggered.fflags & libc::NOTE_EXIT) != 0 {
            libc::kill(libc::getpid(), signal);
        }
    }
}

/// Linux implementation using prctl
///
/// Note: This must be called after fork() but before exec() to be effective.
/// The signal is automatically sent when the parent process terminates.
#[cfg(target_os = "linux")]
pub fn setup_parent_death_signal(_parent_pid: i32, signal: i32) -> std::io::Result<()> {
    unsafe {
        if libc::prctl(libc::PR_SET_PDEATHSIG, signal) != 0 {
            return Err(std::io::Error::last_os_error());
        }
    }
    Ok(())
}

/// No-op for unsupported platforms
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
pub fn setup_parent_death_signal(_parent_pid: i32, _signal: i32) -> std::io::Result<()> {
    Ok(())
}

/// Pre-exec safe parent death notification for BSD/macOS.
///
/// This spawns a watcher process (via fork) that monitors the grandparent
/// and sends a signal to the target process group when grandparent exits.
///
/// Call this in a `pre_exec` hook. It forks a watcher that:
/// 1. Monitors `grandparent_pid` (the mothership) via kqueue
/// 2. When grandparent exits, sends `signal` to `target_pgid` (the child's process group)
/// 3. Exits immediately after signaling
///
/// # Safety
/// Must only be called between fork() and exec() (i.e., in pre_exec).
/// Only uses async-signal-safe functions.
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
pub unsafe fn setup_parent_death_signal_preexec(
    grandparent_pid: i32,
    target_pgid: i32,
    signal: i32,
) {
    unsafe {
        // Fork a watcher process
        let pid = libc::fork();
        if pid < 0 {
            // Fork failed, non-fatal
            return;
        }
        if pid > 0 {
            // Parent (the child about to exec) - continue
            return;
        }

        // Watcher process: monitor grandparent, signal target on exit
        watcher_main(grandparent_pid, target_pgid, signal);
    }
}

/// Watcher process main loop - uses only async-signal-safe operations
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
unsafe fn watcher_main(grandparent_pid: i32, target_pgid: i32, signal: i32) -> ! {
    unsafe {
        // Detach from controlling terminal
        let _ = libc::setsid();

        // Close all file descriptors except what we need
        for fd in 0..1024 {
            let _ = libc::close(fd);
        }

        // Create kqueue
        let kq = libc::kqueue();
        if kq < 0 {
            libc::_exit(1);
        }

        // Set up kevent to watch grandparent
        let mut event: libc::kevent = std::mem::zeroed();
        event.ident = grandparent_pid as libc::uintptr_t;
        event.filter = libc::EVFILT_PROC;
        event.flags = libc::EV_ADD | libc::EV_ONESHOT;
        event.fflags = libc::NOTE_EXIT;
        event.data = 0;
        event.udata = std::ptr::null_mut();

        // Register
        if libc::kevent(kq, &event, 1, std::ptr::null_mut(), 0, std::ptr::null()) < 0 {
            libc::close(kq);
            libc::_exit(1);
        }

        // Check if grandparent already dead (race condition)
        if libc::kill(grandparent_pid, 0) != 0 {
            libc::killpg(target_pgid, signal);
            libc::close(kq);
            libc::_exit(0);
        }

        // Wait for grandparent to exit
        let mut triggered: libc::kevent = std::mem::zeroed();
        let n = libc::kevent(kq, std::ptr::null(), 0, &mut triggered, 1, std::ptr::null());
        libc::close(kq);

        if n > 0 && (triggered.fflags & libc::NOTE_EXIT) != 0 {
            // Grandparent exited - signal target process group
            libc::killpg(target_pgid, signal);
        }

        libc::_exit(0);
    }
}

/// Linux version - uses prctl directly in pre_exec
#[cfg(target_os = "linux")]
pub unsafe fn setup_parent_death_signal_preexec(
    _grandparent_pid: i32,
    _target_pgid: i32,
    signal: i32,
) {
    unsafe {
        let _ = libc::prctl(libc::PR_SET_PDEATHSIG, signal);
    }
}

/// No-op for unsupported platforms
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
pub unsafe fn setup_parent_death_signal_preexec(
    _grandparent_pid: i32,
    _target_pgid: i32,
    _signal: i32,
) {
}

#[cfg(all(test, any(target_os = "macos", target_os = "freebsd")))]
mod tests {
    use super::*;
    use std::time::Duration;

    #[test]
    fn test_setup_with_nonexistent_pid() {
        // Use a very high PID that almost certainly doesn't exist
        let result = setup_parent_death_signal(999_999_999, libc::SIGTERM);
        assert!(result.is_err());
    }

    #[test]
    fn test_setup_with_valid_parent() {
        // Use current process as target (it exists)
        let pid = std::process::id() as i32;
        let result = setup_parent_death_signal(pid, libc::SIGTERM);
        assert!(result.is_ok());
        // Give thread time to start
        std::thread::sleep(Duration::from_millis(10));
    }

    #[test]
    fn test_setup_with_own_parent() {
        // Watch our actual parent process
        let ppid = unsafe { libc::getppid() };
        let result = setup_parent_death_signal(ppid, libc::SIGTERM);
        assert!(result.is_ok());
        std::thread::sleep(Duration::from_millis(10));
    }
}