tastty-driver 0.1.0

Terminal automation driver built on tastty
//! Signals to the child process and process group.

use super::Session;
use crate::{Error, Result, Signal};

impl Session {
    /// Send a signal to the direct child process.
    ///
    /// # Errors
    ///
    /// - [`Error::MissingProcessId`] when the session has no recorded
    ///   child PID (already reaped, or never spawned).
    /// - [`Error::UnsupportedSignal`] on non-Unix platforms.
    /// - [`Error::Signal`] when the underlying [`kill(2)`] fails.
    ///
    /// [`kill(2)`]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html
    pub fn signal_process(&self, signal: Signal) -> Result<()> {
        let pid = self.terminal.process_id().ok_or(Error::MissingProcessId)?;
        signal_process_id(pid, signal)
    }

    /// Send a signal to the child process group.
    ///
    /// # Errors
    ///
    /// Same as [`Session::signal_process`]; the group is addressed by
    /// the negated PID per [`kill(2)`].
    ///
    /// [`kill(2)`]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html
    pub fn signal_group(&self, signal: Signal) -> Result<()> {
        let pid = self.terminal.process_id().ok_or(Error::MissingProcessId)?;
        signal_process_group(pid, signal)
    }
}

#[cfg(unix)]
fn signal_impl(pid: i32, signal: Signal) -> Result<()> {
    let sig =
        nix::sys::signal::Signal::try_from(signal.number()).map_err(|source| Error::Signal {
            signal,
            source: std::io::Error::from_raw_os_error(source as i32),
        })?;
    nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), sig).map_err(|source| Error::Signal {
        signal,
        source: source.into(),
    })
}

#[cfg(not(unix))]
fn signal_impl(_pid: i32, _signal: Signal) -> Result<()> {
    Err(Error::UnsupportedSignal)
}

fn signal_process_id(pid: u32, signal: Signal) -> Result<()> {
    signal_impl(pid as i32, signal)
}

fn signal_process_group(pid: u32, signal: Signal) -> Result<()> {
    signal_impl(-(pid as i32), signal)
}

/// `signal_group` wrapper that treats `ESRCH` as success.
///
/// `terminate_with_grace` calls `kill(2)` against the process group at
/// least once (SIGTERM) and possibly twice (SIGKILL after the grace
/// window). On either call the group may already be gone: a cooperative
/// child may have exited on its own between the caller deciding to
/// terminate and the syscall landing, or between the SIGTERM exit and
/// the post-grace SIGKILL the reaper has already observed and reaped.
/// `ESRCH` ("no such process") at either step means the desired
/// post-condition holds, so it must not surface as a `Signal` error.
pub(super) fn send_group_signal_silencing_esrch(session: &Session, signal: Signal) -> Result<()> {
    match session.signal_group(signal) {
        Ok(()) => Ok(()),
        Err(err) if is_esrch(&err) => Ok(()),
        Err(err) => Err(err),
    }
}

fn is_esrch(err: &Error) -> bool {
    #[cfg(unix)]
    {
        matches!(
            err,
            Error::Signal { source, .. }
                if source.raw_os_error() == Some(nix::libc::ESRCH)
        )
    }
    #[cfg(not(unix))]
    {
        let _ = err;
        false
    }
}