1use super::ChildProcessInfo;
4use std::collections::{HashSet, VecDeque};
5use std::ffi::c_void;
6
7const PROC_PIDT_SHORTBSDINFO: libc::c_int = 13;
8const PROC_PIDTASKINFO: libc::c_int = 4;
9const MAXCOMLEN: usize = 16;
10const SIDL: u32 = 1;
11const SRUN: u32 = 2;
12const SSTOP: u32 = 4;
13const SZOMB: u32 = 5;
14
15#[repr(C)]
16struct ProcBsdShortInfo {
17 pid: u32,
18 parent_pid: u32,
19 process_group_id: u32,
20 status: u32,
21 command: [libc::c_char; MAXCOMLEN],
22 flags: u32,
23 uid: libc::uid_t,
24 gid: libc::gid_t,
25 real_uid: libc::uid_t,
26 real_gid: libc::gid_t,
27 saved_uid: libc::uid_t,
28 saved_gid: libc::gid_t,
29 reserved: u32,
30}
31
32#[repr(C)]
33struct ProcTaskInfo {
34 virtual_size: u64,
35 resident_size: u64,
36 total_user_time: u64,
37 total_system_time: u64,
38 threads_user_time: u64,
39 threads_system_time: u64,
40 policy: i32,
41 faults: i32,
42 pageins: i32,
43 cow_faults: i32,
44 messages_sent: i32,
45 messages_received: i32,
46 mach_syscalls: i32,
47 unix_syscalls: i32,
48 context_switches: i32,
49 thread_count: i32,
50 running_thread_count: i32,
51 priority: i32,
52}
53
54#[link(name = "proc")]
55unsafe extern "C" {
56 fn proc_listchildpids(pid: libc::pid_t, buffer: *mut c_void, buffersize: i32) -> i32;
57 fn proc_pidinfo(
58 pid: libc::pid_t,
59 flavor: libc::c_int,
60 arg: u64,
61 buffer: *mut c_void,
62 buffersize: libc::c_int,
63 ) -> libc::c_int;
64}
65
66pub fn child_pid_entry_count(bytes_written: i32) -> Option<usize> {
67 let bytes = usize::try_from(bytes_written).ok()?;
68 let pid_width = std::mem::size_of::<libc::pid_t>();
69 Some(bytes / pid_width)
70}
71
72fn descendant_pid_signature(descendants: &[u32]) -> u64 {
73 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
74 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
75
76 descendants.iter().fold(FNV_OFFSET, |signature, &pid| {
77 pid.to_le_bytes().iter().fold(signature, |sig, &byte| {
78 (sig ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
79 })
80 })
81}
82
83const fn qualifies_libproc_status(status: u32) -> bool {
84 !matches!(status, SIDL | SSTOP | SZOMB)
85}
86
87const fn libproc_state_indicates_current_activity(
88 status: u32,
89 cpu_time_ms: u64,
90 num_running_threads: i32,
91) -> bool {
92 status == SRUN && cpu_time_ms > 0 && num_running_threads > 0
93}
94
95fn call_proc_listchildpids(pid: libc::pid_t, capacity: usize) -> Option<(i32, Vec<libc::pid_t>)> {
96 let byte_len = capacity.checked_mul(std::mem::size_of::<libc::pid_t>())?;
97 let buffer_size = i32::try_from(byte_len).ok()?;
98 let mut buffer = vec![libc::pid_t::default(); capacity];
99 let bytes_written =
100 unsafe { proc_listchildpids(pid, buffer.as_mut_ptr().cast::<c_void>(), buffer_size) };
101 Some((bytes_written, buffer))
102}
103
104fn collect_child_pids_from_buffer(buffer: Vec<libc::pid_t>, count: usize) -> Vec<u32> {
105 buffer
106 .into_iter()
107 .take(count)
108 .filter_map(|p| u32::try_from(p).ok())
109 .collect()
110}
111
112fn try_read_child_pids(pid: libc::pid_t, capacity: usize) -> Option<Result<Vec<u32>, usize>> {
113 let (bytes_written, buffer) = call_proc_listchildpids(pid, capacity)?;
114 if bytes_written < 0 {
115 return None;
116 }
117 if bytes_written == 0 {
118 return Some(Ok(Vec::new()));
119 }
120 let count = child_pid_entry_count(bytes_written)?;
121 if count < capacity {
122 return Some(Ok(collect_child_pids_from_buffer(buffer, count)));
123 }
124 Some(Err(capacity.checked_mul(2)?))
125}
126
127fn list_child_pids(parent_pid: u32) -> Option<Vec<u32>> {
128 let pid = libc::pid_t::try_from(parent_pid).ok()?;
129 let mut capacity: usize = 32;
130
131 loop {
132 match try_read_child_pids(pid, capacity)? {
133 Ok(pids) => return Some(pids),
134 Err(new_capacity) => capacity = new_capacity,
135 }
136 }
137}
138
139fn fetch_bsd_short_info(pid: u32) -> Option<ProcBsdShortInfo> {
140 let mut info = ProcBsdShortInfo {
141 pid: 0,
142 parent_pid: 0,
143 process_group_id: 0,
144 status: 0,
145 command: [0; MAXCOMLEN],
146 flags: 0,
147 uid: 0,
148 gid: 0,
149 real_uid: 0,
150 real_gid: 0,
151 saved_uid: 0,
152 saved_gid: 0,
153 reserved: 0,
154 };
155 let pid = libc::pid_t::try_from(pid).ok()?;
156 let expected = i32::try_from(std::mem::size_of::<ProcBsdShortInfo>()).ok()?;
157 let bytes = unsafe {
158 proc_pidinfo(
159 pid,
160 PROC_PIDT_SHORTBSDINFO,
161 0,
162 (&raw mut info).cast::<c_void>(),
163 expected,
164 )
165 };
166 (bytes == expected).then_some(info)
167}
168
169fn fetch_task_info(pid: u32) -> Option<ProcTaskInfo> {
170 let mut info = ProcTaskInfo {
171 virtual_size: 0,
172 resident_size: 0,
173 total_user_time: 0,
174 total_system_time: 0,
175 threads_user_time: 0,
176 threads_system_time: 0,
177 policy: 0,
178 faults: 0,
179 pageins: 0,
180 cow_faults: 0,
181 messages_sent: 0,
182 messages_received: 0,
183 mach_syscalls: 0,
184 unix_syscalls: 0,
185 context_switches: 0,
186 thread_count: 0,
187 running_thread_count: 0,
188 priority: 0,
189 };
190 let pid = libc::pid_t::try_from(pid).ok()?;
191 let expected = i32::try_from(std::mem::size_of::<ProcTaskInfo>()).ok()?;
192 let bytes = unsafe {
193 proc_pidinfo(
194 pid,
195 PROC_PIDTASKINFO,
196 0,
197 (&raw mut info).cast::<c_void>(),
198 expected,
199 )
200 };
201 (bytes == expected).then_some(info)
202}
203
204fn process_child_pids(
205 child_pids: Vec<u32>,
206 descendants: &mut Vec<u32>,
207 visited: &mut HashSet<u32>,
208 queue: &mut VecDeque<u32>,
209) {
210 child_pids
211 .into_iter()
212 .filter(|&p| visited.insert(p))
213 .for_each(|p| {
214 descendants.push(p);
215 queue.push_back(p);
216 });
217}
218
219fn drain_pid_queue(
220 queue: &mut VecDeque<u32>,
221 descendants: &mut Vec<u32>,
222 visited: &mut HashSet<u32>,
223) -> Option<()> {
224 while let Some(pid) = queue.pop_front() {
225 let child_pids = list_child_pids(pid)?;
226 process_child_pids(child_pids, descendants, visited, queue);
227 }
228 Some(())
229}
230
231fn collect_descendant_pids(current_pid: u32) -> Option<Vec<u32>> {
232 let mut descendants = Vec::new();
233 let mut visited = HashSet::new();
234 let mut queue = VecDeque::from([current_pid]);
235 drain_pid_queue(&mut queue, &mut descendants, &mut visited)?;
236 descendants.sort_unstable();
237 Some(descendants)
238}
239
240struct DescendantTally {
241 child_count: u32,
242 active_child_count: u32,
243 total_cpu_ms: u64,
244 qualifying_pids: Vec<u32>,
245}
246
247fn pid_qualifies_for_parent(bsd_info: &ProcBsdShortInfo, parent_pid: u32) -> bool {
248 bsd_info.process_group_id == parent_pid && qualifies_libproc_status(bsd_info.status)
249}
250
251fn cpu_time_ms_from_task(task_info: &Option<ProcTaskInfo>) -> u64 {
252 task_info.as_ref().map_or(0, |info| {
253 (info.total_user_time + info.total_system_time) / 1_000_000
254 })
255}
256
257fn running_threads_from_task(task_info: &Option<ProcTaskInfo>) -> i32 {
258 task_info
259 .as_ref()
260 .map_or(0, |info| info.running_thread_count)
261}
262
263fn tally_one_descendant(pid: u32, parent_pid: u32, tally: &mut DescendantTally) {
264 let Some(bsd_info) = fetch_bsd_short_info(pid) else {
265 return;
266 };
267 if !pid_qualifies_for_parent(&bsd_info, parent_pid) {
268 return;
269 }
270 let task_info = fetch_task_info(pid);
271 let cpu_time_ms = cpu_time_ms_from_task(&task_info);
272 let num_running_threads = running_threads_from_task(&task_info);
273 tally.child_count = tally.child_count.saturating_add(1);
274 tally.total_cpu_ms += cpu_time_ms;
275 if libproc_state_indicates_current_activity(bsd_info.status, cpu_time_ms, num_running_threads) {
276 tally.active_child_count = tally.active_child_count.saturating_add(1);
277 }
278 tally.qualifying_pids.push(pid);
279}
280
281fn build_descendant_tally(parent_pid: u32, descendants: &[u32]) -> DescendantTally {
282 let mut tally = DescendantTally {
283 child_count: 0,
284 active_child_count: 0,
285 total_cpu_ms: 0,
286 qualifying_pids: Vec::new(),
287 };
288 descendants
289 .iter()
290 .for_each(|&pid| tally_one_descendant(pid, parent_pid, &mut tally));
291 tally
292}
293
294fn tally_to_child_info(tally: DescendantTally) -> ChildProcessInfo {
295 ChildProcessInfo {
296 child_count: tally.child_count,
297 active_child_count: tally.active_child_count,
298 cpu_time_ms: tally.total_cpu_ms,
299 descendant_pid_signature: descendant_pid_signature(&tally.qualifying_pids),
300 }
301}
302
303fn compute_child_info_from_descendants(
304 parent_pid: u32,
305 descendants: &[u32],
306) -> Option<ChildProcessInfo> {
307 if descendants.is_empty() {
308 return None;
309 }
310 let tally = build_descendant_tally(parent_pid, descendants);
311 if tally.child_count == 0 {
312 return Some(ChildProcessInfo::NONE);
313 }
314 Some(tally_to_child_info(tally))
315}
316
317pub fn child_info_from_libproc(parent_pid: u32) -> Option<ChildProcessInfo> {
318 let descendants = collect_descendant_pids(parent_pid)?;
319 compute_child_info_from_descendants(parent_pid, &descendants)
320}