use crate::child_usage::ChildUsage;
use crate::exit_watch::ExitWatch;
use crate::identity::{Origin, Proof};
use crate::process_handle::lock_child;
use std::{
io,
process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
sync::{Arc, Mutex},
time::{Duration, Instant},
};
pub(crate) const CAN_OBSERVE_WITHOUT_REAPING: bool = cfg!(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
));
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Leader {
Unreaped,
Reaped(ExitStatus),
Lost,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LeaderState {
Running,
Exited,
Reaped,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Members {
Pids(Vec<u32>),
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SignalTarget {
Group,
Leader,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SignalOutcome {
Sent,
Retired,
Gone,
MidExit,
}
#[derive(Debug)]
pub struct UnixChild {
pub stdin: Option<ChildStdin>,
pub stdout: Option<ChildStdout>,
pub stderr: Option<ChildStderr>,
child: Child,
pid: u32,
leader: Leader,
usage: Option<ChildUsage>,
origin: Origin,
watch: Arc<ExitWatch>,
}
impl UnixChild {
pub(crate) fn new(mut child: Child) -> Self {
let watch = Arc::new(ExitWatch::open(child.id()));
let origin = Origin::capture(child.id());
Self {
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
pid: child.id(),
origin,
watch,
child,
leader: Leader::Unreaped,
usage: None,
}
}
pub(crate) fn reaped_usage(&self) -> Option<ChildUsage> {
self.usage
}
fn reap_nonblocking(&mut self) -> io::Result<Option<ExitStatus>> {
use std::os::unix::process::ExitStatusExt;
loop {
let mut raw_status: libc::c_int = 0;
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
let reaped = unsafe {
libc::wait4(
self.pid as libc::pid_t,
&mut raw_status,
libc::WNOHANG,
&mut usage,
)
};
if reaped == 0 {
return Ok(None);
}
if reaped > 0 {
if reaped as u32 != self.pid {
return Err(io::Error::other(
"wait4 reaped a process that is not the child",
));
}
self.usage = Some(ChildUsage::from_rusage(&usage));
return Ok(Some(ExitStatus::from_raw(raw_status)));
}
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
}
pub(crate) fn exit_watch(&self) -> Arc<ExitWatch> {
Arc::clone(&self.watch)
}
pub fn id(&self) -> u32 {
self.pid
}
pub(crate) fn require_identity(&mut self) -> io::Result<()> {
if !matches!(self.origin, Origin::Missing) {
return Ok(());
}
Err(io::Error::other(
"the spawned child could not be identified; it must be stopped because a later \
signal could not be proven to reach it",
))
}
pub(crate) fn cleanup_unidentified(mut self, timeout: Option<Duration>) -> bool {
let _ = self.child.kill();
let Some(timeout) = timeout else {
return self.child.wait().is_ok();
};
let deadline = std::time::Instant::now().checked_add(timeout);
let refused = false;
let reaped = if refused {
Ok(None)
} else {
self.watch.wait_for(deadline, || self.child.try_wait())
};
if matches!(reaped, Ok(Some(_))) {
return true;
}
let mut child = self.child;
let _ = std::thread::Builder::new()
.name("rightkit-partial-reap".into())
.spawn(move || {
let _ = child.wait();
});
false
}
fn prove(&mut self) -> io::Result<bool> {
if !matches!(self.leader, Leader::Unreaped) {
return Ok(false);
}
if self.origin.check(self.pid)? == Proof::Same {
Ok(true)
} else {
self.leader = Leader::Lost;
Ok(false)
}
}
fn require_ours(&mut self) -> io::Result<()> {
if self.prove()? {
Ok(())
} else {
Err(io::Error::from_raw_os_error(libc::ECHILD))
}
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
match self.leader {
Leader::Reaped(status) => return Ok(Some(status)),
Leader::Lost => return Err(io::Error::from_raw_os_error(libc::ECHILD)),
Leader::Unreaped => {}
}
if let Err(error) = self.require_ours() {
return Err(self.note_error(error));
}
match self.reap_nonblocking() {
Ok(Some(status)) => {
self.note_reaped(status);
Ok(Some(status))
}
Ok(None) => Ok(None),
Err(error) => Err(self.note_error(error)),
}
}
pub fn wait(&mut self) -> io::Result<ExitStatus> {
drop(self.stdin.take());
self.wait_reaping()
}
pub fn kill(&mut self) -> io::Result<()> {
self.signal(SignalTarget::Leader, libc::SIGKILL).map(|_| ())
}
pub(crate) fn wait_reaping(&mut self) -> io::Result<ExitStatus> {
let watch = Arc::clone(&self.watch);
watch
.wait_for(None, || self.try_wait())?
.ok_or_else(|| io::Error::other("an unbounded wait ended without a status"))
}
pub(crate) fn leader_state(&mut self) -> io::Result<LeaderState> {
if !self.prove()? {
return Ok(LeaderState::Reaped);
}
match probe_exit(self.pid) {
Ok(true) => Ok(if self.prove()? {
LeaderState::Exited
} else {
LeaderState::Reaped
}),
Ok(false) => Ok(LeaderState::Running),
Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {
self.leader = Leader::Lost;
Ok(LeaderState::Reaped)
}
Err(error) => Err(error),
}
}
pub(crate) fn signal(
&mut self,
target: SignalTarget,
signal: i32,
) -> io::Result<SignalOutcome> {
if self.leader_state()? == LeaderState::Reaped {
return Ok(SignalOutcome::Retired);
}
if !self.prove()? {
return Ok(SignalOutcome::Retired);
}
let id = self.signal_id() as i32;
let target_id = match target {
SignalTarget::Group => -id,
SignalTarget::Leader => id,
};
if raw_kill(self.pid, target_id, signal) == 0 {
return Ok(SignalOutcome::Sent);
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ESRCH) => Ok(SignalOutcome::Gone),
Some(libc::EPERM) if target == SignalTarget::Group => {
Ok(if self.leader_state()? == LeaderState::Running {
SignalOutcome::MidExit
} else {
SignalOutcome::Gone
})
}
_ => Err(error),
}
}
pub(crate) fn group_other_members(&mut self) -> io::Result<Members> {
if self.leader_state()? == LeaderState::Reaped {
return Ok(Members::Pids(Vec::new()));
}
group_members_besides(self.pid, self.signal_id())
}
fn signal_id(&self) -> u32 {
self.pid
}
fn note_reaped(&mut self, status: ExitStatus) {
self.leader = Leader::Reaped(status);
}
fn note_error(&mut self, error: io::Error) -> io::Error {
if error.raw_os_error() == Some(libc::ECHILD) {
self.leader = Leader::Lost;
}
error
}
}
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
))]
fn probe_exit(pid: u32) -> io::Result<bool> {
loop {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::waitid(
libc::P_PID,
pid as libc::id_t,
&mut info,
libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
)
};
if result == 0 {
#[cfg(any(target_os = "linux", target_os = "android"))]
let reported = unsafe { info.si_pid() };
#[cfg(not(any(target_os = "linux", target_os = "android")))]
let reported = info.si_pid;
return Ok(reported as u32 == pid);
}
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
)))]
fn probe_exit(_pid: u32) -> io::Result<bool> {
Ok(false)
}
fn raw_kill(leader: u32, target: i32, signal: i32) -> i32 {
let _ = leader;
unsafe { libc::kill(target, signal) }
}
#[cfg(target_os = "macos")]
fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
const PROC_PGRP_ONLY: u32 = 2;
const CAPACITY: usize = 256;
let mut pids = [0 as libc::pid_t; CAPACITY];
let bytes = unsafe {
libc::proc_listpids(
PROC_PGRP_ONLY,
group,
pids.as_mut_ptr().cast(),
std::mem::size_of_val(&pids) as libc::c_int,
)
};
if bytes <= 0 {
if raw_kill(leader, -(group as i32), 0) == 0 {
return Ok(Members::Unknown);
}
return Ok(match io::Error::last_os_error().raw_os_error() {
Some(libc::ESRCH) | Some(libc::EPERM) => Members::Pids(Vec::new()),
_ => Members::Unknown,
});
}
let count = (bytes as usize / std::mem::size_of::<libc::pid_t>()).min(CAPACITY);
if count >= CAPACITY {
return Ok(Members::Unknown);
}
Ok(Members::Pids(
pids[..count]
.iter()
.filter(|&&pid| pid > 0 && pid as u32 != leader)
.map(|&pid| pid as u32)
.collect(),
))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Ok(Members::Unknown);
};
let mut members = Vec::new();
for entry in entries.flatten() {
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<u32>().ok())
else {
continue;
};
if pid == leader {
continue;
}
let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
continue; };
let Some(rest) = stat.rsplit_once(')').map(|(_, rest)| rest) else {
return Ok(Members::Unknown);
};
let mut fields = rest.split_whitespace();
let (Some(state), Some(_parent), Some(pgrp)) =
(fields.next(), fields.next(), fields.next())
else {
return Ok(Members::Unknown);
};
if pgrp.parse::<u32>().ok() == Some(group) && state != "Z" && state != "X" {
members.push(pid);
}
}
Ok(Members::Pids(members))
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
fn group_members_besides(_leader: u32, _group: u32) -> io::Result<Members> {
Ok(Members::Unknown)
}
pub(crate) fn wait_shared(
shared: &Mutex<UnixChild>,
deadline: Option<Instant>,
) -> io::Result<Option<ExitStatus>> {
let watch = lock_child(shared).exit_watch();
watch.wait_for(deadline, || lock_child(shared).try_wait())
}
pub(crate) fn observe_shared(
shared: &Mutex<UnixChild>,
deadline: Option<Instant>,
) -> io::Result<bool> {
let watch = lock_child(shared).exit_watch();
let seen = watch.wait_for(deadline, || {
let state = lock_child(shared).leader_state();
Ok(match state? {
LeaderState::Running => None,
LeaderState::Exited | LeaderState::Reaped => Some(()),
})
})?;
Ok(seen.is_some())
}