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}")),
};
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}");
}
}
}
}
}