use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use crate::platform::process::{DescendantEvent, DescendantMonitorStop};
const POLL_INTERVAL: Duration = Duration::from_millis(20);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ProcessIdentity {
start_ticks: u64,
}
pub fn start_descendant_monitor(
root_pid: u32,
stop: Arc<DescendantMonitorStop>,
emit: Box<dyn Fn(DescendantEvent) + Send>,
) -> std::io::Result<()> {
let Some(root_identity) = process_identity(root_pid) else {
emit(DescendantEvent::Completed);
return Ok(());
};
enable_subreaper();
std::thread::Builder::new()
.name("rp-linux-descpump".to_string())
.spawn(move || pump_loop(root_pid, root_identity, stop, emit))
.map(|_| ())
.map_err(|error| std::io::Error::other(format!("spawn descendant monitor: {error}")))
}
fn enable_subreaper() {
let _ = unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) };
}
fn parse_start_ticks(stat: &str) -> Option<u64> {
let suffix = stat.get(stat.rfind(')')? + 1..)?;
suffix
.split_ascii_whitespace()
.nth(19)
.and_then(|field| field.parse().ok())
}
fn process_identity(pid: u32) -> Option<ProcessIdentity> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
Some(ProcessIdentity {
start_ticks: parse_start_ticks(&stat)?,
})
}
fn descendant_pids(root_pid: u32) -> HashMap<u32, u32> {
let mut result = HashMap::new();
let mut stack = vec![root_pid];
while let Some(pid) = stack.pop() {
let path = format!("/proc/{pid}/task/{pid}/children");
let Ok(contents) = std::fs::read_to_string(path) else {
continue;
};
for token in contents.split_ascii_whitespace() {
if let Ok(child) = token.parse::<u32>() {
if result.insert(child, pid).is_none() {
stack.push(child);
}
}
}
}
result
}
fn snapshot(root_pid: u32, expected: ProcessIdentity) -> Option<HashMap<u32, u32>> {
let before = process_identity(root_pid);
let descendants = descendant_pids(root_pid);
verified_snapshot(
expected,
before,
descendants,
process_identity(root_pid),
)
}
fn verified_snapshot(
expected: ProcessIdentity,
before: Option<ProcessIdentity>,
descendants: HashMap<u32, u32>,
after: Option<ProcessIdentity>,
) -> Option<HashMap<u32, u32>> {
(before == Some(expected) && after == Some(expected)).then_some(descendants)
}
fn emit_diff(
previous: &HashMap<u32, u32>,
current: &HashMap<u32, u32>,
emit: &dyn Fn(DescendantEvent),
) {
for (&pid, &parent_pid) in current {
if !previous.contains_key(&pid) {
emit(DescendantEvent::Started {
pid,
parent_pid: Some(parent_pid),
});
}
}
for &pid in previous.keys() {
if !current.contains_key(&pid) {
emit(DescendantEvent::Exited(pid));
}
}
}
fn pump_loop_with(
stop: &DescendantMonitorStop,
mut take_snapshot: impl FnMut() -> Option<HashMap<u32, u32>>,
emit: &dyn Fn(DescendantEvent),
mut wait: impl FnMut() -> bool,
) {
let mut known = HashMap::new();
loop {
if stop.is_stopped() {
emit(DescendantEvent::Completed);
return;
}
let Some(current) = take_snapshot() else {
for pid in known.into_keys() {
emit(DescendantEvent::Exited(pid));
}
emit(DescendantEvent::Completed);
return;
};
emit_diff(&known, ¤t, emit);
known = current;
if wait() {
emit(DescendantEvent::Completed);
return;
}
}
}
fn pump_loop(
root_pid: u32,
root_identity: ProcessIdentity,
stop: Arc<DescendantMonitorStop>,
emit: Box<dyn Fn(DescendantEvent) + Send>,
) {
pump_loop_with(
&stop,
|| snapshot(root_pid, root_identity),
emit.as_ref(),
|| stop.wait_timeout(POLL_INTERVAL),
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::mpsc;
fn collect_diff(
previous: &HashMap<u32, u32>,
current: &HashMap<u32, u32>,
) -> Vec<DescendantEvent> {
let (tx, rx) = mpsc::channel();
emit_diff(previous, current, &|event| tx.send(event).unwrap());
drop(tx);
rx.iter().collect()
}
#[test]
fn descendant_sampling_cadence_is_twenty_milliseconds() {
assert_eq!(POLL_INTERVAL, Duration::from_millis(20));
}
#[test]
fn emit_diff_fires_one_started_per_new_pid_with_its_parent() {
let previous = [(10, 1), (20, 10)].into_iter().collect();
let current = [(10, 1), (20, 10), (30, 20), (40, 10)].into_iter().collect();
let events = collect_diff(&previous, ¤t);
let started: HashMap<_, _> = events
.into_iter()
.filter_map(|event| match event {
DescendantEvent::Started { pid, parent_pid } => Some((pid, parent_pid)),
DescendantEvent::Exited(_) | DescendantEvent::Completed => None,
})
.collect();
assert_eq!(
started,
[(30, Some(20)), (40, Some(10))].into_iter().collect()
);
}
#[test]
fn emit_diff_fires_one_exited_per_gone_pid() {
let previous = [(10, 1), (20, 10), (30, 20)].into_iter().collect();
let current = [(10, 1)].into_iter().collect();
let events = collect_diff(&previous, ¤t);
let exited: HashSet<_> = events
.into_iter()
.filter_map(|event| match event {
DescendantEvent::Exited(pid) => Some(pid),
DescendantEvent::Started { .. } | DescendantEvent::Completed => None,
})
.collect();
assert_eq!(exited, [20, 30].into_iter().collect());
}
#[test]
fn emit_diff_no_events_when_steady_state() {
let current = [(10, 1), (20, 10)].into_iter().collect();
assert!(collect_diff(¤t, ¤t).is_empty());
}
#[test]
fn descendant_pids_for_nonexistent_root_returns_empty() {
assert!(descendant_pids(0x7fff_fffe).is_empty());
}
#[test]
fn descendant_pids_for_self_includes_no_phantom_entries() {
assert!(descendant_pids(std::process::id())
.into_iter()
.all(|(pid, parent_pid)| pid > 1 && parent_pid > 0));
}
#[test]
fn parse_start_ticks_handles_spaces_and_parentheses_in_comm() {
let mut fields = vec!["S".to_string()];
fields.extend((4..=21).map(|number| number.to_string()));
fields.push("424242".to_string());
let stat = format!("123 (odd ) process name) {}", fields.join(" "));
assert_eq!(parse_start_ticks(&stat), Some(424242));
}
#[test]
fn identity_mismatch_terminates_pump_without_tracking_reused_pid() {
let stop = DescendantMonitorStop::new();
let (tx, rx) = mpsc::channel();
let mut polls = 0;
pump_loop_with(
&stop,
|| {
polls += 1;
None
},
&|event| tx.send(event).unwrap(),
|| panic!("terminated pump must not wait"),
);
assert_eq!(polls, 1);
assert_eq!(rx.try_recv(), Ok(DescendantEvent::Completed));
assert!(rx.try_recv().is_err());
}
#[test]
fn reused_pid_with_different_start_ticks_rejects_descendant_snapshot() {
let expected = ProcessIdentity { start_ticks: 100 };
let recycled = ProcessIdentity { start_ticks: 200 };
assert_eq!(
verified_snapshot(
expected,
Some(recycled),
[(42, 7)].into_iter().collect(),
Some(recycled),
),
None
);
}
#[test]
fn scripted_normal_pump_emits_descendant_start_and_exit() {
let stop = DescendantMonitorStop::new();
let (tx, rx) = mpsc::channel();
let mut snapshots = [
Some([(42, 7)].into_iter().collect()),
Some(HashMap::new()),
None,
]
.into_iter();
pump_loop_with(
&stop,
|| snapshots.next().flatten(),
&|event| tx.send(event).unwrap(),
|| false,
);
drop(tx);
assert_eq!(
rx.iter().collect::<Vec<_>>(),
[
DescendantEvent::Started {
pid: 42,
parent_pid: Some(7),
},
DescendantEvent::Exited(42),
DescendantEvent::Completed,
]
);
}
#[test]
fn stop_wakes_waiting_pump_without_polling() {
let stop = Arc::new(DescendantMonitorStop::new());
let pump_stop = Arc::clone(&stop);
let (waiting_tx, waiting_rx) = mpsc::channel();
let (done_tx, done_rx) = mpsc::channel();
let (event_tx, event_rx) = mpsc::channel();
let pump = std::thread::spawn(move || {
let mut announced = false;
pump_loop_with(
&pump_stop,
|| {
if !announced {
announced = true;
waiting_tx.send(()).unwrap();
}
Some(HashMap::new())
},
&|event| event_tx.send(event).unwrap(),
|| pump_stop.wait_timeout(Duration::from_secs(30)),
);
done_tx.send(()).unwrap();
});
waiting_rx.recv_timeout(Duration::from_secs(1)).unwrap();
stop.stop();
done_rx.recv_timeout(Duration::from_millis(250)).unwrap();
pump.join().unwrap();
assert_eq!(event_rx.try_recv(), Ok(DescendantEvent::Completed));
assert!(event_rx.try_recv().is_err());
}
}