tokio-process-tools 0.10.1

Correctness-focused async subprocess orchestration for Tokio: bounded output, multi-consumer streams, output detection, guaranteed cleanup and graceful termination.
Documentation
/// Prepare a command so platform-specific signal delivery targets the spawned child *and* any
/// processes it goes on to fork.
///
/// The child is set up as the leader of a new process group (Unix) or its own console process
/// group (Windows). Termination signals are then sent to that group, not the child's PID, so any
/// grandchildren the child fork-execs (a shell wrapper that launches the real binary, a build
/// tool that fans out workers) are signaled together with the leader instead of being orphaned
/// when the leader exits.
///
/// - **Unix:** sets the child's process group to itself (`setpgid(0, 0)` via Tokio's
///   `Command::process_group(0)`), so the child's PID is also its PGID and the group is
///   addressable as `-PGID` in `kill(2)`.
/// - **Windows:** passes `CREATE_NEW_PROCESS_GROUP`, so `GenerateConsoleCtrlEvent` with the
///   child's PID as the process-group ID delivers `CTRL_BREAK_EVENT` to every process in the
///   group.
/// - **Other platforms:** no-op. The crate's termination APIs are gated to Unix and Windows, so
///   spawning still works on other platforms but the resulting child is not set up for
///   group-targeted signal delivery.
pub(crate) fn prepare_command_for_signalling(
    command: &mut tokio::process::Command,
) -> &mut tokio::process::Command {
    #[cfg(windows)]
    {
        use windows_sys::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP;
        command.creation_flags(CREATE_NEW_PROCESS_GROUP)
    }
    #[cfg(unix)]
    {
        // `0` asks the child to be the leader of a new process group whose PGID equals the
        // child's PID. Termination signals can then target `-PGID` to reach the whole tree.
        command.process_group(0)
    }
    #[cfg(all(not(windows), not(unix)))]
    {
        command
    }
}

/// Send `SIGINT` to the child's process group via `killpg`.
///
/// `SIGINT` is the dedicated user-interrupt signal, distinct from the `SIGTERM` delivered by
/// [`send_terminate`]. Spawns set up the child as the leader of a new process group (see
/// [`prepare_command_for_signalling`]), so the signal reaches any grandchildren the child has
/// forked.
///
/// This is a raw signal sender over `tokio::process::Child`. Callers that can reap process state
/// should do so before using it.
#[cfg(unix)]
pub(crate) fn send_interrupt(child: &tokio::process::Child) -> std::io::Result<()> {
    let Some(pid) = child.id() else {
        // Returns `None` if child was already "polled to completion".
        return Ok(());
    };
    send_to_process_group(pid, nix::sys::signal::Signal::SIGINT)
}

/// Send `SIGTERM` to the child's process group via `killpg`.
///
/// `SIGTERM` is the conventional "asked to terminate" signal sent by service supervisors and the
/// operating system at shutdown. Spawns set up the child as the leader of a new process group
/// (see [`prepare_command_for_signalling`]), so the signal reaches any grandchildren the child
/// has forked.
///
/// This is a raw signal sender over `tokio::process::Child`. Callers that can reap process state
/// should do so before using it.
#[cfg(unix)]
pub(crate) fn send_terminate(child: &tokio::process::Child) -> std::io::Result<()> {
    let Some(pid) = child.id() else {
        // Returns `None` if child was already "polled to completion".
        return Ok(());
    };
    send_to_process_group(pid, nix::sys::signal::Signal::SIGTERM)
}

/// Deliver `CTRL_BREAK_EVENT` to the child's console process group via
/// `GenerateConsoleCtrlEvent`.
///
/// `CTRL_BREAK_EVENT` is the only console control event that can be targeted at a nonzero process
/// group: `CTRL_C_EVENT` requires `dwProcessGroupId = 0` and would be broadcast to every process
/// sharing the calling console (including the parent), so it is not usable to terminate a single
/// child group. There is therefore no separate analogue for `SIGINT` vs. `SIGTERM` on Windows.
///
/// This is a raw signal sender over `tokio::process::Child`. Callers that can reap process state
/// should do so before using it.
#[cfg(windows)]
pub(crate) fn send_ctrl_break(child: &tokio::process::Child) -> std::io::Result<()> {
    use windows_sys::Win32::System::Console::CTRL_BREAK_EVENT;
    use windows_sys::Win32::System::Console::GenerateConsoleCtrlEvent;

    let Some(pid) = child.id() else {
        // Returns `None` if child was already "polled to completion".
        return Ok(());
    };

    let success = unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) };
    if success == 0 {
        return Err(std::io::Error::last_os_error());
    }
    Ok(())
}

/// Sends `signal` to every process in the child's process group on Unix.
///
/// `pid` is the spawned child's PID. Because the child is set up as the leader of a new process
/// group at spawn time, its PID also serves as the PGID. The signal is sent via `killpg`, which
/// translates to `kill(-PGID, signal)` and reaches every member of the group, including any
/// grandchildren the child has fork-execed.
#[cfg(unix)]
fn send_to_process_group(pid: u32, signal: nix::sys::signal::Signal) -> std::io::Result<()> {
    use nix::sys::signal;
    use nix::unistd::Pid;

    signal::killpg(Pid::from_raw(pid.cast_signed()), signal).map_err(std::io::Error::other)?;
    Ok(())
}

/// Sends `SIGKILL` to every process in the child's process group on Unix.
///
/// Unlike [`tokio::process::Child::start_kill`], which targets only the child's PID, this hits
/// the whole group so grandchildren are not orphaned. The caller is still responsible for reaping
/// the leader's exit status afterwards (e.g. via `wait`).
#[cfg(unix)]
pub(crate) fn send_kill_to_process_group(pid: u32) -> std::io::Result<()> {
    send_to_process_group(pid, nix::sys::signal::Signal::SIGKILL)
}