liminal-cli 0.3.2

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! The unreaped leader reserves its process-group id until every signal is sent.

use rustix::process::{Pid, Signal, WaitId, WaitIdOptions, kill_process_group, waitid};
use tokio::process::Child;
use tokio::task::JoinHandle;

pub(super) struct Contained {
    child: Child,
    pid: Pid,
    reaped: bool,
}

impl Contained {
    pub fn new(child: Child) -> anyhow::Result<Self> {
        let pid = child
            .id()
            .and_then(|pid| i32::try_from(pid).ok())
            .and_then(Pid::from_raw)
            .ok_or_else(|| anyhow::anyhow!("ACP child has no usable process-group id"))?;
        Ok(Self {
            child,
            pid,
            reaped: false,
        })
    }
    pub const fn child(&mut self) -> &mut Child {
        &mut self.child
    }
    pub fn observe_exit(&self) -> JoinHandle<anyhow::Result<i32>> {
        let pid = self.pid;
        tokio::task::spawn_blocking(move || {
            let status = loop {
                match waitid(
                    WaitId::Pid(pid),
                    WaitIdOptions::EXITED | WaitIdOptions::NOWAIT,
                ) {
                    Ok(Some(status)) => break status,
                    Ok(None) => anyhow::bail!("ACP exit observer returned no status"),
                    Err(rustix::io::Errno::INTR) => {}
                    Err(error) => anyhow::bail!("ACP exit observer failed: {error}"),
                }
            };
            status
                .exit_status()
                .or_else(|| status.terminating_signal().map(|signal| 128 + signal))
                .ok_or_else(|| anyhow::anyhow!("ACP exit has no status or signal"))
        })
    }
    pub fn kill(&mut self) -> anyhow::Result<()> {
        let signalled = match kill_process_group(self.pid, Signal::KILL) {
            Ok(()) | Err(rustix::io::Errno::SRCH) => Ok(()),
            Err(error) => Err(anyhow::anyhow!("ACP group shutdown failed: {error}")),
        };
        // Even a signal failure does not cancel the owned leader's reap.
        if signalled.is_err() {
            self.child.start_kill()?;
        }
        signalled
    }
    pub async fn reap(&mut self) -> anyhow::Result<()> {
        self.child.wait().await?;
        self.reaped = true;
        Ok(())
    }
}

impl Drop for Contained {
    fn drop(&mut self) {
        if !self.reaped {
            if let Err(error) = kill_process_group(self.pid, Signal::KILL) {
                if error != rustix::io::Errno::SRCH {
                    eprintln!("liminal agent group cleanup failed: {error}");
                }
            }
        }
    }
}