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