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::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,
origin: Origin,
watch: Arc<ExitWatch>,
#[cfg(test)]
recycled_id: Option<u32>,
}
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,
#[cfg(test)]
recycled_id: None,
}
}
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(());
}
let _ = self.child.kill();
let _ = self.child.wait();
self.leader = Leader::Lost;
Err(io::Error::other(
"the spawned child could not be identified; it was stopped because a later \
signal could not be proven to reach it",
))
}
fn prove(&mut self) -> io::Result<bool> {
if !matches!(self.leader, Leader::Unreaped) {
return Ok(false);
}
#[cfg(test)]
trace::record(trace::Event::Proof { leader: self.pid });
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))
}
}
#[cfg(test)]
pub(crate) fn simulate_replaced(&mut self) {
self.origin = Origin::Known(crate::identity::Identity {
parent: 0,
started_sec: 1,
started_usec: 1,
});
}
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));
}
#[cfg(test)]
trace::record(trace::Event::Wait { leader: self.pid });
match self.child.try_wait() {
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())
}
#[cfg(test)]
pub(crate) fn group_has_other_members(&mut self) -> io::Result<bool> {
Ok(match self.group_other_members()? {
Members::Pids(pids) => !pids.is_empty(),
Members::Unknown => true,
})
}
fn signal_id(&self) -> u32 {
#[cfg(test)]
if let Some(id) = self.recycled_id {
return id;
}
self.pid
}
#[cfg(test)]
pub(crate) fn simulate_recycled_id(&mut self, id: u32) {
self.recycled_id = Some(id);
}
fn note_reaped(&mut self, status: ExitStatus) {
self.leader = Leader::Reaped(status);
#[cfg(test)]
trace::record(trace::Event::Reaped { leader: self.pid });
}
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 {
#[cfg(test)]
trace::record(trace::Event::Wait { leader: pid });
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 {
#[cfg(test)]
trace::record(trace::Event::Signal {
leader,
target,
signal,
});
#[cfg(not(test))]
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, pid) = {
let child = lock_child(shared);
(child.exit_watch(), child.id())
};
let mut first = true;
watch.wait_for(deadline, || {
let looked = lock_child(shared).try_wait();
pause_for_test(&mut first, pid, matches!(looked, Ok(None)));
looked
})
}
pub(crate) fn observe_shared(
shared: &Mutex<UnixChild>,
deadline: Option<Instant>,
) -> io::Result<bool> {
let (watch, pid) = {
let child = lock_child(shared);
(child.exit_watch(), child.id())
};
let mut first = true;
let seen = watch.wait_for(deadline, || {
let state = lock_child(shared).leader_state();
pause_for_test(&mut first, pid, matches!(state, Ok(LeaderState::Running)));
Ok(match state? {
LeaderState::Running => None,
LeaderState::Exited | LeaderState::Reaped => Some(()),
})
})?;
Ok(seen.is_some())
}
#[cfg(test)]
fn pause_for_test(first: &mut bool, pid: u32, running: bool) {
if *first && running {
*first = false;
gate::pause(pid);
}
}
#[cfg(not(test))]
fn pause_for_test(_first: &mut bool, _pid: u32, _running: bool) {}
#[cfg(test)]
pub(crate) mod trace {
use std::sync::Mutex;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Event {
Signal {
leader: u32,
target: i32,
signal: i32,
},
Reaped {
leader: u32,
},
Proof {
leader: u32,
},
Wait {
leader: u32,
},
}
static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
pub(crate) fn record(event: Event) {
EVENTS.lock().unwrap_or_else(|p| p.into_inner()).push(event);
}
pub(crate) fn mark() -> usize {
EVENTS.lock().unwrap_or_else(|p| p.into_inner()).len()
}
pub(crate) fn since(mark: usize, leader: u32) -> Vec<Event> {
since_with_waits(mark, leader)
.into_iter()
.filter(|event| !matches!(event, Event::Wait { .. } | Event::Proof { .. }))
.collect()
}
pub(crate) fn since_with_waits(mark: usize, leader: u32) -> Vec<Event> {
EVENTS.lock().unwrap_or_else(|p| p.into_inner())[mark..]
.iter()
.copied()
.filter(|event| match event {
Event::Signal { leader: l, .. }
| Event::Reaped { leader: l }
| Event::Proof { leader: l }
| Event::Wait { leader: l } => *l == leader,
})
.collect()
}
}
#[cfg(test)]
pub(crate) mod gate {
use std::sync::{Arc, Mutex};
type Hook = Arc<dyn Fn() + Send + Sync>;
static HOOKS: Mutex<Vec<(u32, Hook)>> = Mutex::new(Vec::new());
pub(crate) fn install(pid: u32, hook: impl Fn() + Send + Sync + 'static) {
let mut hooks = HOOKS.lock().unwrap_or_else(|p| p.into_inner());
hooks.retain(|(id, _)| *id != pid);
hooks.push((pid, Arc::new(hook)));
}
pub(crate) fn remove(pid: u32) {
HOOKS
.lock()
.unwrap_or_else(|p| p.into_inner())
.retain(|(id, _)| *id != pid);
}
pub(crate) fn pause(pid: u32) {
let hook = HOOKS
.lock()
.unwrap_or_else(|p| p.into_inner())
.iter()
.find(|(id, _)| *id == pid)
.map(|(_, hook)| Arc::clone(hook));
if let Some(hook) = hook {
hook();
}
}
}