use std::io;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use tokio::process::{Child, Command};
#[cfg(not(target_os = "freebsd"))]
compile_error!(
"sys::freebsd is the FreeBSD-only backend (it calls procctl(2)); the platform \
dispatcher in sys/mod.rs must not select it for any other target"
);
use crate::Mechanism;
#[cfg(feature = "process-control")]
use crate::Signal;
#[cfg(feature = "limits")]
use crate::limits::{LimitEvidence, ResourceLimits};
#[cfg(feature = "process-control")]
use crate::member::MemberInfo;
#[cfg(feature = "stats")]
use crate::stats::ProcessGroupStats;
use crate::sys::pgroup::ProcessGroup;
#[cfg(feature = "stats")]
use crate::sys::{ProcIdentity, ProcMetrics};
const REAPER_STATUS_OWNED: libc::c_uint = 0x0000_0001;
const REAPER_PIDINFO_VALID: libc::c_uint = 0x0000_0001;
const REAPER_PIDINFO_ZOMBIE: libc::c_uint = 0x0000_0008;
const REAPER_KILL_SUBTREE: libc::c_uint = 0x0000_0002;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
struct ReaperStatus {
rs_flags: libc::c_uint,
rs_children: libc::c_uint,
rs_descendants: libc::c_uint,
rs_reaper: libc::pid_t,
rs_pid: libc::pid_t,
rs_pad0: [libc::c_uint; 15],
}
impl ReaperStatus {
const ZERO: Self = Self {
rs_flags: 0,
rs_children: 0,
rs_descendants: 0,
rs_reaper: 0,
rs_pid: 0,
rs_pad0: [0; 15],
};
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
struct ReaperPidInfo {
pi_pid: libc::pid_t,
pi_subtree: libc::pid_t,
pi_flags: libc::c_uint,
pi_pad0: [libc::c_uint; 15],
}
impl ReaperPidInfo {
const ZERO: Self = Self {
pi_pid: 0,
pi_subtree: 0,
pi_flags: 0,
pi_pad0: [0; 15],
};
fn is_valid(&self) -> bool {
self.pi_flags & REAPER_PIDINFO_VALID != 0
}
fn is_zombie(&self) -> bool {
self.pi_flags & REAPER_PIDINFO_ZOMBIE != 0
}
fn is_own_fork(&self) -> bool {
self.pi_pid == self.pi_subtree
}
}
#[repr(C)]
#[derive(Debug)]
struct ReaperPids {
rp_count: libc::c_uint,
rp_pad0: [libc::c_uint; 15],
rp_pids: *mut ReaperPidInfo,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
struct ReaperKill {
rk_sig: libc::c_int,
rk_flags: libc::c_uint,
rk_subtree: libc::pid_t,
rk_killed: libc::c_uint,
rk_fpid: libc::pid_t,
rk_pad0: [libc::c_uint; 15],
}
fn self_pid() -> libc::pid_t {
unsafe { libc::getpid() }
}
fn procctl_self(cmd: libc::c_int, data: *mut libc::c_void) -> io::Result<()> {
let rc = unsafe { libc::procctl(libc::P_PID, self_pid() as libc::id_t, cmd, data) };
if rc == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
fn reap_status() -> io::Result<ReaperStatus> {
let mut status = ReaperStatus::ZERO;
procctl_self(
libc::PROC_REAP_STATUS,
std::ptr::addr_of_mut!(status).cast::<libc::c_void>(),
)?;
Ok(status)
}
fn acquire_reaper_status() -> bool {
static ACQUIRED: OnceLock<bool> = OnceLock::new();
*ACQUIRED.get_or_init(|| {
match procctl_self(libc::PROC_REAP_ACQUIRE, std::ptr::null_mut()) {
Ok(()) => {}
Err(err) if err.raw_os_error() == Some(libc::EBUSY) => {}
Err(_) => return false,
}
reap_status()
.is_ok_and(|s| s.rs_flags & REAPER_STATUS_OWNED != 0 || s.rs_reaper == self_pid())
})
}
const GETPIDS_SLACK: usize = 16;
const GETPIDS_GROW_ATTEMPTS: u32 = 4;
const GETPIDS_MAX: usize = 1 << 20;
struct Listing {
entries: Vec<ReaperPidInfo>,
truncated: bool,
}
impl Listing {
const EMPTY: Self = Self {
entries: Vec::new(),
truncated: false,
};
}
fn descendants() -> io::Result<Listing> {
let status = reap_status()?;
if status.rs_descendants == 0 {
return Ok(Listing::EMPTY);
}
let mut capacity = (status.rs_descendants as usize)
.saturating_add(GETPIDS_SLACK)
.min(GETPIDS_MAX);
let mut attempt = 0;
loop {
let mut buf = vec![ReaperPidInfo::ZERO; capacity];
let mut request = ReaperPids {
rp_count: capacity as libc::c_uint,
rp_pad0: [0; 15],
rp_pids: buf.as_mut_ptr(),
};
procctl_self(
libc::PROC_REAP_GETPIDS,
std::ptr::addr_of_mut!(request).cast::<libc::c_void>(),
)?;
let filled = buf.iter().take_while(|entry| entry.is_valid()).count();
buf.truncate(filled);
attempt += 1;
if filled < capacity {
return Ok(Listing {
entries: buf,
truncated: false,
});
}
if attempt >= GETPIDS_GROW_ATTEMPTS || capacity >= GETPIDS_MAX {
return Ok(Listing {
entries: buf,
truncated: true,
});
}
capacity = capacity.saturating_mul(2).min(GETPIDS_MAX);
}
}
fn is_member(entry: &ReaperPidInfo, roots: &[libc::pid_t]) -> bool {
entry.is_valid() && !entry.is_zombie() && roots.contains(&entry.pi_subtree)
}
fn is_stray_zombie(entry: &ReaperPidInfo) -> bool {
entry.is_valid() && entry.is_zombie() && !entry.is_own_fork()
}
fn has_live_descendant(all: &[ReaperPidInfo], roots: &[libc::pid_t]) -> bool {
all.iter()
.any(|entry| !entry.is_own_fork() && is_member(entry, roots))
}
fn reap_stray_zombies(all: &[ReaperPidInfo]) {
for entry in all.iter().filter(|entry| is_stray_zombie(entry)) {
let mut status: libc::c_int = 0;
let _ = unsafe { libc::waitpid(entry.pi_pid, &mut status, libc::WNOHANG) };
}
}
fn is_live_descendant(pid: libc::pid_t) -> bool {
if pid <= 0 {
return false;
}
descendants().is_ok_and(|listing| {
listing
.entries
.iter()
.any(|entry| entry.pi_pid == pid && entry.is_valid() && !entry.is_zombie())
})
}
const DRAIN_BUDGET: Duration = Duration::from_millis(100);
const DRAIN_POLL: Duration = Duration::from_millis(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Root {
pid: libc::pid_t,
seq: u64,
}
#[derive(Debug, Default)]
struct RootSet {
items: Vec<Root>,
next_seq: u64,
}
struct Reaper {
active: bool,
outside_members: std::sync::atomic::AtomicBool,
roots: Mutex<RootSet>,
}
impl Reaper {
fn new() -> Self {
Self {
active: acquire_reaper_status(),
outside_members: std::sync::atomic::AtomicBool::new(false),
roots: Mutex::new(RootSet::default()),
}
}
fn has_outside_members(&self) -> bool {
self.outside_members
.load(std::sync::atomic::Ordering::Relaxed)
}
#[cfg(feature = "process-control")]
fn mark_outside_member(&self) {
self.outside_members
.store(true, std::sync::atomic::Ordering::Relaxed);
}
#[cfg(feature = "process-control")]
fn covers(&self, pid: libc::pid_t) -> bool {
descendants().is_ok_and(|listing| {
listing
.entries
.iter()
.any(|entry| entry.pi_pid == pid && entry.is_valid())
})
}
fn record(&self, pid: libc::pid_t) {
if !self.active || pid <= 0 {
return;
}
let since = self.seq_mark();
let listing = descendants().ok();
self.insert_root(pid, listing.as_ref(), since);
}
fn insert_root(&self, pid: libc::pid_t, listing: Option<&Listing>, since: u64) {
let mut roots = self.lock_roots();
if let Some(listing) = listing {
Self::prune_locked(&mut roots, listing, since);
}
roots.items.retain(|root| root.pid != pid);
let seq = roots.next_seq;
roots.next_seq += 1;
roots.items.push(Root { pid, seq });
}
fn lock_roots(&self) -> std::sync::MutexGuard<'_, RootSet> {
self.roots.lock().unwrap_or_else(|e| e.into_inner())
}
fn roots(&self) -> Vec<libc::pid_t> {
self.lock_roots()
.items
.iter()
.map(|root| root.pid)
.collect()
}
fn root_snapshot(&self) -> Vec<Root> {
self.lock_roots().items.clone()
}
fn forget(&self, root: Root) {
self.lock_roots().items.retain(|item| *item != root);
}
#[cfg(feature = "pty")]
fn hard_kill_subtree(&self, root: libc::pid_t) {
if !self.active {
return; }
let mut request = ReaperKill {
rk_sig: libc::SIGKILL,
rk_flags: REAPER_KILL_SUBTREE,
rk_subtree: root,
rk_killed: 0,
rk_fpid: 0,
rk_pad0: [0; 15],
};
let _ = procctl_self(
libc::PROC_REAP_KILL,
std::ptr::addr_of_mut!(request).cast::<libc::c_void>(),
);
}
fn seq_mark(&self) -> u64 {
self.lock_roots().next_seq
}
fn prune(&self, listing: &Listing, since: u64) {
Self::prune_locked(&mut self.lock_roots(), listing, since);
}
fn prune_locked(roots: &mut RootSet, listing: &Listing, since: u64) {
if listing.truncated {
return;
}
roots.items.retain(|root| {
root.seq >= since
|| listing
.entries
.iter()
.any(|entry| entry.pi_subtree == root.pid)
});
}
#[cfg(any(feature = "process-control", feature = "stats"))]
fn tree(&self) -> io::Result<Vec<ReaperPidInfo>> {
self.read_tree(true)
}
fn tree_probe(&self) -> io::Result<Vec<ReaperPidInfo>> {
self.read_tree(false)
}
fn read_tree(&self, prune: bool) -> io::Result<Vec<ReaperPidInfo>> {
let listing = self.read_listing(prune)?;
let roots = self.roots();
Ok(listing
.entries
.into_iter()
.filter(|entry| is_member(entry, &roots))
.collect())
}
fn read_listing(&self, prune: bool) -> io::Result<Listing> {
let since = self.seq_mark();
let listing = descendants()?;
reap_stray_zombies(&listing.entries);
if prune {
self.prune(&listing, since);
}
Ok(listing)
}
fn prune_stale_roots(&self) {
let _ = self.read_listing(true);
}
fn signal_tree(&self, sig: libc::c_int) -> io::Result<()> {
self.prune_stale_roots();
let mut surfaced: Option<io::Error> = None;
for root in self.root_snapshot() {
let mut request = ReaperKill {
rk_sig: sig,
rk_flags: REAPER_KILL_SUBTREE,
rk_subtree: root.pid,
rk_killed: 0,
rk_fpid: 0,
rk_pad0: [0; 15],
};
let Err(err) = procctl_self(
libc::PROC_REAP_KILL,
std::ptr::addr_of_mut!(request).cast::<libc::c_void>(),
) else {
continue;
};
if err.raw_os_error() == Some(libc::ESRCH) {
self.forget(root);
}
if surfaced.is_none() && is_honest_failure(&err, request.rk_fpid) {
surfaced = Some(err);
}
}
match surfaced {
Some(err) => Err(err),
None => Ok(()),
}
}
fn drain_dead(&self, budget: Duration) {
let deadline = Instant::now() + budget;
loop {
let Ok(listing) = self.read_listing(true) else {
return;
};
if !has_live_descendant(&listing.entries, &self.roots()) || Instant::now() >= deadline {
return;
}
std::thread::sleep(DRAIN_POLL);
}
}
}
fn is_honest_failure(err: &io::Error, first_failing_pid: libc::pid_t) -> bool {
match err.raw_os_error() {
Some(libc::ESRCH) => false,
Some(libc::EPERM) => is_live_descendant(first_failing_pid),
_ => true,
}
}
pub(crate) struct Job {
group: ProcessGroup,
reaper: Reaper,
}
impl Job {
pub(crate) fn new(#[cfg(feature = "limits")] limits: &ResourceLimits) -> io::Result<Self> {
#[cfg(feature = "limits")]
if limits.any() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"resource limits require a cgroup or Job Object; unavailable on this target",
));
}
Ok(Job {
group: ProcessGroup::new(),
reaper: Reaper::new(),
})
}
pub(crate) fn spawn(
&self,
cmd: &mut Command,
opts: &crate::sys::SpawnOptions,
) -> io::Result<Child> {
let child = self.group.spawn(cmd, opts)?;
self.record_root(&child);
Ok(child)
}
#[cfg(feature = "pty")]
pub(crate) fn spawn_pty(
&self,
cmd: &mut Command,
opts: &crate::sys::SpawnOptions,
_env: Option<Vec<(std::ffi::OsString, std::ffi::OsString)>>,
) -> io::Result<crate::sys::pty::PtySpawn> {
let displaced = std::cell::Cell::new(crate::sys::DisplacedSpare::default());
crate::sys::pty::spawn_pty(
cmd,
opts,
|c, o| {
let (child, spare) = self.group.spawn_displacing_spare(c, o)?;
self.record_root(&child);
displaced.set(spare);
Ok(child)
},
|pid| self.rollback_pty_spawn(pid, displaced.take()),
)
}
#[cfg(feature = "pty")]
pub(crate) fn rollback_pty_spawn(&self, pid: u32, displaced: crate::sys::DisplacedSpare) {
self.reaper.hard_kill_subtree(pid as libc::pid_t);
self.group.rollback_pty_spawn(pid, displaced);
}
fn record_root(&self, child: &Child) {
if let Some(pid) = child.id() {
self.reaper.record(pid as libc::pid_t);
}
}
#[cfg(feature = "process-control")]
pub(crate) fn adopt(&self, child: &Child) -> io::Result<()> {
self.group.adopt(child)?;
let Some(pid) = child.id().map(|pid| pid as libc::pid_t) else {
return Ok(());
};
if !self.reaper.active {
return Ok(());
}
if self.reaper.covers(pid) {
self.reaper.record(pid);
} else {
self.reaper.mark_outside_member();
}
Ok(())
}
#[cfg(feature = "process-control")]
pub(crate) fn adopt_external(&self, pid: u32) -> io::Result<()> {
self.group.adopt_external(pid)
}
pub(crate) fn kill_all(&self) -> io::Result<()> {
if !self.reaper.active {
return self.group.kill_all();
}
let reaper = self.reaper.signal_tree(libc::SIGKILL);
let group = self.group.kill_all();
reaper.and(group)
}
#[cfg(feature = "limits")]
pub(crate) fn update_limits(&self, limits: &ResourceLimits) -> io::Result<()> {
if limits.any() {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"resource limits require a cgroup or Job Object; unavailable on this target",
))
} else {
Ok(())
}
}
#[cfg(feature = "limits")]
pub(crate) fn limit_evidence(&self, _capped: crate::limits::CappedAxes) -> LimitEvidence {
LimitEvidence::unknown()
}
#[cfg(feature = "process-control")]
pub(crate) fn signal(&self, sig: Signal) -> io::Result<()> {
let raw = sig.raw();
if !self.reaper.active || raw <= 0 {
return self.group.signal(raw);
}
let reaped = self.reaper.signal_tree(raw);
if self.reaper.has_outside_members() {
return reaped.and(self.group.signal(raw));
}
reaped
}
#[cfg(feature = "process-control")]
pub(crate) fn soft_stop_scope(&self) -> crate::SoftStopScope {
crate::SoftStopScope::WholeTree
}
#[cfg(feature = "process-control")]
pub(crate) fn suspend(&self) -> io::Result<()> {
if !self.reaper.active {
return self.group.suspend();
}
let reaped = self.reaper.signal_tree(libc::SIGSTOP);
if self.reaper.has_outside_members() {
return reaped.and(self.group.suspend());
}
reaped
}
#[cfg(feature = "process-control")]
pub(crate) fn resume(&self) -> io::Result<()> {
if !self.reaper.active {
return self.group.resume();
}
let reaped = self.reaper.signal_tree(libc::SIGCONT);
if self.reaper.has_outside_members() {
return reaped.and(self.group.resume());
}
reaped
}
#[cfg(feature = "process-control")]
pub(crate) fn members(&self) -> io::Result<Vec<u32>> {
if !self.reaper.active {
return Ok(self
.group
.members()
.into_iter()
.map(|pid| pid as u32)
.collect());
}
Ok(self.member_pids(&self.reaper.tree()?))
}
#[cfg(feature = "process-control")]
fn member_pids(&self, tree: &[ReaperPidInfo]) -> Vec<u32> {
let mut pids: Vec<u32> = tree.iter().map(|entry| entry.pi_pid as u32).collect();
if self.reaper.has_outside_members() {
for pid in self.group.members() {
let pid = pid as u32;
if !pids.contains(&pid) {
pids.push(pid);
}
}
}
pids
}
#[cfg(feature = "process-control")]
pub(crate) fn members_info(&self) -> io::Result<Vec<MemberInfo>> {
if !self.reaper.active {
return Ok(self.group.members_info());
}
Ok(self
.member_pids(&self.reaper.tree()?)
.into_iter()
.map(|pid| MemberInfo::new(pid, None, None, None))
.collect())
}
pub(crate) async fn graceful_shutdown(
&self,
signal: i32,
timeout: Duration,
escalate: bool,
) -> io::Result<super::graceful::GracefulOutcome> {
if !self.reaper.active {
return self
.group
.graceful_shutdown(signal, timeout, escalate)
.await;
}
super::graceful::run(self, self.group.skip_drop_kill(), signal, timeout, escalate).await
}
#[cfg(feature = "stats")]
pub(crate) fn stats(&self) -> io::Result<ProcessGroupStats> {
if !self.reaper.active {
return self.group.stats();
}
let tree = self.reaper.tree()?;
#[cfg(feature = "process-control")]
let active_process_count = self.member_pids(&tree).len();
#[cfg(not(feature = "process-control"))]
let active_process_count = tree.len();
Ok(ProcessGroupStats {
active_process_count,
total_cpu_time: None,
peak_memory_bytes: None,
io_read_bytes: None,
io_write_bytes: None,
peak_process_count: None,
})
}
pub(crate) fn mechanism(&self) -> Mechanism {
if self.reaper.active {
Mechanism::ProcessReaper
} else {
Mechanism::ProcessGroup
}
}
}
impl super::graceful::GracefulTarget for Job {
fn signal_all(&self, signal: i32) -> super::graceful::SoftDelivery {
let mut sent = self.reaper.signal_tree(signal).is_ok();
if self.reaper.has_outside_members() {
sent &= super::graceful::GracefulTarget::signal_all(&self.group, signal)
== super::graceful::SoftDelivery::Sent;
}
if sent {
super::graceful::SoftDelivery::Sent
} else {
super::graceful::SoftDelivery::Failed
}
}
fn is_drained(&self) -> bool {
let Ok(tree) = self.reaper.tree_probe() else {
return super::graceful::GracefulTarget::is_drained(&self.group);
};
if !tree.is_empty() {
return false;
}
!self.reaper.has_outside_members()
|| super::graceful::GracefulTarget::is_drained(&self.group)
}
fn alive_count(&self) -> Option<usize> {
let tree = self.reaper.tree_probe().ok()?;
#[cfg(feature = "process-control")]
{
Some(self.member_pids(&tree).len())
}
#[cfg(not(feature = "process-control"))]
{
Some(tree.len())
}
}
fn hard_kill(&self) -> io::Result<()> {
let reaper = self.reaper.signal_tree(libc::SIGKILL);
let group = super::graceful::GracefulTarget::hard_kill(&self.group);
reaper.and(group)
}
}
impl Drop for Job {
fn drop(&mut self) {
if !self.reaper.active {
return;
}
if self.group.skip_drop_kill().is_set() {
if let Ok(listing) = descendants() {
reap_stray_zombies(&listing.entries);
}
return;
}
let _ = self.reaper.signal_tree(libc::SIGKILL);
self.reaper.drain_dead(DRAIN_BUDGET);
}
}
pub(crate) fn detect_mechanism() -> Mechanism {
Mechanism::ProcessReaper
}
#[cfg(feature = "stats")]
pub(crate) fn process_metrics(_pid: u32, _expected: Option<ProcIdentity>) -> ProcMetrics {
ProcMetrics::default()
}
#[cfg(feature = "process-control")]
pub(crate) fn process_info(pid: u32) -> io::Result<Option<MemberInfo>> {
crate::sys::pgroup::process_info(pid)
}
#[cfg(feature = "stats")]
pub(crate) fn process_identity(_pid: u32) -> Option<ProcIdentity> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(pid: libc::pid_t, subtree: libc::pid_t, zombie: bool) -> ReaperPidInfo {
ReaperPidInfo {
pi_pid: pid,
pi_subtree: subtree,
pi_flags: REAPER_PIDINFO_VALID | if zombie { REAPER_PIDINFO_ZOMBIE } else { 0 },
pi_pad0: [0; 15],
}
}
fn listing(entries: &[ReaperPidInfo]) -> Listing {
Listing {
entries: entries.to_vec(),
truncated: false,
}
}
fn cut_short(entries: &[ReaperPidInfo]) -> Listing {
Listing {
entries: entries.to_vec(),
truncated: true,
}
}
fn record(reaper: &Reaper, pid: libc::pid_t) {
reaper.insert_root(pid, None, reaper.seq_mark());
}
#[test]
fn reaper_abi_layout_matches_the_kernel_headers() {
use std::mem::{align_of, offset_of, size_of};
assert_eq!(size_of::<ReaperStatus>(), 80);
assert_eq!(size_of::<ReaperKill>(), 80);
assert_eq!(size_of::<ReaperPidInfo>(), 72);
assert_eq!(offset_of!(ReaperPids, rp_pids), 64);
assert_eq!(
size_of::<ReaperPids>(),
64 + size_of::<*mut ReaperPidInfo>()
);
assert_eq!(align_of::<ReaperPidInfo>(), align_of::<libc::c_uint>());
assert_eq!(REAPER_STATUS_OWNED, 0x1);
assert_eq!(REAPER_PIDINFO_VALID, 0x1);
assert_eq!(REAPER_PIDINFO_ZOMBIE, 0x8);
assert_eq!(REAPER_KILL_SUBTREE, 0x2);
}
#[test]
fn membership_is_scoped_to_this_jobs_subtrees() {
let roots = vec![100, 200];
assert!(is_member(&entry(100, 100, false), &roots));
assert!(is_member(&entry(101, 100, false), &roots));
assert!(is_member(&entry(102, 100, false), &roots));
assert!(!is_member(&entry(300, 300, false), &roots));
assert!(!is_member(&entry(301, 300, false), &roots));
}
#[test]
fn a_zombie_is_not_a_live_member() {
let roots = vec![100];
assert!(!is_member(&entry(101, 100, true), &roots));
assert!(!is_member(&ReaperPidInfo::ZERO, &roots));
}
#[test]
fn only_reparented_corpses_are_swept() {
assert!(is_stray_zombie(&entry(101, 100, true)));
assert!(!is_stray_zombie(&entry(100, 100, true)));
assert!(!is_stray_zombie(&entry(101, 100, false)));
}
#[test]
fn the_drain_waits_only_for_processes_below_a_root() {
let roots = vec![100];
assert!(has_live_descendant(&[entry(101, 100, false)], &roots));
assert!(!has_live_descendant(&[entry(101, 100, true)], &roots));
assert!(!has_live_descendant(&[entry(100, 100, false)], &roots));
assert!(!has_live_descendant(&[entry(301, 300, false)], &roots));
}
#[test]
fn a_kill_failure_surfaces_only_when_it_is_a_real_one() {
assert!(!is_honest_failure(
&io::Error::from_raw_os_error(libc::ESRCH),
0
));
assert!(is_honest_failure(
&io::Error::from_raw_os_error(libc::EINVAL),
0
));
assert!(is_honest_failure(
&io::Error::from_raw_os_error(libc::ECAPMODE),
0
));
assert!(!is_honest_failure(
&io::Error::from_raw_os_error(libc::EPERM),
0
));
assert!(!is_honest_failure(
&io::Error::from_raw_os_error(libc::EPERM),
-1
));
}
fn reaper(active: bool) -> Reaper {
Reaper {
active,
outside_members: std::sync::atomic::AtomicBool::new(false),
roots: Mutex::new(RootSet::default()),
}
}
#[cfg(feature = "process-control")]
#[test]
fn the_outside_member_latch_is_off_until_an_adopt_needs_it_and_then_stays_on() {
let reaper = reaper(true);
assert!(!reaper.has_outside_members());
reaper.mark_outside_member();
assert!(reaper.has_outside_members());
record(&reaper, 100);
assert!(reaper.has_outside_members());
}
#[test]
fn roots_are_recorded_once_and_pruned_only_when_the_subtree_is_empty() {
let reaper = reaper(true);
record(&reaper, 100);
record(&reaper, 100); record(&reaper, 200);
assert_eq!(reaper.roots(), vec![100, 200]);
let mark = reaper.seq_mark();
reaper.prune(&listing(&[entry(101, 100, false)]), mark);
assert_eq!(reaper.roots(), vec![100]);
reaper.prune(&listing(&[entry(101, 100, true)]), mark);
assert_eq!(reaper.roots(), vec![100]);
reaper.prune(&listing(&[]), mark);
assert!(reaper.roots().is_empty());
}
#[test]
fn a_pid_that_cannot_name_a_process_is_never_recorded() {
let reaper = reaper(true);
reaper.record(0);
reaper.record(-5);
assert!(reaper.roots().is_empty());
}
#[test]
fn pruning_never_forgets_a_root_recorded_after_the_listing_was_taken() {
let reaper = reaper(true);
record(&reaper, 100);
let mark = reaper.seq_mark(); record(&reaper, 200); reaper.prune(&listing(&[]), mark);
assert_eq!(reaper.roots(), vec![200]);
}
#[test]
fn a_truncated_listing_prunes_nothing() {
let reaper = reaper(true);
record(&reaper, 100);
record(&reaper, 200);
let mark = reaper.seq_mark();
reaper.prune(&cut_short(&[entry(301, 300, false)]), mark);
assert_eq!(reaper.roots(), vec![100, 200]);
reaper.prune(&listing(&[entry(301, 300, false)]), mark);
assert!(reaper.roots().is_empty());
}
#[test]
fn recording_forgets_the_roots_the_kernel_has_forgotten() {
let reaper = reaper(true);
record(&reaper, 100);
record(&reaper, 200);
let mark = reaper.seq_mark();
reaper.insert_root(300, Some(&listing(&[entry(201, 200, false)])), mark);
assert_eq!(reaper.roots(), vec![200, 300]);
}
#[test]
fn recording_a_recycled_number_replaces_the_stale_root_rather_than_keeping_it() {
let reaper = reaper(true);
record(&reaper, 100);
let in_flight = reaper.seq_mark(); record(&reaper, 100); assert_eq!(
reaper.roots(),
vec![100],
"still exactly one root for the pid"
);
reaper.prune(&listing(&[]), in_flight);
assert_eq!(
reaper.roots(),
vec![100],
"the re-recorded root outranks the stale listing"
);
}
#[test]
fn forgetting_a_root_matches_the_stamp_not_just_the_number() {
let reaper = reaper(true);
record(&reaper, 100);
let stale = reaper.root_snapshot()[0];
record(&reaper, 100); reaper.forget(stale);
assert_eq!(reaper.roots(), vec![100]);
let current = reaper.root_snapshot()[0];
reaper.forget(current);
assert!(reaper.roots().is_empty());
}
#[test]
fn an_inactive_reaper_records_nothing() {
let reaper = reaper(false);
reaper.record(100);
assert!(reaper.roots().is_empty());
}
}