Skip to main content

proc_cli/core/
process.rs

1//! Cross-platform process abstraction
2//!
3//! Provides a unified interface for discovering and managing processes
4//! across macOS, Linux, and Windows.
5
6use crate::core::name_match::{classify, Hit, NameMatch, Names};
7use crate::error::{ProcError, Result};
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10use std::time::Duration;
11#[cfg(windows)]
12use sysinfo::Users;
13
14/// Runs `refresh`, a sysinfo process refresh, so that no two run at once.
15///
16/// On FreeBSD libkvm reads a process's command line and environment into a
17/// buffer kept in static variables (`kvm_argv` in lib/libkvm/kvm_proc.c), one
18/// for the whole program, so two threads refreshing at the same time read each
19/// other's half-written arrays and crash (seen in the parallel test suite). A
20/// program-wide lock makes that impossible. And libprocstat, which sysinfo asks
21/// for each process's files, prints `kinfo_getfile(): No such process` to
22/// stderr for a process that exits while it reads (lib/libprocstat/libprocstat.c),
23/// so stderr points at /dev/null while the refresh runs. Elsewhere this only
24/// calls `refresh`.
25pub(crate) fn sysinfo_locked<T>(refresh: impl FnOnce() -> T) -> T {
26    #[cfg(target_os = "freebsd")]
27    {
28        static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
29        let _held = LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
30        let _quiet = QuietStderr::new();
31        refresh()
32    }
33    #[cfg(not(target_os = "freebsd"))]
34    refresh()
35}
36
37/// Stderr pointed at /dev/null until dropped (FreeBSD, see [`sysinfo_locked`]).
38/// If either descriptor can't be had, stderr is left as it is.
39#[cfg(target_os = "freebsd")]
40struct QuietStderr {
41    saved: Option<libc::c_int>,
42}
43
44#[cfg(target_os = "freebsd")]
45impl QuietStderr {
46    fn new() -> Self {
47        // SAFETY: plain descriptor calls; every descriptor opened here is
48        // closed, and stderr is restored in Drop
49        unsafe {
50            let saved = libc::dup(libc::STDERR_FILENO);
51            if saved < 0 {
52                return QuietStderr { saved: None };
53            }
54            let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
55            if null < 0 {
56                libc::close(saved);
57                return QuietStderr { saved: None };
58            }
59            libc::dup2(null, libc::STDERR_FILENO);
60            libc::close(null);
61            QuietStderr { saved: Some(saved) }
62        }
63    }
64}
65
66#[cfg(target_os = "freebsd")]
67impl Drop for QuietStderr {
68    fn drop(&mut self) {
69        if let Some(saved) = self.saved {
70            // SAFETY: `saved` is the descriptor `new` duplicated stderr into
71            unsafe {
72                libc::dup2(saved, libc::STDERR_FILENO);
73                libc::close(saved);
74            }
75        }
76    }
77}
78
79/// Each running process's name, by PID, as [`Process`] records it (one refresh
80/// of exactly those PIDs); a PID that is gone is missing from the map. For
81/// socket tables whose own name column is cut short (FreeBSD's sockstat keeps
82/// 10 characters)
83#[cfg(any(target_os = "freebsd", target_os = "netbsd", test))]
84pub(crate) fn names_by_pid(pids: &[u32]) -> std::collections::HashMap<u32, String> {
85    let wanted: Vec<Pid> = pids.iter().map(|&pid| Pid::from_u32(pid)).collect();
86    let mut sys = System::new();
87    sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&wanted), true));
88    sys.processes()
89        .iter()
90        .map(|(pid, proc)| (pid.as_u32(), Process::from_sysinfo(*pid, proc).name))
91        .collect()
92}
93
94/// A snapshot of every process and of the system, as `System::new_all` and
95/// `refresh_all` give it, taken under [`sysinfo_locked`]
96pub(crate) fn refreshed_system() -> System {
97    sysinfo_locked(|| {
98        let mut sys = System::new_all();
99        sys.refresh_all();
100        sys
101    })
102}
103use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid};
104
105/// Resolve a user ID to its user name, falling back to the ID itself.
106///
107/// On Unix this asks the system's user database (getpwuid_r), as `ps` and
108/// `id` do: sysinfo's user list leaves out every account whose shell ends in
109/// `/false` (macOS's daemon accounts, many service accounts) and every ID from
110/// 65536 up (directory and container users). Each ID is looked up once per run.
111#[cfg(unix)]
112fn user_name(uid: &Uid) -> String {
113    use std::collections::HashMap;
114    use std::sync::Mutex;
115
116    static NAMES: OnceLock<Mutex<HashMap<u32, String>>> = OnceLock::new();
117    let id: u32 = **uid;
118    let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
119    names
120        .entry(id)
121        .or_insert_with(|| {
122            nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(id))
123                .ok()
124                .flatten()
125                .map(|user| user.name)
126                .unwrap_or_else(|| id.to_string())
127        })
128        .clone()
129}
130
131/// Resolve a user ID to its user name, falling back to the ID itself.
132/// The user list is read once per run.
133#[cfg(windows)]
134fn user_name(uid: &Uid) -> String {
135    static USERS: OnceLock<Users> = OnceLock::new();
136    USERS
137        .get_or_init(Users::new_with_refreshed_list)
138        .get_user_by_id(uid)
139        .map(|user| user.name().to_string())
140        .unwrap_or_else(|| uid.to_string())
141}
142
143/// Make text from other processes safe to print: control characters become
144/// visible escapes, as `lsof` and `ps` do. Names, command lines and paths are
145/// set by whoever runs a process, and printed raw an ESC sequence could
146/// recolour, retitle or rewrite the terminal of whoever runs proc (and a
147/// newline would split a table row). Applied where process data is read, so
148/// every view and the JSON show the same text.
149///
150/// Escaped: C0 controls and DEL (`\x1b`, with `\n`, `\r`, `\t` by name), C1
151/// controls (`\x9b`), and bidirectional overrides that reorder text on screen
152/// (`\u{202e}`). Everything else, non-ASCII included, is unchanged. Works on
153/// chars, never byte offsets, so it can't split a character.
154pub fn escape_control(text: &str) -> std::borrow::Cow<'_, str> {
155    fn needs_escape(c: char) -> bool {
156        c.is_control()
157            || matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
158    }
159    if !text.chars().any(needs_escape) {
160        return std::borrow::Cow::Borrowed(text);
161    }
162    let mut out = String::with_capacity(text.len() + 8);
163    for c in text.chars() {
164        match c {
165            '\n' => out.push_str("\\n"),
166            '\r' => out.push_str("\\r"),
167            '\t' => out.push_str("\\t"),
168            c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
169            c if needs_escape(c) => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
170            c => out.push(c),
171        }
172    }
173    std::borrow::Cow::Owned(out)
174}
175
176/// `stuck` and `unstick` take this many readings, this far apart, and a process
177/// must look stuck in every one
178const STUCK_READINGS: usize = 2;
179const STUCK_READING_GAP: Duration = Duration::from_secs(1);
180
181/// Stuck in one reading: busy (over half a core), or in uninterruptible sleep,
182/// blocked in the kernel (usually on I/O) where it can't even be interrupted
183fn looks_stuck(cpu_percent: f32, status: SysProcessStatus) -> bool {
184    cpu_percent > 50.0 || status == SysProcessStatus::UninterruptibleDiskSleep
185}
186
187/// CPU use in percent of one core: the CPU time a process used between two
188/// reads ([`cpu_time_ms`]) over the wall time between them.
189/// sysinfo's own `cpu_usage` can't be trusted across two reads: on Windows a
190/// process's first update stores no baseline, so the next one divides its
191/// lifetime CPU by all CPU time since boot and reads ~0% for a full core.
192fn cpu_percent_between(used_before_ms: u64, used_after_ms: u64, wall: Duration) -> f32 {
193    let wall_ms = wall.as_secs_f64() * 1000.0;
194    if wall_ms <= 0.0 {
195        return 0.0;
196    }
197    (used_after_ms.saturating_sub(used_before_ms) as f64 / wall_ms * 100.0) as f32
198}
199
200/// The error for finding orphans on Windows (shared by the library call and the command)
201#[cfg(not(unix))]
202pub(crate) fn orphans_not_supported() -> ProcError {
203    ProcError::NotSupported(
204        "orphans is not supported on Windows: Windows doesn't re-parent processes, \
205         so there is no reliable way to tell an orphan"
206            .to_string(),
207    )
208}
209
210/// Why a command such as `taskkill` failed, in its own words: what it wrote to
211/// stderr, else to stdout, else its exit status
212/// (compiled on Unix for the tests only)
213#[cfg(any(windows, test))]
214fn command_failure(stdout: &[u8], stderr: &[u8], status: &str) -> String {
215    [stderr, stdout]
216        .iter()
217        .map(|out| {
218            // one line, whatever the tool wrapped its reason over
219            String::from_utf8_lossy(out)
220                .lines()
221                .map(str::trim)
222                .filter(|line| !line.is_empty())
223                .collect::<Vec<_>>()
224                .join(" ")
225        })
226        .find(|text| !text.is_empty())
227        .unwrap_or_else(|| format!("exited with {status}"))
228}
229
230/// The result of asking sysinfo to kill process `pid`, which says whether the
231/// kill was delivered
232fn kill_outcome(delivered: bool, pid: u32) -> Result<()> {
233    if delivered {
234        Ok(())
235    } else {
236        Err(ProcError::SignalError(format!(
237            "Failed to kill process {}",
238            pid
239        )))
240    }
241}
242
243/// Whether the process holding a PID now (start time `current`, 0 when it can't
244/// be read) is the one a record with start time `recorded` describes. Checked
245/// right before each signal. A record that knows its start time is the same
246/// process only if the current one reads within a second of it: an unreadable
247/// start time on the PID's holder now is not proof, so it gets no signal (on
248/// macOS that is another user's process that took the PID). A record without a
249/// start time has nothing to compare and counts as the same.
250fn same_start(recorded: Option<u64>, current: u64) -> bool {
251    match recorded {
252        Some(ours) => current > 0 && current.abs_diff(ours) <= 1,
253        None => true,
254    }
255}
256
257/// The program `find_by_open_file` asks which processes have a file open
258#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
259pub(crate) const OPEN_FILE_PROGRAM: &str = "fstat";
260#[cfg(all(unix, not(any(target_os = "freebsd", target_os = "netbsd"))))]
261pub(crate) const OPEN_FILE_PROGRAM: &str = "lsof";
262
263/// The PIDs `lsof -t` printed, one per line
264#[cfg(any(all(unix, not(any(target_os = "freebsd", target_os = "netbsd"))), test))]
265fn lsof_pids(stdout: &str) -> Vec<u32> {
266    stdout
267        .lines()
268        .filter_map(|line| line.trim().parse().ok())
269        .collect()
270}
271
272/// The PIDs in what `fstat FILE` printed (FreeBSD and NetBSD): a header, then
273/// one row per open file, `USER CMD PID FD MOUNT ... NAME`, so a process that
274/// holds the file twice is listed twice. A command name can hold spaces and
275/// digits ("Web 4021" is a valid executable name), so the first number is not
276/// always the PID. The mount point always starts with `/`, and neither a user
277/// name nor a command name can hold one (the kernel takes the command name
278/// from the executable's file name): the PID is two fields before the first
279/// field that starts with `/`, after at least one command-name field. A row
280/// that doesn't fit is left out, so an odd row misses a holder, never names
281/// another process. The header has no `/`
282#[cfg(any(target_os = "freebsd", target_os = "netbsd", test))]
283fn fstat_pids(stdout: &str) -> Vec<u32> {
284    let mut pids: Vec<u32> = stdout
285        .lines()
286        .filter_map(|line| {
287            let fields: Vec<&str> = line.split_whitespace().collect();
288            let mount = fields.iter().position(|field| field.starts_with('/'))?;
289            // USER, at least one CMD field, PID, FD, then the mount point
290            let pid = fields.get(mount.checked_sub(2).filter(|&at| at >= 2)?)?;
291            pid.bytes()
292                .all(|b| b.is_ascii_digit())
293                .then(|| pid.parse().ok())?
294        })
295        .collect();
296    pids.sort_unstable();
297    pids.dedup();
298    pids
299}
300
301/// How many times a same-process check reads a process that reports no start time
302const START_TIME_READS: u32 = 10;
303
304/// The wait between those reads
305const START_TIME_READ_GAP: Duration = Duration::from_millis(2);
306
307/// Runs `read` (one refresh of a process: the start time it read, None if the
308/// process is gone) once, and again while it reads `Some(0)`, up to
309/// [`START_TIME_READS`] times in all; only once unless `wants_start`, since a
310/// record without a start time compares none. NetBSD leaves the start time and
311/// the CPU time out of a process's record (p_uvalid 0) for a few hundred
312/// microseconds at a time while the process takes a signal or execs: in each
313/// of 10 rounds, reads in the 2 ms after SIGCONT found such a record, between
314/// reads with the real values. [`same_start`] takes 0 for another process, and
315/// a CPU time of 0 makes the next reading's look like the process's whole
316/// lifetime. Where 0 lasts (another user's process on macOS) the check still
317/// fails closed, 20 ms later.
318fn read_until_started(wants_start: bool, mut read: impl FnMut() -> Option<u64>) {
319    let mut reads = 1;
320    while read() == Some(0) && wants_start && reads < START_TIME_READS {
321        std::thread::sleep(START_TIME_READ_GAP);
322        reads += 1;
323    }
324}
325
326/// What a `taskkill` run (no `/F`) came to: success, or the reason it gave
327/// (compiled on Unix for the tests only)
328#[cfg(any(windows, test))]
329fn taskkill_outcome(output: &std::process::Output) -> Result<()> {
330    if output.status.success() {
331        Ok(())
332    } else {
333        Err(ProcError::SignalError(command_failure(
334            &output.stdout,
335            &output.stderr,
336            &output.status.to_string(),
337        )))
338    }
339}
340
341/// The CPU time each process in `sys` has used so far, in ms
342fn cpu_used(sys: &System) -> std::collections::HashMap<Pid, u64> {
343    sys.processes()
344        .iter()
345        .map(|(pid, p)| (*pid, cpu_time_ms(*pid, p)))
346        .collect()
347}
348
349/// The CPU time process `pid` has used so far, in ms. On NetBSD sysinfo counts
350/// it in hundredths of a second and rounds it to whole seconds, so over a 0.2 s
351/// window a process spinning at 100% read 0%: there it is the kernel's own
352/// counter, which `ps` shows, read right after the refresh that found `process`.
353/// 0 once the process is gone, so a window it ended in reads no CPU.
354fn cpu_time_ms(pid: Pid, process: &sysinfo::Process) -> u64 {
355    #[cfg(target_os = "netbsd")]
356    {
357        let _ = process;
358        netbsd_cpu_time_ms(pid.as_u32()).unwrap_or(0)
359    }
360    #[cfg(not(target_os = "netbsd"))]
361    {
362        let _ = pid;
363        process.accumulated_cpu_time()
364    }
365}
366
367/// NetBSD: the CPU time a process has used (p_rtime), from its kinfo_proc2;
368/// None if the kernel has no such process. The kernel leaves the CPU time out
369/// with the start time (p_uvalid 0, both 0) for a moment while the process
370/// takes a signal, so this reads again as [`read_until_started`] does
371#[cfg(target_os = "netbsd")]
372fn netbsd_cpu_time_ms(pid: u32) -> Option<u64> {
373    let mut last = None;
374    read_until_started(true, || {
375        last = netbsd_kinfo(pid);
376        last.map(|info| u64::from(info.p_ustart_sec))
377    });
378    last.filter(|info| info.p_uvalid != 0)
379        .map(|info| u64::from(info.p_rtime_sec) * 1000 + u64::from(info.p_rtime_usec) / 1000)
380}
381
382/// NetBSD: the kernel's kinfo_proc2 record of process `pid`; None if there is none
383#[cfg(target_os = "netbsd")]
384fn netbsd_kinfo(pid: u32) -> Option<libc::kinfo_proc2> {
385    let size = std::mem::size_of::<libc::kinfo_proc2>();
386    let mib = [
387        libc::CTL_KERN,
388        libc::KERN_PROC2,
389        libc::KERN_PROC_PID,
390        pid as libc::c_int,
391        size as libc::c_int,
392        1,
393    ];
394    // SAFETY: kinfo_proc2 is plain integers and arrays, so all zeroes is a valid value
395    let mut info: libc::kinfo_proc2 = unsafe { std::mem::zeroed() };
396    let mut len = size;
397    // SAFETY: `mib` holds its 6 names, and sysctl writes at most `len` bytes,
398    // the size of `info`, into it
399    let read = unsafe {
400        libc::sysctl(
401            mib.as_ptr(),
402            mib.len() as libc::c_uint,
403            (&mut info as *mut libc::kinfo_proc2).cast(),
404            &mut len,
405            std::ptr::null(),
406            0,
407        )
408    };
409    (read == 0 && len == size).then_some(info)
410}
411
412/// Whether a process has been running for `timeout_secs`. A process's age is in whole
413/// seconds, so a process under a second old reads 0 and must still count for
414/// `--timeout 0`: "longer than" means this many seconds or more.
415fn ran_long_enough(run_time: u64, timeout_secs: u64) -> bool {
416    run_time >= timeout_secs
417}
418
419/// Whether one reading of a process counts toward "stuck": a process, not a
420/// thread, that has run for `timeout_secs` and looks stuck. `find_stuck` filters
421/// through this, so a test of it guards the comparison where it is used.
422fn counts_as_stuck(is_thread: bool, run_time: u64, timeout_secs: u64, looks_stuck: bool) -> bool {
423    !is_thread && ran_long_enough(run_time, timeout_secs) && looks_stuck
424}
425
426/// macOS: sysinfo drops zombies, because they have no executable path or arguments
427/// left to read, so `proc list --status zombie` could never find one. Read every
428/// process from the kernel, as `ps` does, and keep the zombies sysinfo didn't return.
429#[cfg(target_os = "macos")]
430fn macos_zombies(known: &std::collections::HashSet<u32>) -> Vec<Process> {
431    let table = read_proc_table(|buf, size| {
432        let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL, 0];
433        let null = std::ptr::null_mut();
434        // SAFETY: `buf` is null (report the size needed) or points to `*size`
435        // writable bytes, which is how read_proc_table calls it
436        unsafe { libc::sysctl(mib.as_mut_ptr(), 4, buf, size, null, 0) }
437    });
438    zombies_in(&table, known)
439}
440
441/// libc has no kinfo_proc for macOS, so the few fields we need are read at their
442/// offsets in the 64-bit layout (checked against a live process's known values)
443#[cfg(target_os = "macos")]
444const KINFO_PROC_SIZE: usize = 648;
445
446/// The kernel's table of every process, as raw kinfo_proc records; empty if the
447/// kernel refuses. `sysctl` is the call to make (a buffer or null, and the size
448/// in and out), so a test can make it fail.
449#[cfg(target_os = "macos")]
450fn read_proc_table(
451    sysctl: impl Fn(*mut libc::c_void, &mut libc::size_t) -> libc::c_int,
452) -> Vec<u8> {
453    let mut size: libc::size_t = 0;
454    // the first call only reports the size needed
455    if sysctl(std::ptr::null_mut(), &mut size) != 0 {
456        return Vec::new();
457    }
458    // room for processes started in between
459    size += 64 * KINFO_PROC_SIZE;
460    let mut buf: Vec<u8> = vec![0; size];
461    if sysctl(buf.as_mut_ptr().cast(), &mut size) != 0 {
462        return Vec::new();
463    }
464    buf.truncate(size);
465    buf
466}
467
468/// The zombies in a table read by `read_proc_table` that `known` doesn't list
469#[cfg(target_os = "macos")]
470fn zombies_in(buf: &[u8], known: &std::collections::HashSet<u32>) -> Vec<Process> {
471    const P_STARTTIME_SEC: usize = 0;
472    const P_STAT: usize = 36;
473    const P_PID: usize = 40;
474    const P_COMM: usize = 243; // MAXCOMLEN + 1 bytes
475    const P_RUID: usize = 392;
476    const E_PPID: usize = 560;
477    const SZOMB: u8 = 5;
478
479    if !buf.len().is_multiple_of(KINFO_PROC_SIZE) {
480        return Vec::new(); // a layout we don't know; better to show nothing than garbage
481    }
482
483    let read_u32 = |p: &[u8], at: usize| u32::from_ne_bytes(p[at..at + 4].try_into().unwrap());
484    let now = std::time::SystemTime::now()
485        .duration_since(std::time::UNIX_EPOCH)
486        .map(|d| d.as_secs())
487        .unwrap_or(0);
488
489    buf.as_chunks::<KINFO_PROC_SIZE>()
490        .0
491        .iter()
492        .filter(|p| p[P_STAT] == SZOMB)
493        .filter_map(|p| {
494            let pid = read_u32(p, P_PID);
495            if pid == 0 || known.contains(&pid) {
496                return None;
497            }
498            let comm = &p[P_COMM..P_COMM + 17];
499            let comm = &comm[..comm.iter().position(|&c| c == 0).unwrap_or(comm.len())];
500            let name = escape_control(&String::from_utf8_lossy(comm)).into_owned();
501            let start =
502                u64::from_ne_bytes(p[P_STARTTIME_SEC..P_STARTTIME_SEC + 8].try_into().unwrap());
503            Some(Process {
504                pid,
505                name: if name.is_empty() {
506                    "<defunct>".to_string()
507                } else {
508                    name
509                },
510                exe_path: None,
511                cwd: None,
512                command: None,
513                argv0: None,
514                raw: None,
515                cpu_percent: 0.0,
516                memory_mb: 0.0,
517                status: ProcessStatus::Zombie,
518                user: Uid::try_from(read_u32(p, P_RUID) as usize)
519                    .ok()
520                    .map(|uid| escape_control(&user_name(&uid)).into_owned()),
521                parent_pid: Some(read_u32(p, E_PPID)),
522                start_time: (start > 0).then_some(start),
523                uptime_secs: (start > 0).then(|| now.saturating_sub(start)),
524            })
525        })
526        .collect()
527}
528
529/// On Linux, sysinfo lists every thread of a process as an entry of its own,
530/// with the process's command line, so a node server shows up a dozen times.
531/// Process listings skip them; `thread_kind` is always `None` elsewhere.
532pub fn is_thread(proc: &sysinfo::Process) -> bool {
533    proc.thread_kind().is_some()
534}
535
536/// Linux reports a working directory that was deleted as "/path (deleted)".
537/// Strip that marker so the cwd matches `--in` and reads the same as on macOS;
538/// a real directory with that name still exists, so it is kept as is.
539fn strip_deleted_marker(cwd: String) -> String {
540    match cwd.strip_suffix(" (deleted)") {
541        Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
542        _ => cwd,
543    }
544}
545
546/// Process status
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
548#[serde(rename_all = "lowercase")]
549pub enum ProcessStatus {
550    /// Process is actively executing on CPU
551    Running,
552    /// Process is waiting for an event or resource
553    Sleeping,
554    /// Process has been stopped (e.g., by SIGSTOP)
555    Stopped,
556    /// Process has terminated but not yet been reaped by parent
557    Zombie,
558    /// Process is being terminated
559    Dead,
560    /// Process status could not be determined
561    Unknown,
562}
563
564impl From<SysProcessStatus> for ProcessStatus {
565    fn from(status: SysProcessStatus) -> Self {
566        match status {
567            SysProcessStatus::Run => ProcessStatus::Running,
568            SysProcessStatus::Sleep => ProcessStatus::Sleeping,
569            SysProcessStatus::Stop => ProcessStatus::Stopped,
570            SysProcessStatus::Zombie => ProcessStatus::Zombie,
571            SysProcessStatus::Dead => ProcessStatus::Dead,
572            _ => ProcessStatus::Unknown,
573        }
574    }
575}
576
577/// Represents a system process with relevant information
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct Process {
580    /// Process ID
581    pub pid: u32,
582    /// Process name (executable name)
583    pub name: String,
584    /// Path to the executable
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub exe_path: Option<String>,
587    /// Current working directory
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub cwd: Option<String>,
590    /// Full command line (if available)
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub command: Option<String>,
593    /// The first word of the command line, kept whole (a path with spaces in
594    /// it stays one piece) and exactly as the OS gave it, for matching names;
595    /// never printed, and not part of the JSON
596    #[serde(skip)]
597    pub argv0: Option<String>,
598    /// The name, executable and command line exactly as the OS gave them, when
599    /// escaping control characters for display changed any of them. Matching
600    /// reads these, so a name matches the same way whether it is looked up in
601    /// the process list or against a record; `None` when nothing was escaped
602    /// (the fields above are then the raw text). Not part of the JSON.
603    #[serde(skip)]
604    pub raw: Option<Box<RawText>>,
605    /// CPU usage percentage (0.0 - 100.0+)
606    pub cpu_percent: f32,
607    /// Memory usage in megabytes
608    pub memory_mb: f64,
609    /// Process status
610    pub status: ProcessStatus,
611    /// User who owns the process
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub user: Option<String>,
614    /// Parent process ID
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub parent_pid: Option<u32>,
617    /// Process start time (Unix timestamp)
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub start_time: Option<u64>,
620    /// Seconds the process has been running
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub uptime_secs: Option<u64>,
623}
624
625/// This process and its ancestors, as far up as ten levels (proc → shell →
626/// shell wrapper → IDE → ...). A name lookup skips them all: their command
627/// lines hold proc's own arguments (`zsh -c 'proc by node'`).
628pub(crate) fn self_and_ancestors(sys: &System) -> std::collections::HashSet<Pid> {
629    let self_pid = Pid::from_u32(std::process::id());
630    let mut pids = std::collections::HashSet::new();
631    pids.insert(self_pid);
632    let mut current = self_pid;
633    for _ in 0..10 {
634        if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
635            pids.insert(parent);
636            current = parent;
637        } else {
638            break;
639        }
640    }
641    pids
642}
643
644/// The text of a process that name matching reads, as the OS gave it: control
645/// characters unescaped, unlike the fields a process is displayed from
646#[derive(Debug, Clone)]
647pub struct RawText {
648    /// The process name
649    pub name: String,
650    /// Path of the executable
651    pub exe_path: Option<String>,
652    /// The full command line
653    pub command: Option<String>,
654    /// The working directory
655    pub cwd: Option<String>,
656}
657
658/// How a name relates to a process sysinfo listed, under `mode`. Reads the
659/// name, executable and command line straight from sysinfo, so no record is
660/// built for the many processes that don't match.
661pub(crate) fn classify_sysinfo(pattern: &str, proc: &sysinfo::Process, mode: NameMatch) -> Hit {
662    let name = proc.name().to_string_lossy();
663    let exe = proc.exe().map(|exe| exe.to_string_lossy());
664    let argv0 = proc.cmd().first().map(|arg| arg.to_string_lossy());
665    // the joined command line is only needed to search inside it
666    let command = (!proc.cmd().is_empty()).then(|| {
667        proc.cmd()
668            .iter()
669            .map(|arg| arg.to_string_lossy())
670            .collect::<Vec<_>>()
671            .join(" ")
672    });
673    classify(
674        pattern,
675        &Names {
676            name: &name,
677            exe: exe.as_deref(),
678            argv0: argv0.as_deref(),
679            command: command.as_deref(),
680        },
681        mode,
682    )
683}
684
685impl Process {
686    /// The names a name target is compared with
687    pub fn names(&self) -> Names<'_> {
688        match &self.raw {
689            Some(raw) => Names {
690                name: &raw.name,
691                exe: raw.exe_path.as_deref(),
692                argv0: self.argv0.as_deref(),
693                command: raw.command.as_deref(),
694            },
695            None => Names {
696                name: &self.name,
697                exe: self.exe_path.as_deref(),
698                argv0: self.argv0.as_deref(),
699                command: self.command.as_deref(),
700            },
701        }
702    }
703
704    /// The working directory exactly as the OS gave it, for matching `--in`.
705    /// `cwd` is what is shown, with control characters escaped, so a directory
706    /// named `a<newline>b` and one named `a\nb` (backslash, `n`) both read
707    /// `a\nb` there; this one tells them apart.
708    pub fn raw_cwd(&self) -> Option<&str> {
709        match &self.raw {
710            Some(raw) => raw.cwd.as_deref(),
711            None => self.cwd.as_deref(),
712        }
713    }
714
715    /// Find all processes a name matches (see [`NameMatch`]): by default the
716    /// whole name, with `--cmd` any part of the name or command line.
717    ///
718    /// Proc itself and its ancestor shells never match. When nothing matches by
719    /// name but processes have the text in their command line, the error says
720    /// so: it costs nothing, the same pass over the processes counts them.
721    pub fn find_by_name(pattern: &str, mode: NameMatch) -> Result<Vec<Process>> {
722        let sys = refreshed_system();
723
724        // Exclude the entire ancestor chain — their command lines contain proc's
725        // arguments (e.g. "zsh -c proc by node"), causing false positives
726        let ancestor_pids = self_and_ancestors(&sys);
727        let mut in_command_line_only = 0;
728        let mut processes: Vec<Process> = sys
729            .processes()
730            .iter()
731            .filter_map(|(pid, proc)| {
732                // Exclude self, ancestor shells, and Linux threads
733                if ancestor_pids.contains(pid) || is_thread(proc) {
734                    return None;
735                }
736                match classify_sysinfo(pattern, proc, mode) {
737                    Hit::Match => Some(Process::from_sysinfo(*pid, proc)),
738                    Hit::CommandLineOnly => {
739                        in_command_line_only += 1;
740                        None
741                    }
742                    Hit::No => None,
743                }
744            })
745            .collect();
746        // sysinfo keeps its processes in a randomly ordered map: PID ascending
747        // makes every list that starts here the same on every run
748        processes.sort_by_key(|p| p.pid);
749
750        if processes.is_empty() {
751            return Err(ProcError::name_not_found(pattern, in_command_line_only));
752        }
753
754        Ok(processes)
755    }
756
757    /// Find a specific process by PID
758    pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
759        let sys = refreshed_system();
760
761        let sysinfo_pid = Pid::from_u32(pid);
762
763        Ok(sys
764            .processes()
765            .get(&sysinfo_pid)
766            .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
767    }
768
769    /// Get all running processes (excludes proc's own process)
770    pub fn find_all() -> Result<Vec<Process>> {
771        let sys = refreshed_system();
772
773        let self_pid = sysinfo::Pid::from_u32(std::process::id());
774        #[allow(unused_mut)]
775        let mut processes: Vec<Process> = sys
776            .processes()
777            .iter()
778            .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
779            .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
780            .collect();
781
782        #[cfg(target_os = "macos")]
783        {
784            let known: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
785            processes.extend(macos_zombies(&known));
786        }
787
788        // sysinfo keeps its processes in a randomly ordered map (and the
789        // zombies are appended after them): PID ascending makes every list
790        // that starts here the same on every run
791        processes.sort_by_key(|p| p.pid);
792
793        Ok(processes)
794    }
795
796    /// Find processes running a specific executable path
797    pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
798        let all = Self::find_all()?;
799        let path_str = path.to_string_lossy();
800
801        Ok(all
802            .into_iter()
803            .filter(|p| {
804                if let Some(ref exe) = p.exe_path {
805                    // Exact match (compare as strings to avoid PathBuf allocation)
806                    exe == &*path_str || std::path::Path::new(exe) == path
807                } else {
808                    false
809                }
810            })
811            .collect())
812    }
813
814    /// Find processes that have a file open (Unix only): `lsof`, or on FreeBSD
815    /// and NetBSD `fstat`, which their base system has. pkgsrc's lsof 4.91
816    /// found no holder at all on NetBSD 11 x86_64, and fstat is built with the
817    /// kernel it reads
818    #[cfg(unix)]
819    pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
820        Self::find_by_open_file_with(OPEN_FILE_PROGRAM, path)
821    }
822
823    /// `find_by_open_file` with the program to run, so a test can have it missing
824    #[cfg(unix)]
825    fn find_by_open_file_with(program: &str, path: &std::path::Path) -> Result<Vec<Process>> {
826        use std::process::Command;
827
828        let mut command = Command::new(program);
829        // -t = terse (PIDs only); fstat has no such option
830        #[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))]
831        command.arg("-t");
832        let output = crate::ui::interrupt::unblocked_in_child(&mut command)
833            .arg(&*path.to_string_lossy())
834            .output();
835
836        let output = match output {
837            Ok(o) => o,
838            Err(_) => return Ok(vec![]), // the program is not available
839        };
840
841        if !output.status.success() {
842            return Ok(vec![]); // No processes have file open
843        }
844
845        let stdout = String::from_utf8_lossy(&output.stdout);
846        #[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
847        let pids = fstat_pids(&stdout);
848        #[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))]
849        let pids = lsof_pids(&stdout);
850
851        let mut processes = Vec::new();
852        for pid in pids {
853            if let Ok(Some(proc)) = Self::find_by_pid(pid) {
854                processes.push(proc);
855            }
856        }
857
858        Ok(processes)
859    }
860
861    /// Find processes that have a file open (Windows stub)
862    #[cfg(not(unix))]
863    pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
864        // Windows: Could use handle.exe from Sysinternals, but skip for now
865        Ok(vec![])
866    }
867
868    /// Find processes that look stuck: running longer than `timeout`, and in each
869    /// of two readings a second apart either using over half a core or blocked in
870    /// uninterruptible sleep. One reading
871    /// used to be enough, so a momentary spike counted. A long build at full CPU
872    /// still looks the same as a loop that never ends; only the user can tell.
873    pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
874        let timeout_secs = timeout.as_secs();
875        // each read is timed from its start, as in `measure_cpu`
876        let mut read_at = std::time::Instant::now();
877        let mut sys = sysinfo_locked(System::new_all);
878        let mut used = cpu_used(&sys);
879        let mut cpu: std::collections::HashMap<Pid, f32> = std::collections::HashMap::new();
880        let mut stuck: Option<std::collections::HashSet<Pid>> = None;
881
882        for _ in 0..STUCK_READINGS {
883            std::thread::sleep(STUCK_READING_GAP);
884            let started = std::time::Instant::now();
885            sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true));
886            let wall = started - read_at;
887            read_at = started;
888            // a process first seen in this read has no CPU figure yet
889            cpu = sys
890                .processes()
891                .iter()
892                .filter_map(|(pid, p)| {
893                    let before = used.get(pid)?;
894                    Some((
895                        *pid,
896                        cpu_percent_between(*before, cpu_time_ms(*pid, p), wall),
897                    ))
898                })
899                .collect();
900            used = cpu_used(&sys);
901            let now: std::collections::HashSet<Pid> = sys
902                .processes()
903                .iter()
904                .filter(|(pid, p)| {
905                    let busy = cpu.get(pid).copied().unwrap_or(0.0);
906                    counts_as_stuck(
907                        is_thread(p),
908                        p.run_time(),
909                        timeout_secs,
910                        looks_stuck(busy, p.status()),
911                    )
912                })
913                .map(|(pid, _)| *pid)
914                .collect();
915            stuck = Some(match stuck {
916                None => now,
917                Some(before) => before.intersection(&now).copied().collect(),
918            });
919        }
920
921        let stuck = stuck.unwrap_or_default();
922        let mut found: Vec<Process> = sys
923            .processes()
924            .iter()
925            .filter(|(pid, _)| stuck.contains(pid))
926            .map(|(pid, p)| {
927                let mut process = Process::from_sysinfo(*pid, p);
928                // the figure the last read measured, not sysinfo's own
929                if let Some(&busy) = cpu.get(pid) {
930                    process.cpu_percent = busy;
931                }
932                process
933            })
934            .collect();
935        // sysinfo keeps its processes in a randomly ordered map: PID ascending
936        found.sort_by_key(|p| p.pid);
937        Ok(found)
938    }
939
940    /// Whether this process looks stuck now, measured the way `find_stuck` does
941    /// (stuck in two readings a second apart), for a process named as a target.
942    pub fn looks_stuck_now(&self) -> bool {
943        let pid = Pid::from_u32(self.pid);
944        let mut sys = System::new();
945        let mut read_at = std::time::Instant::now();
946        self.refresh_for_check(&mut sys);
947        let mut used = sys
948            .process(pid)
949            .filter(|p| self.is_same(p))
950            .map(|p| cpu_time_ms(pid, p));
951        (0..STUCK_READINGS).all(|_| {
952            std::thread::sleep(STUCK_READING_GAP);
953            let started = std::time::Instant::now();
954            self.refresh_for_check(&mut sys);
955            let wall = started - read_at;
956            read_at = started;
957            let (Some(before), Some(p)) = (used, sys.process(pid).filter(|p| self.is_same(p)))
958            else {
959                return false;
960            };
961            let after = cpu_time_ms(pid, p);
962            used = Some(after);
963            looks_stuck(cpu_percent_between(before, after, wall), p.status())
964        })
965    }
966
967    /// CPU use over `window` (at least sysinfo's minimum), in percent of one core;
968    /// None if the process is gone. A reading over a few milliseconds is ~0 for
969    /// any process, so a real window is needed.
970    ///
971    /// Unix only (the method does not exist on Windows): its one caller is the gentle recovery `unstick` tries there;
972    /// on Windows `unstick` only terminates.
973    #[cfg(unix)]
974    pub fn cpu_over(&self, window: Duration) -> Option<f32> {
975        let pid = Pid::from_u32(self.pid);
976        let mut sys = System::new();
977        let started = std::time::Instant::now();
978        self.refresh_for_check(&mut sys);
979        let before = sys
980            .process(pid)
981            .filter(|p| self.is_same(p))
982            .map(|p| cpu_time_ms(pid, p))?;
983        std::thread::sleep(window.max(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL));
984        let wall = started.elapsed();
985        self.refresh_for_check(&mut sys);
986        sys.process(pid)
987            .filter(|p| self.is_same(p))
988            .map(|p| cpu_percent_between(before, cpu_time_ms(pid, p), wall))
989    }
990
991    /// Measure the CPU use of these processes over 0.2 s and set `cpu_percent`,
992    /// in percent of one core. A lookup reads CPU a few milliseconds after its
993    /// first refresh, which is ~0% even for a process using a whole core, so
994    /// commands call this before they show, filter or sort by CPU. One 0.2 s wait
995    /// covers the whole batch; commands that never show CPU skip it.
996    ///
997    /// CPU is the CPU time each process used between two reads, over the wall
998    /// time we measure between them. sysinfo's own `cpu_usage` divides by its
999    /// system-wide tick interval instead, which read about 2% for a process
1000    /// spinning at 100% in some runs.
1001    pub fn measure_cpu(processes: &mut [Process]) {
1002        if processes.is_empty() {
1003            return;
1004        }
1005        let pids: Vec<Pid> = processes.iter().map(|p| Pid::from_u32(p.pid)).collect();
1006        let which = sysinfo::ProcessesToUpdate::Some(&pids);
1007        let cpu_only = sysinfo::ProcessRefreshKind::nothing().with_cpu();
1008        let mut sys = System::new();
1009
1010        let first = std::time::Instant::now();
1011        sysinfo_locked(|| sys.refresh_processes_specifics(which, true, cpu_only));
1012        let before = cpu_used(&sys);
1013        std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
1014        // each refresh reads the processes in the same order, so timing both
1015        // from their start gives every process the same window
1016        let wall = first.elapsed();
1017        sysinfo_locked(|| sys.refresh_processes_specifics(which, true, cpu_only));
1018
1019        for process in processes.iter_mut() {
1020            let pid = Pid::from_u32(process.pid);
1021            let (Some(current), Some(&cpu_before)) = (sys.process(pid), before.get(&pid)) else {
1022                continue;
1023            };
1024            if process.is_same(current) {
1025                process.cpu_percent =
1026                    cpu_percent_between(cpu_before, cpu_time_ms(pid, current), wall);
1027            }
1028        }
1029    }
1030
1031    /// Force kill the process (SIGKILL on Unix, taskkill /F on Windows)
1032    pub fn kill(&self) -> Result<()> {
1033        let mut sys = System::new();
1034        self.refresh_for_check(&mut sys);
1035
1036        match sys.process(Pid::from_u32(self.pid)) {
1037            Some(proc) if self.is_same(proc) => kill_outcome(proc.kill(), self.pid),
1038            // gone, or the PID now belongs to a new process
1039            _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
1040        }
1041    }
1042
1043    /// Refreshes this record's PID in `sys` for a check with [`is_same`](Self::is_same),
1044    /// reading again while the process reports no start time ([`read_until_started`])
1045    fn refresh_for_check(&self, sys: &mut System) {
1046        let pid = Pid::from_u32(self.pid);
1047        read_until_started(self.start_time.is_some(), || {
1048            sysinfo_locked(|| {
1049                sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true)
1050            });
1051            sys.process(pid).map(|p| p.start_time())
1052        });
1053    }
1054
1055    /// Whether `current`, the process now holding this PID, is the one this record
1056    /// describes. A different start time means the PID was reused by a new process.
1057    fn is_same(&self, current: &sysinfo::Process) -> bool {
1058        same_start(self.start_time, current.start_time())
1059    }
1060
1061    /// Checked right before each signal, so a delayed one (stop's SIGKILL comes up
1062    /// to --timeout seconds after the target was resolved) can't reach a new
1063    /// process that reused the PID.
1064    fn ensure_same_process(&self) -> Result<()> {
1065        let mut sys = System::new();
1066        let pid = Pid::from_u32(self.pid);
1067        self.refresh_for_check(&mut sys);
1068        match sys.process(pid) {
1069            Some(current) if self.is_same(current) => Ok(()),
1070            _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
1071        }
1072    }
1073
1074    /// Force kill and wait up to 5 seconds for the process to be gone.
1075    ///
1076    /// Never blocks forever (sysinfo's own kill_and_wait polls with no limit and
1077    /// never ends for a zombie). A process still alive after 5 seconds, such as
1078    /// one stuck in the kernel, is an error.
1079    pub fn kill_and_wait(&self) -> Result<()> {
1080        self.kill_and_wait_within(Duration::from_secs(5))
1081    }
1082
1083    /// `kill_and_wait` with the time to wait for the process to be gone
1084    fn kill_and_wait_within(&self, wait: Duration) -> Result<()> {
1085        self.kill_then_wait(wait, Process::kill)
1086    }
1087
1088    /// `kill_and_wait_within` with the kill to send, so a test can send one that
1089    /// leaves the process running (a real kill is over before it returns on
1090    /// Windows, though the process can stay listed for a moment after)
1091    fn kill_then_wait(&self, wait: Duration, kill: impl Fn(&Process) -> Result<()>) -> Result<()> {
1092        match kill(self) {
1093            // it exited on its own just before the kill
1094            Err(ProcError::ProcessNotFound(_)) => return Ok(()),
1095            other => other?,
1096        }
1097        let deadline = std::time::Instant::now() + wait;
1098        while std::time::Instant::now() < deadline {
1099            if !self.is_running() {
1100                return Ok(());
1101            }
1102            std::thread::sleep(Duration::from_millis(50));
1103        }
1104        Err(ProcError::SignalError(format!(
1105            "Process {} is still running {} seconds after SIGKILL",
1106            self.pid,
1107            wait.as_secs()
1108        )))
1109    }
1110
1111    /// Send SIGTERM for graceful termination (Unix) or taskkill (Windows)
1112    #[cfg(unix)]
1113    pub fn terminate(&self) -> Result<()> {
1114        use nix::sys::signal::{kill, Signal};
1115        use nix::unistd::Pid as NixPid;
1116
1117        self.ensure_same_process()?;
1118        kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
1119            .map_err(|e| ProcError::SignalError(e.to_string()))
1120    }
1121
1122    /// Graceful termination (Windows): `taskkill` without `/F` asks the
1123    /// process to close its windows, so it fails for a console program, which
1124    /// has none. That failure is returned, with taskkill's reason, rather than
1125    /// reported as success.
1126    #[cfg(windows)]
1127    pub fn terminate(&self) -> Result<()> {
1128        use std::process::Command;
1129
1130        self.ensure_same_process()?;
1131        let output = Command::new("taskkill")
1132            .args(["/PID", &self.pid.to_string()])
1133            .output()
1134            .map_err(|e| ProcError::SystemError(e.to_string()))?;
1135
1136        taskkill_outcome(&output)
1137    }
1138
1139    /// Send an arbitrary signal to the process (Unix only)
1140    #[cfg(unix)]
1141    pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
1142        use nix::sys::signal::kill;
1143        use nix::unistd::Pid as NixPid;
1144        self.ensure_same_process()?;
1145        kill(NixPid::from_raw(self.pid as i32), signal)
1146            .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
1147    }
1148
1149    // Note: On Windows, send_signal is not available. Commands that use it
1150    // (freeze, thaw) have their own #[cfg(not(unix))] stubs that return NotSupported.
1151
1152    /// Find orphaned processes (parent is PID 1 / init / launchd, excluding system daemons)
1153    ///
1154    /// Unix only: on Windows this returns `ProcError::NotSupported`, because
1155    /// Windows has no PID 1 and does not re-parent a process whose parent exited,
1156    /// so an empty list would claim "no orphans" where nothing could be checked.
1157    #[cfg(unix)]
1158    pub fn find_orphans() -> Result<Vec<Process>> {
1159        Ok(Self::orphans_among(Self::find_all()?))
1160    }
1161
1162    /// Windows: orphans cannot be told, so this is an error, not an empty list
1163    #[cfg(not(unix))]
1164    pub fn find_orphans() -> Result<Vec<Process>> {
1165        Err(orphans_not_supported())
1166    }
1167
1168    /// The orphans among `all`
1169    #[cfg(unix)]
1170    fn orphans_among(all: Vec<Process>) -> Vec<Process> {
1171        all.into_iter()
1172            .filter(|p| {
1173                if let Some(ppid) = p.parent_pid {
1174                    ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
1175                } else {
1176                    false
1177                }
1178            })
1179            .collect()
1180    }
1181
1182    /// Check if a process is a system daemon (naturally has PPID 1)
1183    ///
1184    /// Filters out processes that legitimately have PPID 1 — system daemons,
1185    /// kernel threads, and services managed by init/systemd/launchd.
1186    /// Heuristic: no cwd or cwd is "/" with exe in system paths.
1187    #[cfg(unix)]
1188    fn is_system_process(p: &Process) -> bool {
1189        if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
1190            if let Some(ref exe) = p.exe_path {
1191                // macOS system paths
1192                if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
1193                    return true;
1194                }
1195                // Shared Unix system paths (macOS + Linux)
1196                if exe.starts_with("/usr/sbin/")
1197                    || exe.starts_with("/sbin/")
1198                    || exe.starts_with("/usr/bin/")
1199                    || exe.starts_with("/usr/lib/")
1200                    || exe.starts_with("/usr/lib64/")
1201                    || exe.starts_with("/lib/")
1202                    || exe.starts_with("/lib64/")
1203                    || exe.starts_with("/opt/")
1204                    || exe.starts_with("/snap/")
1205                {
1206                    return true;
1207                }
1208            }
1209            return true; // No exe path = likely kernel thread
1210        }
1211        false
1212    }
1213
1214    /// Check if the process is still alive. A zombie (exited, not yet reaped by
1215    /// its parent) has exited, and a process with a different start time is a new
1216    /// one that reused the PID, so neither counts.
1217    pub fn exists(&self) -> bool {
1218        let mut sys = System::new();
1219        let pid = Pid::from_u32(self.pid);
1220        self.refresh_for_check(&mut sys);
1221        match sys.process(pid) {
1222            None => false,
1223            Some(current) if current.status() == SysProcessStatus::Zombie => false,
1224            Some(current) => self.is_same(current),
1225        }
1226    }
1227
1228    /// Check if the process is still running (alias for exists for compatibility)
1229    pub fn is_running(&self) -> bool {
1230        self.exists()
1231    }
1232
1233    /// Convert from sysinfo Process
1234    pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
1235        let cmd_vec = proc.cmd();
1236        // Every text field is set by whoever runs the process: escape control
1237        // characters before anything prints them. Name matching reads the raw
1238        // text (`raw`, kept only when escaping changed something).
1239        let raw_command = (!cmd_vec.is_empty()).then(|| {
1240            cmd_vec
1241                .iter()
1242                .map(|s| s.to_string_lossy())
1243                .collect::<Vec<_>>()
1244                .join(" ")
1245        });
1246        let raw_name = proc.name().to_string_lossy();
1247        let raw_exe = proc.exe().map(|p| p.to_string_lossy());
1248        let raw_cwd = proc
1249            .cwd()
1250            .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
1251        let changed = |text: &str| matches!(escape_control(text), std::borrow::Cow::Owned(_));
1252        let raw = (changed(&raw_name)
1253            || raw_exe.as_deref().is_some_and(changed)
1254            || raw_command.as_deref().is_some_and(changed)
1255            || raw_cwd.as_deref().is_some_and(changed))
1256        .then(|| {
1257            Box::new(RawText {
1258                name: raw_name.to_string(),
1259                exe_path: raw_exe.as_ref().map(|exe| exe.to_string()),
1260                command: raw_command.clone(),
1261                cwd: raw_cwd.clone(),
1262            })
1263        });
1264        let command = raw_command
1265            .as_deref()
1266            .map(|joined| escape_control(joined).into_owned());
1267
1268        let exe_path = raw_exe
1269            .as_deref()
1270            .map(|exe| escape_control(exe).into_owned());
1271        let cwd = raw_cwd
1272            .as_deref()
1273            .map(|path| escape_control(path).into_owned());
1274
1275        Process {
1276            pid: pid.as_u32(),
1277            name: escape_control(&raw_name).into_owned(),
1278            exe_path,
1279            cwd,
1280            command,
1281            argv0: cmd_vec
1282                .first()
1283                .map(|arg| arg.to_string_lossy().into_owned()),
1284            raw,
1285            cpu_percent: proc.cpu_usage(),
1286            memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
1287            status: ProcessStatus::from(proc.status()),
1288            user: proc
1289                .user_id()
1290                .map(|uid| escape_control(&user_name(uid)).into_owned()),
1291            parent_pid: proc.parent().map(|p| p.as_u32()),
1292            // sysinfo reports 0 when it can't read the start time (on macOS, any
1293            // other user's process), which would make the uptime about 56 years
1294            start_time: (proc.start_time() > 0).then(|| proc.start_time()),
1295            uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
1296        }
1297    }
1298}
1299
1300/// Parse a signal name to a nix Signal (Unix only)
1301///
1302/// Accepts: "HUP", "SIGHUP", "hup" (signal names only, not numbers —
1303/// numeric signal values differ between macOS and Linux)
1304#[cfg(unix)]
1305pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
1306    use nix::sys::signal::Signal;
1307
1308    let upper = name.to_uppercase();
1309    let upper = upper.trim_start_matches("SIG");
1310    match upper {
1311        "HUP" => Ok(Signal::SIGHUP),
1312        "INT" => Ok(Signal::SIGINT),
1313        "QUIT" => Ok(Signal::SIGQUIT),
1314        "ABRT" => Ok(Signal::SIGABRT),
1315        "KILL" => Ok(Signal::SIGKILL),
1316        "TERM" => Ok(Signal::SIGTERM),
1317        "STOP" => Ok(Signal::SIGSTOP),
1318        "CONT" => Ok(Signal::SIGCONT),
1319        "USR1" => Ok(Signal::SIGUSR1),
1320        "USR2" => Ok(Signal::SIGUSR2),
1321        _ => Err(ProcError::InvalidInput(format!(
1322            "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
1323            name
1324        ))),
1325    }
1326}
1327
1328#[cfg(test)]
1329mod tests {
1330    use super::*;
1331
1332    #[test]
1333    fn names_by_pid_names_a_process_as_the_process_list_does() {
1334        let me = std::process::id();
1335        let names = names_by_pid(&[me, 4_000_000]);
1336        let record = Process::find_by_pid(me).unwrap().unwrap();
1337        assert_eq!(names.get(&me), Some(&record.name));
1338        assert!(!names.contains_key(&4_000_000), "a PID nobody holds");
1339    }
1340
1341    #[test]
1342    fn a_signal_needs_the_start_time_the_record_saw() {
1343        // the same start time, or within the second the clock rounds to
1344        assert!(same_start(Some(1_000), 1_000));
1345        assert!(same_start(Some(1_000), 1_001));
1346        // a newer process on the PID
1347        assert!(!same_start(Some(1_000), 4_600));
1348        // the holder's start time can't be read: not proof it is the same one
1349        assert!(!same_start(Some(1_000), 0));
1350        // a record without a start time has nothing to compare
1351        assert!(same_start(None, 0));
1352        assert!(same_start(None, 1_000));
1353    }
1354
1355    #[test]
1356    fn a_start_time_that_reads_0_is_read_again_until_it_is_there() {
1357        // how many reads each sequence of answers gets
1358        let reads = |wants_start: bool, answers: &[Option<u64>]| {
1359            let mut left = answers.iter();
1360            let mut count = 0;
1361            read_until_started(wants_start, || {
1362                count += 1;
1363                // past the given answers, the start time stays unread
1364                left.next().copied().unwrap_or(Some(0))
1365            });
1366            count
1367        };
1368        // NetBSD, while the process takes a signal: there a moment later
1369        assert_eq!(reads(true, &[Some(0), Some(0), Some(1_000)]), 3);
1370        // never readable: gives up after START_TIME_READS reads
1371        assert_eq!(reads(true, &[]), START_TIME_READS);
1372        // read at once, or the process is gone: one read
1373        assert_eq!(reads(true, &[Some(1_000)]), 1);
1374        assert_eq!(reads(true, &[None]), 1);
1375        // a record without a start time compares none, so 0 is not read again
1376        assert_eq!(reads(false, &[Some(0)]), 1);
1377    }
1378
1379    /// A service account (shell `/bin/false` or `/usr/bin/false`) is named like
1380    /// any other: sysinfo's user list leaves such accounts out, so proc showed
1381    /// their user ID instead. macOS's `daemon` account has that shell; a Linux
1382    /// machine need not have one, so the test runs on macOS. It asks the
1383    /// directory service, as proc does: Nix's macOS sandbox can't read /etc/passwd
1384    #[cfg(target_os = "macos")]
1385    #[test]
1386    fn an_account_whose_shell_is_false_is_named() {
1387        let daemon = nix::unistd::User::from_name("daemon")
1388            .unwrap()
1389            .expect("macOS has a daemon account");
1390        assert!(
1391            daemon.shell.to_string_lossy().ends_with("/false"),
1392            "{:?}",
1393            daemon.shell
1394        );
1395        let uid = Uid::try_from(daemon.uid.as_raw() as usize).unwrap();
1396        assert_eq!(user_name(&uid), "daemon");
1397    }
1398
1399    #[cfg(unix)]
1400    #[test]
1401    fn a_user_id_nobody_has_is_shown_as_the_number() {
1402        let uid = Uid::try_from(4_000_000_000usize).unwrap();
1403        assert_eq!(user_name(&uid), "4000000000");
1404    }
1405
1406    #[test]
1407    fn cpu_is_the_cpu_time_used_over_the_wall_time() {
1408        let second = Duration::from_secs(1);
1409        // a full core for a second, half a core, and nothing
1410        assert_eq!(cpu_percent_between(5_000, 6_000, second), 100.0);
1411        assert_eq!(cpu_percent_between(5_000, 5_500, second), 50.0);
1412        assert_eq!(cpu_percent_between(5_000, 5_000, second), 0.0);
1413        // two cores' worth reads 200%, in percent of one core
1414        assert_eq!(cpu_percent_between(0, 2_000, second), 200.0);
1415        // a counter that went backwards (a reused PID) or no time passing is 0
1416        assert_eq!(cpu_percent_between(6_000, 5_000, second), 0.0);
1417        assert_eq!(cpu_percent_between(0, 1_000, Duration::ZERO), 0.0);
1418    }
1419
1420    #[test]
1421    fn a_failed_command_is_explained_in_its_own_words() {
1422        let reason = "ERROR: The process with PID 7 could not be terminated.";
1423        assert_eq!(
1424            command_failure(b"", reason.as_bytes(), "exit code: 1"),
1425            reason
1426        );
1427        // stdout when stderr is empty, trimmed
1428        assert_eq!(
1429            command_failure(b"  not here\r\n", b"", "exit code: 1"),
1430            "not here"
1431        );
1432        // taskkill wraps its reason over several lines; the error keeps them on one
1433        assert_eq!(
1434            command_failure(
1435                b"",
1436                b"ERROR: first part\r\n  second part.\r\n\r\n",
1437                "exit code: 1"
1438            ),
1439            "ERROR: first part second part."
1440        );
1441        assert_eq!(
1442            command_failure(b"", b"  \n", "exit code: 128"),
1443            "exited with exit code: 128"
1444        );
1445    }
1446
1447    #[test]
1448    fn busy_means_over_half_a_core_or_blocked_in_the_kernel() {
1449        assert!(looks_stuck(50.1, SysProcessStatus::Run));
1450        assert!(!looks_stuck(50.0, SysProcessStatus::Run));
1451        assert!(!looks_stuck(0.0, SysProcessStatus::Sleep));
1452        assert!(looks_stuck(0.0, SysProcessStatus::UninterruptibleDiskSleep));
1453    }
1454
1455    #[test]
1456    fn a_reading_counts_as_stuck_from_the_timeout_on() {
1457        assert!(counts_as_stuck(false, 0, 0, true));
1458        assert!(!counts_as_stuck(false, 59, 60, true));
1459        assert!(counts_as_stuck(false, 60, 60, true));
1460        assert!(counts_as_stuck(false, 61, 60, true));
1461        // and only a busy process, never a thread
1462        assert!(!counts_as_stuck(false, 60, 60, false));
1463        assert!(!counts_as_stuck(true, 60, 60, true));
1464    }
1465
1466    #[test]
1467    fn a_process_that_ran_the_timeout_has_run_long_enough() {
1468        // --timeout 0 finds a process that reads 0 seconds old
1469        assert!(ran_long_enough(0, 0));
1470        assert!(ran_long_enough(5, 5));
1471        assert!(ran_long_enough(6, 5));
1472        assert!(!ran_long_enough(4, 5));
1473    }
1474
1475    #[test]
1476    fn test_find_all_processes() {
1477        let processes = Process::find_all().unwrap();
1478        assert!(!processes.is_empty(), "Should find at least one process");
1479    }
1480
1481    #[test]
1482    fn every_process_source_lists_pid_ascending() {
1483        use crate::test_support::{end, idle_child, record_of};
1484
1485        let mut children: Vec<_> = (0..3).map(|_| idle_child()).collect();
1486        let mine: Vec<u32> = children.iter().map(|c| record_of(c).pid).collect();
1487        let name = record_of(&children[0]).name;
1488
1489        let ascending = |what: &str, list: &[Process]| {
1490            let pids: Vec<u32> = list.iter().map(|p| p.pid).collect();
1491            let mut sorted = pids.clone();
1492            sorted.sort_unstable();
1493            sorted.dedup();
1494            assert_eq!(pids, sorted, "{what} is not PID ascending");
1495            pids
1496        };
1497        let all = ascending("find_all", &Process::find_all().unwrap());
1498        let named = ascending(
1499            "find_by_name",
1500            &Process::find_by_name(&name, NameMatch::Exact).unwrap(),
1501        );
1502        ascending("find_stuck", &Process::find_stuck(Duration::ZERO).unwrap());
1503        for pid in &mine {
1504            assert!(all.contains(pid), "find_all has {pid}");
1505            assert!(named.contains(pid), "find_by_name has {pid}");
1506        }
1507        children.iter_mut().for_each(end);
1508    }
1509
1510    #[test]
1511    fn test_find_by_pid_self() {
1512        let pid = std::process::id();
1513        let process = Process::find_by_pid(pid).unwrap();
1514        assert!(process.is_some(), "Should find own process");
1515    }
1516
1517    #[test]
1518    fn test_is_running_tells_a_reused_pid_apart() {
1519        let mut me = Process::find_by_pid(std::process::id()).unwrap().unwrap();
1520        assert!(me.is_running());
1521        // Same PID, different start time: a new process that reused the PID
1522        let start = me.start_time.expect("a running process has a start time");
1523        me.start_time = Some(start - 3600);
1524        assert!(!me.is_running(), "a reused PID counted as the same process");
1525    }
1526
1527    #[test]
1528    fn test_escape_control_makes_terminal_input_harmless() {
1529        // Ordinary text, including non-ASCII, is untouched
1530        assert_eq!(escape_control("node server.js"), "node server.js");
1531        assert_eq!(escape_control("café ✓ 日本"), "café ✓ 日本");
1532        // ESC sequences: colours, window title (OSC), clipboard (OSC 52)
1533        assert_eq!(escape_control("a\x1b[31mb"), "a\\x1b[31mb");
1534        assert_eq!(escape_control("\x1b]0;pwned\x07"), "\\x1b]0;pwned\\x07");
1535        // Line breaks and tabs would split or shift a table row
1536        assert_eq!(escape_control("one\ntwo\tthree\r"), "one\\ntwo\\tthree\\r");
1537        // DEL, and C1 controls such as the 8-bit CSI (U+009B)
1538        assert_eq!(escape_control("x\x7fy\u{9b}2J"), "x\\x7fy\\x9b2J");
1539        // Bidirectional overrides can reorder text on screen ("Trojan Source")
1540        assert_eq!(escape_control("ab\u{202e}cd"), "ab\\u{202e}cd");
1541    }
1542
1543    #[cfg(unix)]
1544    #[test]
1545    fn test_looks_stuck_needs_sustained_cpu() {
1546        let spawn = |script: &str| {
1547            std::process::Command::new("sh")
1548                .args(["-c", script])
1549                .spawn()
1550                .unwrap()
1551        };
1552        let mut spinning = spawn("while :; do :; done");
1553        let mut idle = spawn("exec sleep 300");
1554        std::thread::sleep(Duration::from_millis(300));
1555        let record = |pid: u32| Process::find_by_pid(pid).unwrap().unwrap();
1556
1557        let spinning_stuck = record(spinning.id()).looks_stuck_now();
1558        let idle_stuck = record(idle.id()).looks_stuck_now();
1559        for child in [&mut spinning, &mut idle] {
1560            let _ = child.kill();
1561            let _ = child.wait();
1562        }
1563        assert!(
1564            spinning_stuck,
1565            "a process spinning at 100% CPU wasn't stuck"
1566        );
1567        assert!(!idle_stuck, "an idle process counted as stuck");
1568    }
1569
1570    #[test]
1571    fn test_signals_never_reach_a_process_that_reused_the_pid() {
1572        let mut child = crate::test_support::idle_child();
1573        let mut stale = crate::test_support::record_of(&child);
1574        // A record from before the PID was reused: same PID, older start time
1575        stale.start_time = stale.start_time.map(|t| t - 3600);
1576
1577        let kill = stale.kill();
1578        let term = stale.terminate();
1579        #[cfg(unix)]
1580        let sig = stale.send_signal(nix::sys::signal::Signal::SIGKILL);
1581        let alive = child.try_wait().unwrap().is_none();
1582        let _ = child.kill();
1583        let _ = child.wait();
1584
1585        assert!(
1586            alive,
1587            "a stale record signalled the process now using its PID"
1588        );
1589        assert!(
1590            matches!(kill, Err(ProcError::ProcessNotFound(_))),
1591            "{kill:?}"
1592        );
1593        assert!(
1594            matches!(term, Err(ProcError::ProcessNotFound(_))),
1595            "{term:?}"
1596        );
1597        #[cfg(unix)]
1598        assert!(matches!(sig, Err(ProcError::ProcessNotFound(_))), "{sig:?}");
1599    }
1600
1601    /// A control character in a process's name, executable or command line is
1602    /// shown escaped, but name matching reads what the OS gave: a process found
1603    /// by `find_by_name` is also kept by `--by`, and the other way round
1604    #[cfg(unix)]
1605    #[test]
1606    fn text_with_control_characters_matches_as_typed_and_displays_escaped() {
1607        use crate::core::matches_by_filter;
1608        use std::os::unix::process::CommandExt;
1609
1610        let word = format!("zz\u{7}ctl{}x", std::process::id());
1611        // A shell waiting on its stdin, not `sleep`: where `sleep` is one name
1612        // of a multi-call binary (Nix's coreutils), it refuses to run under
1613        // another first argument. The shell ends when the pipe closes.
1614        let mut child = std::process::Command::new("sh")
1615            .arg0(&word)
1616            .args(["-c", "read line"])
1617            .stdin(std::process::Stdio::piped())
1618            .spawn()
1619            .unwrap();
1620        let pid = child.id();
1621        // `spawn` returns once the exec has begun, not once it has finished:
1622        // measured on Linux, /proc/<pid>/cmdline is still empty for a moment
1623        // after it (name and exe are already the new program's). A record taken
1624        // in that moment has no first argument, so wait for the command line.
1625        let record = std::iter::repeat_with(|| {
1626            std::thread::sleep(Duration::from_millis(5));
1627            Process::find_by_pid(pid).unwrap().unwrap()
1628        })
1629        .take(1000)
1630        .find(|record| record.argv0.is_some());
1631        let part = format!("\u{7}ctl{}", std::process::id());
1632
1633        let found = |pattern: &str, mode| {
1634            Process::find_by_name(pattern, mode)
1635                .map(|found| found.iter().any(|p| p.pid == pid))
1636                .unwrap_or(false)
1637        };
1638        let named = found(&word, NameMatch::Exact);
1639        let named_in_command = found(&part, NameMatch::Command);
1640        let _ = child.kill();
1641        let _ = child.wait();
1642
1643        let record = record.expect("the child's command line never appeared");
1644        assert!(named, "find_by_name read the escaped first argument");
1645        assert!(
1646            named_in_command,
1647            "find_by_name read the escaped command line"
1648        );
1649        assert!(
1650            matches_by_filter(&record, &word, NameMatch::Exact),
1651            "--by read the escaped first argument"
1652        );
1653        assert!(
1654            matches_by_filter(&record, &part, NameMatch::Command),
1655            "--by --cmd read the escaped command line"
1656        );
1657        // what is shown is escaped
1658        let shown = record.command.as_deref().unwrap();
1659        assert!(shown.contains("\\x07ctl"), "{shown}");
1660        assert!(!shown.contains('\u{7}'), "{shown:?}");
1661    }
1662
1663    /// The same, on Windows, where a process has no `arg0` to set: a control
1664    /// character in the command line (the listener example ignores extra
1665    /// arguments) is shown escaped and still matches as typed with `--cmd`
1666    #[cfg(windows)]
1667    #[test]
1668    fn a_control_character_in_a_windows_command_line_matches_as_typed_and_displays_escaped() {
1669        use crate::core::matches_by_filter;
1670
1671        let mut exe = std::env::current_exe().unwrap();
1672        exe.pop();
1673        exe.pop();
1674        exe.push("examples");
1675        exe.push("listener.exe");
1676        assert!(exe.exists(), "build the example first: {}", exe.display());
1677        let part = format!("\u{7}ctl{}", std::process::id());
1678        let mut child = std::process::Command::new(&exe)
1679            .arg(&part)
1680            .stdin(std::process::Stdio::null())
1681            .stdout(std::process::Stdio::null())
1682            .spawn()
1683            .unwrap();
1684        let pid = child.id();
1685        let record = std::iter::repeat_with(|| {
1686            std::thread::sleep(Duration::from_millis(20));
1687            Process::find_by_pid(pid).unwrap()
1688        })
1689        .take(500)
1690        .flatten()
1691        .find(|record| record.command.is_some())
1692        .expect("the child's command line never appeared");
1693        let found = Process::find_by_name(&part, NameMatch::Command)
1694            .map(|found| found.iter().any(|p| p.pid == pid))
1695            .unwrap_or(false);
1696        let kept = matches_by_filter(&record, &part, NameMatch::Command);
1697        crate::test_support::end(&mut child);
1698
1699        assert!(found, "find_by_name read the escaped command line");
1700        assert!(kept, "--by --cmd read the escaped command line");
1701        let shown = record.command.as_deref().unwrap();
1702        assert!(shown.contains("\\x07ctl"), "{shown}");
1703        assert!(!shown.contains('\u{7}'), "{shown:?}");
1704    }
1705
1706    #[test]
1707    fn a_name_is_read_from_the_raw_text_when_escaping_changed_it() {
1708        let record = Process {
1709            name: "a\\x07b".to_string(),
1710            raw: Some(Box::new(RawText {
1711                name: "a\u{7}b".to_string(),
1712                exe_path: Some("c\u{7}d".to_string()),
1713                command: Some("e\u{7}f".to_string()),
1714                cwd: None,
1715            })),
1716            argv0: Some("g".to_string()),
1717            ..ghost()
1718        };
1719        let names = record.names();
1720        assert_eq!(names.name, "a\u{7}b");
1721        assert_eq!(names.exe, Some("c\u{7}d"));
1722        assert_eq!(names.argv0, Some("g"));
1723        assert_eq!(names.command, Some("e\u{7}f"));
1724    }
1725
1726    #[test]
1727    fn the_ancestors_stop_where_the_process_list_does() {
1728        // A list that holds only this process: its parent is named but not
1729        // listed, so the climb ends there instead of going on
1730        let me = Pid::from_u32(std::process::id());
1731        let mut sys = System::new();
1732        sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[me]), true));
1733        let parent = sys.process(me).and_then(|p| p.parent());
1734        let expected: std::collections::HashSet<Pid> = std::iter::once(me).chain(parent).collect();
1735        assert_eq!(self_and_ancestors(&sys), expected);
1736    }
1737
1738    #[test]
1739    fn test_strip_deleted_marker() {
1740        assert_eq!(
1741            strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
1742            "/nonexistent/proc-gone"
1743        );
1744        assert_eq!(
1745            strip_deleted_marker("/tmp/project".to_string()),
1746            "/tmp/project"
1747        );
1748
1749        // A directory really named "x (deleted)" keeps its name
1750        let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
1751        std::fs::create_dir_all(&real).unwrap();
1752        let path = real.to_string_lossy().to_string();
1753        assert_eq!(strip_deleted_marker(path.clone()), path);
1754        std::fs::remove_dir(&real).unwrap();
1755    }
1756
1757    #[test]
1758    fn a_name_nothing_has_is_plain_not_found_in_both_modes() {
1759        for mode in [NameMatch::Exact, NameMatch::Command] {
1760            let result = Process::find_by_name("nonexistent_process_12345", mode);
1761            assert!(
1762                matches!(&result, Err(ProcError::ProcessNotFound(n)) if n == "nonexistent_process_12345"),
1763                "{result:?}"
1764            );
1765        }
1766    }
1767
1768    /// A shell waiting for input that never comes, with a marker in its command
1769    /// line: it is named `sh`, and the marker is only in `sh -c ...`. It runs
1770    /// nothing else, so there is no child to leave behind.
1771    #[cfg(unix)]
1772    fn waiting_shell_with(marker: &str) -> std::process::Child {
1773        std::process::Command::new("sh")
1774            .args(["-c", &format!("read line; : {marker}")])
1775            .stdin(std::process::Stdio::piped())
1776            .spawn()
1777            .unwrap()
1778    }
1779
1780    #[cfg(unix)]
1781    #[test]
1782    fn a_word_only_in_a_command_line_is_found_with_cmd_and_hinted_without() {
1783        let marker = format!("hintmarker{}", std::process::id());
1784        let mut child = waiting_shell_with(&marker);
1785        let pid = child.id();
1786        let exact = Process::find_by_name(&marker, NameMatch::Exact);
1787        let cmd = Process::find_by_name(&marker, NameMatch::Command);
1788        let _ = child.kill();
1789        let _ = child.wait();
1790
1791        // by default no process is named that, and the error says one has it
1792        // in its command line
1793        assert!(
1794            matches!(&exact, Err(ProcError::NameNotFound { name, in_command_line })
1795                if name == &marker && *in_command_line >= 1),
1796            "{exact:?}"
1797        );
1798        // with --cmd that process is the match, and the only one
1799        let found = cmd.unwrap();
1800        assert_eq!(found.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![pid]);
1801    }
1802
1803    #[cfg(unix)]
1804    #[test]
1805    fn test_find_orphans_returns_ok() {
1806        // Should not error — may or may not find orphans depending on system state
1807        let result = Process::find_orphans();
1808        assert!(result.is_ok());
1809    }
1810
1811    #[cfg(not(unix))]
1812    #[test]
1813    fn find_orphans_is_not_supported_on_windows() {
1814        let result = Process::find_orphans();
1815        assert!(
1816            matches!(&result, Err(ProcError::NotSupported(m)) if m.starts_with("orphans is not supported on Windows")),
1817            "{result:?}"
1818        );
1819    }
1820
1821    #[cfg(unix)]
1822    #[test]
1823    fn test_is_system_process_system_paths() {
1824        let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
1825            pid: 100,
1826            name: "test".to_string(),
1827            exe_path: exe.map(String::from),
1828            cwd: cwd.map(String::from),
1829            command: None,
1830            argv0: None,
1831            raw: None,
1832            cpu_percent: 0.0,
1833            memory_mb: 0.0,
1834            status: ProcessStatus::Running,
1835            user: None,
1836            parent_pid: Some(1),
1837            start_time: None,
1838            uptime_secs: None,
1839        };
1840
1841        // System daemons with cwd "/" and system exe paths
1842        assert!(Process::is_system_process(&make_proc(
1843            Some("/usr/sbin/sshd"),
1844            Some("/")
1845        )));
1846        assert!(Process::is_system_process(&make_proc(
1847            Some("/System/Library/foo"),
1848            Some("/")
1849        )));
1850        assert!(Process::is_system_process(&make_proc(
1851            Some("/usr/bin/systemd"),
1852            Some("/")
1853        )));
1854        assert!(Process::is_system_process(&make_proc(
1855            Some("/usr/lib/snapd/snapd"),
1856            Some("/")
1857        )));
1858
1859        // No exe path with cwd "/" = kernel thread
1860        assert!(Process::is_system_process(&make_proc(None, Some("/"))));
1861
1862        // No cwd at all = likely kernel thread
1863        assert!(Process::is_system_process(&make_proc(
1864            Some("/usr/bin/foo"),
1865            None
1866        )));
1867
1868        // Every system directory counts, each checked on its own
1869        for exe in [
1870            "/System/x",
1871            "/usr/libexec/x",
1872            "/usr/sbin/x",
1873            "/sbin/x",
1874            "/usr/bin/x",
1875            "/usr/lib/x",
1876            "/usr/lib64/x",
1877            "/lib/x",
1878            "/lib64/x",
1879            "/opt/x",
1880            "/snap/x",
1881        ] {
1882            assert!(
1883                Process::is_system_process(&make_proc(Some(exe), Some("/"))),
1884                "{exe}"
1885            );
1886        }
1887        // a daemon with cwd "/" outside them still counts: nothing says it is a user's
1888        assert!(Process::is_system_process(&make_proc(
1889            Some("/srv/x"),
1890            Some("/")
1891        )));
1892
1893        // User process with user cwd = NOT system
1894        assert!(!Process::is_system_process(&make_proc(
1895            Some("/usr/bin/node"),
1896            Some("/home/user/project")
1897        )));
1898        assert!(!Process::is_system_process(&make_proc(
1899            Some("/home/user/.local/bin/app"),
1900            Some("/home/user")
1901        )));
1902    }
1903
1904    #[cfg(unix)]
1905    #[test]
1906    fn test_parse_signal_name_valid() {
1907        use nix::sys::signal::Signal;
1908
1909        assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
1910        assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
1911        assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
1912        assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
1913        assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
1914        assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
1915        assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
1916        assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
1917        assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1918        assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
1919        assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
1920        assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
1921        assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
1922    }
1923
1924    #[cfg(unix)]
1925    #[test]
1926    fn test_parse_signal_name_invalid() {
1927        assert!(parse_signal_name("INVALID").is_err());
1928        assert!(parse_signal_name("FOO").is_err());
1929        assert!(parse_signal_name("").is_err());
1930    }
1931
1932    #[cfg(unix)]
1933    #[test]
1934    fn test_parse_signal_name_case_insensitive() {
1935        use nix::sys::signal::Signal;
1936
1937        assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
1938        assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
1939        assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1940        assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
1941        assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
1942    }
1943
1944    #[cfg(unix)]
1945    #[test]
1946    fn test_send_signal_nonexistent_process() {
1947        use nix::sys::signal::Signal;
1948
1949        // PID 99999999 almost certainly doesn't exist
1950        let proc = Process {
1951            pid: 99999999,
1952            name: "ghost".to_string(),
1953            exe_path: None,
1954            cwd: None,
1955            command: None,
1956            argv0: None,
1957            raw: None,
1958            cpu_percent: 0.0,
1959            memory_mb: 0.0,
1960            status: ProcessStatus::Running,
1961            user: None,
1962            parent_pid: None,
1963            start_time: None,
1964            uptime_secs: None,
1965        };
1966
1967        let result = proc.send_signal(Signal::SIGCONT);
1968        assert!(result.is_err());
1969    }
1970
1971    /// A record of a process that doesn't exist
1972    fn ghost() -> Process {
1973        Process {
1974            pid: 99999999,
1975            name: "ghost".to_string(),
1976            exe_path: None,
1977            cwd: None,
1978            command: None,
1979            argv0: None,
1980            raw: None,
1981            cpu_percent: 0.0,
1982            memory_mb: 0.0,
1983            status: ProcessStatus::Running,
1984            user: None,
1985            parent_pid: None,
1986            start_time: None,
1987            uptime_secs: None,
1988        }
1989    }
1990
1991    #[test]
1992    fn every_sysinfo_status_maps_to_ours() {
1993        for (sys, ours) in [
1994            (SysProcessStatus::Run, ProcessStatus::Running),
1995            (SysProcessStatus::Sleep, ProcessStatus::Sleeping),
1996            (SysProcessStatus::Stop, ProcessStatus::Stopped),
1997            (SysProcessStatus::Zombie, ProcessStatus::Zombie),
1998            (SysProcessStatus::Dead, ProcessStatus::Dead),
1999            (SysProcessStatus::Idle, ProcessStatus::Unknown),
2000        ] {
2001            assert_eq!(ProcessStatus::from(sys), ours, "{sys:?}");
2002        }
2003    }
2004
2005    #[test]
2006    fn a_process_that_is_gone_does_not_look_stuck() {
2007        assert!(!ghost().looks_stuck_now());
2008    }
2009
2010    #[test]
2011    fn killing_a_process_that_is_already_gone_is_success() {
2012        assert!(matches!(ghost().kill(), Err(ProcError::ProcessNotFound(_))));
2013        ghost().kill_and_wait().unwrap();
2014    }
2015
2016    #[test]
2017    fn a_kill_that_was_not_delivered_is_an_error() {
2018        kill_outcome(true, 7).unwrap();
2019        let err = kill_outcome(false, 7).unwrap_err();
2020        assert!(
2021            matches!(&err, ProcError::SignalError(m) if m == "Failed to kill process 7"),
2022            "{err:?}"
2023        );
2024    }
2025
2026    #[test]
2027    fn taskkill_without_force_reports_what_it_said() {
2028        #[cfg(unix)]
2029        use std::os::unix::process::ExitStatusExt;
2030        #[cfg(windows)]
2031        use std::os::windows::process::ExitStatusExt;
2032        use std::process::{ExitStatus, Output};
2033
2034        let output = |raw, stdout: &str, stderr: &str| Output {
2035            status: ExitStatus::from_raw(raw),
2036            stdout: stdout.as_bytes().to_vec(),
2037            stderr: stderr.as_bytes().to_vec(),
2038        };
2039        taskkill_outcome(&output(0, "SUCCESS: sent termination signal", "")).unwrap();
2040
2041        // exit code 1 on Windows; the same wait status on Unix
2042        #[cfg(unix)]
2043        let failed = 1 << 8;
2044        #[cfg(windows)]
2045        let failed = 1;
2046        let err =
2047            taskkill_outcome(&output(failed, "", "ERROR: could not be terminated")).unwrap_err();
2048        assert!(
2049            matches!(&err, ProcError::SignalError(m) if m == "ERROR: could not be terminated"),
2050            "{err:?}"
2051        );
2052    }
2053
2054    #[cfg(unix)]
2055    #[test]
2056    fn orphans_are_the_children_of_init_that_are_not_system_daemons() {
2057        let proc = |pid: u32, ppid: Option<u32>, exe: Option<&str>, cwd: Option<&str>| Process {
2058            pid,
2059            name: format!("p{pid}"),
2060            exe_path: exe.map(String::from),
2061            cwd: cwd.map(String::from),
2062            parent_pid: ppid,
2063            ..ghost()
2064        };
2065        let orphans = Process::orphans_among(vec![
2066            // a user's process whose parent died
2067            proc(10, Some(1), Some("/home/z/app"), Some("/home/z")),
2068            // a system daemon: cwd "/" and an exe in a system directory
2069            proc(11, Some(1), Some("/usr/sbin/sshd"), Some("/")),
2070            // init itself, and a process with a live parent
2071            proc(1, Some(1), Some("/sbin/init"), Some("/home/z")),
2072            proc(12, Some(500), Some("/home/z/app"), Some("/home/z")),
2073            // no parent known
2074            proc(13, None, Some("/home/z/app"), Some("/home/z")),
2075        ]);
2076        let pids: Vec<u32> = orphans.iter().map(|p| p.pid).collect();
2077        assert_eq!(pids, vec![10]);
2078    }
2079
2080    #[cfg(unix)]
2081    #[test]
2082    fn a_missing_lsof_finds_no_open_file_owners() {
2083        let found = Process::find_by_open_file_with("proc-no-such-lsof", std::path::Path::new("/"));
2084        assert!(found.unwrap().is_empty());
2085    }
2086
2087    #[test]
2088    fn lsof_prints_one_pid_a_line() {
2089        assert_eq!(lsof_pids("1851\n2614\n"), vec![1851, 2614]);
2090        assert!(lsof_pids("").is_empty());
2091    }
2092
2093    #[test]
2094    fn fstat_rows_give_the_pids_that_hold_the_file() {
2095        // as FreeBSD 15.1 and NetBSD 11.0 printed them
2096        let freebsd = "USER     CMD          PID   FD MOUNT      INUM MODE         SZ|DV R/W NAME\n\
2097            proc     sleep       2614    3 /        1445173 -rw-r--r--       5  r  /home/proc/held.txt\n";
2098        let netbsd = "USER     CMD          PID   FD  MOUNT         INUM MODE         SZ|DV R/W NAME\n\
2099            proc     sleep       1851    3  /           425799 -rw-r--r--       5 r   /home/proc/held.txt\n";
2100        assert_eq!(fstat_pids(freebsd), vec![2614]);
2101        assert_eq!(fstat_pids(netbsd), vec![1851]);
2102        // nobody holds it: the header alone
2103        assert!(fstat_pids(
2104            "USER     CMD          PID   FD MOUNT      INUM MODE         SZ|DV R/W NAME\n"
2105        )
2106        .is_empty());
2107        // a command name with a space, a path with digits and a space, and one
2108        // process listed for two descriptors
2109        let rows =
2110            "proc     Web Content  4021   12 /  77 -rw-r--r--  5 r  /home/proc/2026 notes.txt\n\
2111            proc     Web Content  4021   13 /  77 -rw-r--r--  5 r  /home/proc/2026 notes.txt\n\
2112            root     sshd           90  text /  12 -r-xr-xr-x  9 r  /home/proc/2026 notes.txt\n";
2113        assert_eq!(fstat_pids(rows), vec![90, 4021]);
2114        // command names made of numbers are not taken for the PID: an
2115        // executable named "4021", and one named "Web 4021"
2116        let numeric =
2117            "proc     4021          777    3 /home  77 -rw-r--r--  5 r  /home/proc/held.txt\n\
2118            proc     Web 4021      778    3 /home  77 -rw-r--r--  5 r  /home/proc/held.txt\n";
2119        assert_eq!(fstat_pids(numeric), vec![777, 778]);
2120        // a row with no mount point, or no command name before the PID, is left out
2121        assert!(
2122            fstat_pids("proc     sleep  2614    3 -  77 -rw-r--r--  5 r  held.txt\n").is_empty()
2123        );
2124        assert!(fstat_pids("proc  2614    3 /  77 -rw-r--r--  5 r  /h\n").is_empty());
2125    }
2126
2127    #[test]
2128    fn a_wait_for_a_process_that_stays_is_a_wait_and_then_an_error() {
2129        let mut child = crate::test_support::idle_child();
2130        let record = crate::test_support::record_of(&child);
2131        let started = std::time::Instant::now();
2132        // a kill that does nothing: the process is still there, however long
2133        // it is looked at
2134        let result = record.kill_then_wait(Duration::from_millis(300), |_| Ok(()));
2135        let waited = started.elapsed();
2136        let still_there = child.try_wait().unwrap().is_none();
2137        crate::test_support::end(&mut child);
2138        assert!(
2139            matches!(&result, Err(ProcError::SignalError(_))),
2140            "{result:?}"
2141        );
2142        assert!(waited >= Duration::from_millis(300), "{waited:?}");
2143        assert!(still_there);
2144    }
2145
2146    #[test]
2147    fn a_process_still_alive_when_the_wait_ends_is_an_error() {
2148        let mut child = crate::test_support::idle_child();
2149        let record = crate::test_support::record_of(&child);
2150        // no time to wait: the SIGKILL is sent, but the process isn't seen gone
2151        let result = record.kill_and_wait_within(Duration::ZERO);
2152        let _ = child.kill();
2153        let _ = child.wait();
2154        assert!(
2155            matches!(&result, Err(ProcError::SignalError(m))
2156                if m == &format!("Process {} is still running 0 seconds after SIGKILL", record.pid)),
2157            "{result:?}"
2158        );
2159    }
2160
2161    /// One kinfo_proc record, with the fields `zombies_in` reads
2162    #[cfg(target_os = "macos")]
2163    fn kinfo(pid: u32, stat: u8, comm: &str, ruid: u32, ppid: u32, start: u64) -> Vec<u8> {
2164        let mut rec = vec![0u8; KINFO_PROC_SIZE];
2165        rec[0..8].copy_from_slice(&start.to_ne_bytes());
2166        rec[36] = stat;
2167        rec[40..44].copy_from_slice(&pid.to_ne_bytes());
2168        rec[243..243 + comm.len()].copy_from_slice(comm.as_bytes());
2169        rec[392..396].copy_from_slice(&ruid.to_ne_bytes());
2170        rec[560..564].copy_from_slice(&ppid.to_ne_bytes());
2171        rec
2172    }
2173
2174    #[cfg(target_os = "macos")]
2175    #[test]
2176    fn zombies_are_read_from_the_kernels_table() {
2177        let known = std::collections::HashSet::from([30]);
2178        let table: Vec<u8> = [
2179            kinfo(10, 5, "defunct-app", 0, 99, 1_700_000_000),
2180            // no name left, and no start time
2181            kinfo(11, 5, "", 0, 1, 0),
2182            // alive, not a zombie
2183            kinfo(20, 2, "alive", 0, 1, 1_700_000_000),
2184            // already listed by sysinfo
2185            kinfo(30, 5, "known", 0, 1, 1_700_000_000),
2186            // the kernel's own slot
2187            kinfo(0, 5, "kernel", 0, 0, 0),
2188        ]
2189        .concat();
2190
2191        let zombies = zombies_in(&table, &known);
2192        let pids: Vec<u32> = zombies.iter().map(|z| z.pid).collect();
2193        assert_eq!(pids, vec![10, 11]);
2194
2195        let named = &zombies[0];
2196        assert_eq!(named.name, "defunct-app");
2197        assert_eq!(named.status, ProcessStatus::Zombie);
2198        assert_eq!(named.parent_pid, Some(99));
2199        assert_eq!(named.start_time, Some(1_700_000_000));
2200        assert!(named.uptime_secs.unwrap() > 0);
2201        assert_eq!(named.user.as_deref(), Some("root"));
2202
2203        let unnamed = &zombies[1];
2204        assert_eq!(unnamed.name, "<defunct>");
2205        assert_eq!(unnamed.start_time, None);
2206        assert_eq!(unnamed.uptime_secs, None);
2207    }
2208
2209    #[cfg(target_os = "macos")]
2210    #[test]
2211    fn a_table_of_an_unknown_layout_shows_no_zombies() {
2212        let known = std::collections::HashSet::new();
2213        let mut table = kinfo(10, 5, "z", 0, 1, 0);
2214        table.push(0);
2215        assert!(zombies_in(&table, &known).is_empty());
2216    }
2217
2218    #[cfg(target_os = "macos")]
2219    #[test]
2220    fn a_refused_sysctl_gives_an_empty_table() {
2221        use std::cell::Cell;
2222
2223        // refused when asked for the size
2224        assert!(read_proc_table(|_, _| -1).is_empty());
2225
2226        // refused when asked for the table
2227        let calls = Cell::new(0);
2228        let table = read_proc_table(|_, size| {
2229            calls.set(calls.get() + 1);
2230            if calls.get() == 1 {
2231                *size = 2 * KINFO_PROC_SIZE;
2232                0
2233            } else {
2234                -1
2235            }
2236        });
2237        assert_eq!(calls.get(), 2);
2238        assert!(table.is_empty());
2239
2240        // answered: the table is cut to the size the kernel reports
2241        let calls = Cell::new(0);
2242        let table = read_proc_table(|buf, size| {
2243            calls.set(calls.get() + 1);
2244            if calls.get() == 1 {
2245                *size = 2 * KINFO_PROC_SIZE;
2246            } else {
2247                assert_eq!(*size, 66 * KINFO_PROC_SIZE, "room for 64 newcomers");
2248                // SAFETY: `buf` has `*size` writable bytes
2249                unsafe { buf.cast::<u8>().write(7) };
2250                *size = KINFO_PROC_SIZE;
2251            }
2252            0
2253        });
2254        assert_eq!(table.len(), KINFO_PROC_SIZE);
2255        assert_eq!(table[0], 7);
2256    }
2257}