use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use crate::platform::process::{DescendantEvent, DescendantMonitorStop, ProcessSnapshot};
const RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
const STOP_EVENT_IDENT: libc::uintptr_t = libc::uintptr_t::MAX;
type Identity = (u64, u64);
struct Kqueue(libc::c_int);
impl Drop for Kqueue {
fn drop(&mut self) {
unsafe { libc::close(self.0) };
}
}
pub fn start_descendant_monitor(
root_pid: u32,
stop: Arc<DescendantMonitorStop>,
emit: Box<dyn Fn(DescendantEvent) + Send>,
) -> std::io::Result<()> {
let Some(identity) = process_identity(root_pid) else {
emit(DescendantEvent::Completed);
return Ok(());
};
std::thread::Builder::new()
.name("rp-macos-descpump".to_string())
.spawn(move || pump_loop(root_pid, identity, stop, emit))
.map(|_| ())
.map_err(|error| std::io::Error::other(format!("spawn descendant monitor: {error}")))
}
fn process_identity(pid: u32) -> Option<Identity> {
super::process_snapshot_for_pid(pid)
.map(|snapshot| (snapshot.start_time_a, snapshot.start_time_b))
}
fn descendants(root_pid: u32) -> HashMap<u32, u32> {
descendants_of(root_pid, &super::process_snapshot())
}
fn descendants_of(root_pid: u32, snapshots: &[ProcessSnapshot]) -> HashMap<u32, u32> {
let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
for snapshot in snapshots {
children
.entry(snapshot.parent_pid)
.or_default()
.push(snapshot.pid);
}
let mut result = HashMap::new();
let mut stack = vec![root_pid];
while let Some(pid) = stack.pop() {
if let Some(child_pids) = children.get(&pid) {
for &child in child_pids {
if result.insert(child, pid).is_none() {
stack.push(child);
}
}
}
}
result
}
fn snapshot(root_pid: u32, expected: Identity) -> Option<HashMap<u32, u32>> {
let before = process_identity(root_pid);
let descendants = descendants(root_pid);
verified_snapshot(expected, before, descendants, process_identity(root_pid))
}
fn verified_snapshot(
expected: Identity,
before: Option<Identity>,
descendants: HashMap<u32, u32>,
after: Option<Identity>,
) -> Option<HashMap<u32, u32>> {
(before == Some(expected) && after == Some(expected)).then_some(descendants)
}
fn pump_loop(
root_pid: u32,
root_identity: Identity,
stop: Arc<DescendantMonitorStop>,
emit: Box<dyn Fn(DescendantEvent) + Send>,
) {
let queue = unsafe { libc::kqueue() };
let queue = (queue >= 0).then(|| Arc::new(Kqueue(queue)));
if let Some(queue) = queue.as_ref() {
register_stop_event(queue.0);
let notifier_queue = Arc::clone(queue);
let notifier_stop = Arc::clone(&stop);
let _ = std::thread::Builder::new()
.name("rp-macos-kqueue-stop".to_owned())
.spawn(move || {
while !notifier_stop.wait_timeout(Duration::from_secs(24 * 60 * 60)) {}
trigger_stop_event(notifier_queue.0);
});
}
let mut known = HashMap::new();
loop {
if stop.is_stopped() {
emit(DescendantEvent::Completed);
return;
}
let Some(current) = snapshot(root_pid, root_identity) else {
for pid in known.into_keys() {
emit(DescendantEvent::Exited(pid));
}
stop.stop();
emit(DescendantEvent::Completed);
return;
};
for (&pid, &parent_pid) in ¤t {
if !known.contains_key(&pid) {
emit(DescendantEvent::Started {
pid,
parent_pid: Some(parent_pid),
});
}
}
for &pid in known.keys() {
if !current.contains_key(&pid) {
emit(DescendantEvent::Exited(pid));
}
}
if let Some(queue) = queue.as_ref() {
register_process_hint(queue.0, root_pid);
for &pid in current.keys() {
register_process_hint(queue.0, pid);
}
}
known = current;
if wait_for_hint(queue.as_ref(), &stop) {
emit(DescendantEvent::Completed);
return;
}
}
}
fn register_stop_event(queue: libc::c_int) {
let change = libc::kevent {
ident: STOP_EVENT_IDENT,
filter: libc::EVFILT_USER,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: 0,
data: 0,
udata: std::ptr::null_mut(),
};
unsafe {
libc::kevent(
queue,
&raw const change,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
);
}
}
fn trigger_stop_event(queue: libc::c_int) {
let change = libc::kevent {
ident: STOP_EVENT_IDENT,
filter: libc::EVFILT_USER,
flags: 0,
fflags: libc::NOTE_TRIGGER,
data: 0,
udata: std::ptr::null_mut(),
};
unsafe {
libc::kevent(
queue,
&raw const change,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
);
}
}
fn register_process_hint(queue: libc::c_int, pid: u32) {
let change = libc::kevent {
ident: pid as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_ENABLE | libc::EV_CLEAR,
fflags: libc::NOTE_FORK | libc::NOTE_EXEC | libc::NOTE_EXIT,
data: 0,
udata: std::ptr::null_mut(),
};
unsafe {
libc::kevent(
queue,
&raw const change,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
);
}
}
fn wait_for_hint(queue: Option<&Arc<Kqueue>>, stop: &DescendantMonitorStop) -> bool {
let Some(queue) = queue else {
return stop.wait_timeout(RECONCILE_INTERVAL);
};
let timeout = libc::timespec {
tv_sec: 0,
tv_nsec: RECONCILE_INTERVAL.as_nanos() as libc::c_long,
};
let mut event: libc::kevent = unsafe { std::mem::zeroed() };
unsafe {
libc::kevent(
queue.0,
std::ptr::null(),
0,
&raw mut event,
1,
&raw const timeout,
);
}
stop.is_stopped()
}
#[cfg(test)]
mod tests {
use super::*;
fn process(pid: u32, parent_pid: u32, start_time_b: u64) -> ProcessSnapshot {
ProcessSnapshot {
pid,
parent_pid,
start_time_a: 100,
start_time_b,
}
}
fn descendants_if_root_matches(
root_pid: u32,
expected: Identity,
snapshots: &[ProcessSnapshot],
) -> Option<HashMap<u32, u32>> {
snapshots
.iter()
.find(|snapshot| snapshot.pid == root_pid)
.and_then(|snapshot| {
((snapshot.start_time_a, snapshot.start_time_b) == expected)
.then(|| descendants_of(root_pid, snapshots))
})
}
#[test]
fn descendants_of_handles_branching_tree() {
let snapshots = [
process(100, 0, 1),
process(200, 100, 2),
process(201, 200, 3),
process(300, 100, 4),
process(999, 1, 5),
];
assert_eq!(
descendants_of(100, &snapshots),
[(200, 100), (201, 200), (300, 100)].into_iter().collect()
);
}
#[test]
fn descendants_of_for_unknown_root_returns_empty() {
let snapshots = [process(100, 0, 1), process(200, 100, 2)];
assert!(descendants_of(0x7fff_fffe, &snapshots).is_empty());
}
#[test]
fn list_all_processes_returns_non_empty_on_real_macos() {
let snapshots = super::super::process_snapshot();
assert!(snapshots.len() > 5);
assert!(snapshots
.iter()
.any(|snapshot| snapshot.pid == std::process::id()));
}
#[test]
fn reused_root_pid_identity_mismatch_terminates_snapshot() {
let expected = (100, 1);
let recycled = [process(100, 0, 99), process(200, 100, 2)];
assert_eq!(descendants_if_root_matches(100, expected, &recycled), None);
}
#[test]
fn root_identity_change_after_walk_rejects_mixed_snapshot() {
let expected = (100, 1);
let recycled = (100, 99);
assert_eq!(
verified_snapshot(
expected,
Some(expected),
[(42, 7)].into_iter().collect(),
Some(recycled),
),
None
);
}
}