1use std::io;
23
24#[derive(Clone, Debug, Eq, PartialEq)]
26#[non_exhaustive]
27pub struct ProcessEntry {
28 pub pid: u32,
30 pub parent_pid: u32,
32 pub command_line: Option<String>,
36 pub working_set_bytes: Option<u64>,
38}
39
40pub fn process_table() -> io::Result<Vec<ProcessEntry>> {
44 imp::table()
45}
46
47#[cfg(any(target_os = "linux", target_os = "android", test))]
50fn parse_stat_parent(stat: &str) -> Option<u32> {
51 let rest = &stat[stat.rfind(')')? + 1..];
52 rest.split_whitespace().nth(1)?.parse().ok()
54}
55
56#[cfg(any(target_os = "linux", target_os = "android", test))]
58fn join_cmdline(bytes: &[u8]) -> Option<String> {
59 let trimmed = bytes.strip_suffix(&[0]).unwrap_or(bytes);
60 if trimmed.is_empty() {
61 return None;
62 }
63 let text = String::from_utf8_lossy(trimmed);
64 let joined = text.split('\0').collect::<Vec<_>>().join(" ");
65 (!joined.trim().is_empty()).then_some(joined)
66}
67
68#[cfg(any(target_os = "macos", test))]
71fn join_procargs2(buffer: &[u8]) -> Option<String> {
72 let count = usize::try_from(i32::from_ne_bytes(buffer.get(..4)?.try_into().ok()?)).ok()?;
73 let mut i = 4;
74 while i < buffer.len() && buffer[i] != 0 {
75 i += 1;
76 }
77 while i < buffer.len() && buffer[i] == 0 {
78 i += 1;
79 }
80 let mut args = Vec::with_capacity(count);
81 for _ in 0..count {
82 if i >= buffer.len() {
83 return None;
84 }
85 let start = i;
86 while i < buffer.len() && buffer[i] != 0 {
87 i += 1;
88 }
89 if i >= buffer.len() {
90 return None; }
92 args.push(String::from_utf8_lossy(&buffer[start..i]).into_owned());
93 i += 1;
94 }
95 let joined = args.join(" ");
96 (!joined.is_empty()).then_some(joined)
97}
98
99#[cfg(any(target_os = "linux", target_os = "android"))]
100mod imp {
101 use super::{join_cmdline, parse_stat_parent, ProcessEntry};
102 use std::io;
103
104 pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
105 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64;
107 let mut rows = Vec::new();
108 for entry in std::fs::read_dir("/proc")?.flatten() {
109 let Some(pid) = entry
110 .file_name()
111 .to_str()
112 .and_then(|name| name.parse::<u32>().ok())
113 else {
114 continue;
115 };
116 let Some(parent_pid) = std::fs::read(format!("/proc/{pid}/stat"))
119 .ok()
120 .and_then(|stat| parse_stat_parent(&String::from_utf8_lossy(&stat)))
121 else {
122 continue;
123 };
124 let working_set_bytes = std::fs::read_to_string(format!("/proc/{pid}/statm"))
125 .ok()
126 .and_then(|statm| statm.split_whitespace().nth(1)?.parse::<u64>().ok())
127 .map(|pages| pages.saturating_mul(page));
128 let command_line = std::fs::read(format!("/proc/{pid}/cmdline"))
129 .ok()
130 .and_then(|bytes| join_cmdline(&bytes));
131 rows.push(ProcessEntry {
132 pid,
133 parent_pid,
134 command_line,
135 working_set_bytes,
136 });
137 }
138 Ok(rows)
139 }
140}
141
142#[cfg(target_os = "macos")]
143mod imp {
144 use super::{join_procargs2, ProcessEntry};
145 use std::io;
146
147 pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
148 const PROC_ALL_PIDS: u32 = 1;
150 let size = std::mem::size_of::<libc::pid_t>();
151 let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
153 if needed <= 0 {
154 return Err(io::Error::last_os_error());
155 }
156 let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
157 let written = unsafe {
159 libc::proc_listpids(
160 PROC_ALL_PIDS,
161 0,
162 pids.as_mut_ptr().cast(),
163 (pids.len() * size) as libc::c_int,
164 )
165 };
166 if written <= 0 {
167 return Err(io::Error::last_os_error());
168 }
169 pids.truncate(written as usize / size);
170 let mut rows = Vec::new();
171 for pid in pids.into_iter().filter(|pid| *pid > 0) {
172 let Some(parent_pid) = parent_of(pid) else {
173 continue; };
175 rows.push(ProcessEntry {
176 pid: pid as u32,
177 parent_pid,
178 command_line: command_line(pid as u32),
179 working_set_bytes: resident(pid),
180 });
181 }
182 Ok(rows)
183 }
184
185 fn parent_of(pid: libc::pid_t) -> Option<u32> {
186 let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
188 let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
189 let written = unsafe {
191 libc::proc_pidinfo(
192 pid,
193 libc::PROC_PIDTBSDINFO,
194 0,
195 (&mut info as *mut libc::proc_bsdinfo).cast(),
196 size,
197 )
198 };
199 (written == size && info.pbi_pid == pid as u32).then_some(info.pbi_ppid)
200 }
201
202 fn resident(pid: libc::pid_t) -> Option<u64> {
203 let mut info: libc::proc_taskinfo = unsafe { std::mem::zeroed() };
205 let size = std::mem::size_of::<libc::proc_taskinfo>() as libc::c_int;
206 let written = unsafe {
208 libc::proc_pidinfo(
209 pid,
210 libc::PROC_PIDTASKINFO,
211 0,
212 (&mut info as *mut libc::proc_taskinfo).cast(),
213 size,
214 )
215 };
216 (written == size).then_some(info.pti_resident_size)
217 }
218
219 fn command_line(pid: u32) -> Option<String> {
220 let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int];
221 let mut capacity: usize = 0;
222 let probed = unsafe {
224 libc::sysctl(
225 name.as_mut_ptr(),
226 3,
227 std::ptr::null_mut(),
228 &mut capacity,
229 std::ptr::null_mut(),
230 0,
231 )
232 };
233 if probed != 0 {
234 return None;
235 }
236 let mut buffer = vec![0u8; capacity + 4096];
237 let mut length = buffer.len();
238 let read = unsafe {
240 libc::sysctl(
241 name.as_mut_ptr(),
242 3,
243 buffer.as_mut_ptr().cast(),
244 &mut length,
245 std::ptr::null_mut(),
246 0,
247 )
248 };
249 if read != 0 {
250 return None;
251 }
252 buffer.truncate(length);
253 join_procargs2(&buffer)
254 }
255}
256
257#[cfg(windows)]
258mod imp {
259 use super::ProcessEntry;
260 use crate::process_windows::os_error;
261 use std::{ffi::c_void, io};
262 use windows::Win32::{
263 Foundation::{CloseHandle, HANDLE},
264 System::{
265 Diagnostics::ToolHelp::{
266 CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
267 TH32CS_SNAPPROCESS,
268 },
269 ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS},
270 Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION},
271 },
272 };
273
274 #[link(name = "ntdll")]
275 extern "system" {
276 fn NtQueryInformationProcess(
277 process: *mut c_void,
278 class: u32,
279 information: *mut c_void,
280 length: u32,
281 returned: *mut u32,
282 ) -> i32;
283 }
284
285 const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
287 const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
288 const STATUS_BUFFER_OVERFLOW: u32 = 0x8000_0005;
289 const STATUS_BUFFER_TOO_SMALL: u32 = 0xC000_0023;
290
291 #[repr(C)]
292 struct UnicodeString {
293 length: u16,
294 maximum_length: u16,
295 buffer: *const u16,
296 }
297
298 struct Owned(HANDLE);
299 impl Drop for Owned {
300 fn drop(&mut self) {
301 let _ = unsafe { CloseHandle(self.0) };
303 }
304 }
305
306 pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
307 let snapshot =
309 Owned(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?);
310 let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
312 entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
313 unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
315 let mut rows = Vec::new();
316 loop {
317 let pid = entry.th32ProcessID;
318 let (command_line, working_set_bytes) = if pid == 0 {
319 (None, None) } else {
321 read_process(pid)
322 };
323 rows.push(ProcessEntry {
324 pid,
325 parent_pid: entry.th32ParentProcessID,
326 command_line,
327 working_set_bytes,
328 });
329 if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
331 break;
332 }
333 }
334 Ok(rows)
335 }
336
337 fn read_process(pid: u32) -> (Option<String>, Option<u64>) {
340 let Ok(handle) = (unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) })
342 else {
343 return (None, None);
344 };
345 let process = Owned(handle);
346 (command_line(&process), working_set(&process))
347 }
348
349 fn working_set(process: &Owned) -> Option<u64> {
350 let mut counters = PROCESS_MEMORY_COUNTERS::default();
351 let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
352 counters.cb = size;
353 unsafe { GetProcessMemoryInfo(process.0, &mut counters, size) }.ok()?;
355 Some(counters.WorkingSetSize as u64)
356 }
357
358 fn command_line(process: &Owned) -> Option<String> {
359 let mut size: usize = 1024;
360 for _ in 0..4 {
361 let mut buffer = vec![0u64; size.div_ceil(8)];
362 let mut returned = 0u32;
363 let status = unsafe {
365 NtQueryInformationProcess(
366 process.0 .0,
367 PROCESS_COMMAND_LINE_INFORMATION,
368 buffer.as_mut_ptr().cast(),
369 (buffer.len() * 8) as u32,
370 &mut returned,
371 )
372 } as u32;
373 match status {
374 0 => return read_unicode_string(&buffer),
375 STATUS_INFO_LENGTH_MISMATCH | STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL => {
376 size = (returned as usize).max(size * 2);
377 }
378 _ => return None, }
380 }
381 None
382 }
383
384 fn read_unicode_string(buffer: &[u64]) -> Option<String> {
385 let base = buffer.as_ptr() as usize;
386 let end = base + buffer.len() * 8;
387 if buffer.len() * 8 < std::mem::size_of::<UnicodeString>() {
388 return None;
389 }
390 let header = unsafe { &*(buffer.as_ptr().cast::<UnicodeString>()) };
392 let text = header.buffer as usize;
393 let bytes = usize::from(header.length);
394 if bytes == 0 {
395 return None;
396 }
397 if text < base + std::mem::size_of::<UnicodeString>()
399 || !text.is_multiple_of(2)
400 || !bytes.is_multiple_of(2)
401 || text + bytes > end
402 {
403 return None;
404 }
405 let units = unsafe { std::slice::from_raw_parts(header.buffer, bytes / 2) };
407 Some(String::from_utf16_lossy(units))
408 }
409}
410
411#[cfg(not(any(
412 target_os = "linux",
413 target_os = "android",
414 target_os = "macos",
415 windows
416)))]
417mod imp {
418 use super::ProcessEntry;
419 use std::io;
420
421 pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
422 Err(io::Error::from(io::ErrorKind::Unsupported))
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn stat_parent_survives_spaces_and_parens_in_the_name() {
432 assert_eq!(parse_stat_parent("42 (bash) S 7 42 42 0 -1"), Some(7));
433 assert_eq!(parse_stat_parent("42 (a b) c) R 9 1 1"), Some(9));
434 assert_eq!(parse_stat_parent("garbage"), None);
435 assert_eq!(parse_stat_parent("42 (x) S"), None);
436 }
437
438 #[test]
439 fn cmdline_joins_arguments_and_empty_is_none() {
440 assert_eq!(
441 join_cmdline(b"ls\0-l\0/tmp\0").as_deref(),
442 Some("ls -l /tmp")
443 );
444 assert_eq!(join_cmdline(b"sd-server\0").as_deref(), Some("sd-server"));
445 assert_eq!(join_cmdline(b""), None);
446 assert_eq!(join_cmdline(b"\0"), None);
447 }
448
449 #[test]
450 fn procargs2_yields_arguments_without_environment() {
451 let mut buffer = 2i32.to_ne_bytes().to_vec();
452 buffer.extend_from_slice(b"/bin/tool\0\0\0tool\0--flag\0HOME=/x\0");
453 assert_eq!(join_procargs2(&buffer).as_deref(), Some("tool --flag"));
454 assert_eq!(join_procargs2(&3i32.to_ne_bytes()), None);
455 let mut cut = 1i32.to_ne_bytes().to_vec();
456 cut.extend_from_slice(b"/bin/tool\0tool");
457 assert_eq!(join_procargs2(&cut), None);
458 }
459
460 #[cfg(unix)]
462 fn own_parent() -> u32 {
463 unsafe { libc::getppid() as u32 }
465 }
466
467 #[cfg(any(target_os = "linux", target_os = "macos", windows))]
468 #[test]
469 fn the_table_contains_this_process_with_its_parent_and_memory() {
470 let rows = process_table().expect("process table");
471 let me = rows
472 .iter()
473 .find(|row| row.pid == std::process::id())
474 .expect("own pid is listed");
475 assert_ne!(me.parent_pid, std::process::id());
476 #[cfg(unix)]
477 assert_eq!(me.parent_pid, own_parent());
478 assert!(me.working_set_bytes.unwrap_or(0) > 0);
479 let line = me.command_line.as_deref().expect("own command line");
480 assert!(!line.is_empty());
481 #[cfg(unix)]
482 {
483 let argv: Vec<String> = std::env::args_os()
484 .map(|arg| arg.to_string_lossy().into_owned())
485 .collect();
486 assert_eq!(line, argv.join(" "));
487 }
488 let exe = std::env::current_exe().unwrap();
489 let stem = exe.file_stem().unwrap().to_string_lossy().into_owned();
490 assert!(line.contains(&stem), "{line} should name {stem}");
491 }
492
493 #[cfg(any(target_os = "linux", target_os = "macos", windows))]
494 #[test]
495 fn a_spawned_child_is_listed_under_this_parent_with_its_arguments() {
496 use std::process::{Command, Stdio};
497 const MARK: &str = "7654";
500 let mut command = if cfg!(windows) {
501 let mut c = Command::new("ping.exe");
502 c.args(["-n", MARK, "127.0.0.1"]);
503 c
504 } else {
505 let mut c = Command::new("sleep");
506 c.arg(MARK);
507 c
508 };
509 let mut child = command
510 .stdin(Stdio::null())
511 .stdout(Stdio::null())
512 .stderr(Stdio::null())
513 .spawn()
514 .expect("spawn");
515 let pid = child.id();
516 let mut found = None;
517 for _ in 0..50 {
518 found = process_table().unwrap().into_iter().find(|row| {
519 row.pid == pid
520 && row
521 .command_line
522 .as_deref()
523 .is_some_and(|line| line.contains(MARK))
524 });
525 if found.is_some() {
526 break;
527 }
528 std::thread::sleep(std::time::Duration::from_millis(100));
529 }
530 let _ = child.kill();
531 let _ = child.wait();
532 let row = found.expect("child listed with its command line");
533 assert_eq!(row.parent_pid, std::process::id());
534 let line = row.command_line.unwrap();
535 let expected = if cfg!(windows) {
536 format!("-n {MARK} 127.0.0.1")
537 } else {
538 format!("sleep {MARK}")
539 };
540 assert!(line.contains(&expected), "{line} should contain {expected}");
541 }
542}