Skip to main content

rightkit_child_process/
foreign.rs

1//! Processes this program did not spawn: a pid read from a pid file, a helper another
2//! launcher started, a descendant that outlived its parent.
3//!
4//! [`AdoptedProcess`](crate::AdoptedProcess) can only say a pid is running. A pid alone is a
5//! name the kernel reuses, so signalling a remembered number can reach an unrelated process.
6//! [`AdoptedIdentity`] remembers the pid together with the process's start time (and its
7//! executable path, informational), read while the process was known to be the right one, and
8//! every signal first checks that the pid still names that process:
9//!
10//! * a pid that now names a different process (other start time; the executable path is not
11//!   compared because a process that `exec`s keeps its pid and start time) is refused with [`SignalResult::IdentityMismatch`] and nothing is
12//!   sent;
13//! * a pid that names nothing is [`SignalResult::Gone`];
14//! * this process is never signalled, and neither is a pid that cannot be described.
15//!
16//! The parent pid is deliberately not part of the identity: an orphan is re-parented without
17//! becoming a different process.
18//!
19//! # Limits
20//!
21//! Unix: the check and the `kill` are two system calls. The pid can change hands in the
22//! microseconds between them only if the original process exits and the number wraps, the
23//! gap [`identity`](crate::identity) documents; it cannot be closed from user space. Windows
24//! has no such gap: the check and `TerminateProcess` use one handle. An exited process that
25//! nobody has reaped yet (a Unix zombie) still verifies as the remembered identity, because
26//! its description must stay readable while its owner finishes with its group; liveness
27//! ([`AdoptedIdentity::is_running`]) treats it as dead. This crate never reaps a foreign
28//! zombie: reaping is its parent's job, and a `waitpid` here could steal a status.
29//!
30//! Descendants are found by scanning the process table for parent links, so only descendants
31//! whose parent is still alive can be found: once a parent exits, its children are
32//! re-parented away. Take the list ([`AdoptedIdentity::descendants`]) while the root is
33//! alive, and sweep it ([`sweep`]) later; the identities keep the sweep safe.
34
35mod scan;
36
37pub use scan::{
38    find_processes, sweep_matching, MatchSweepOptions, MatchSweepReport, MatchedProcess,
39    ProcessQuery,
40};
41
42use std::{
43    io,
44    num::NonZeroU32,
45    path::PathBuf,
46    thread,
47    time::{Duration, Instant, SystemTime},
48};
49
50/// When a process started, in the host's own unit (macOS: seconds and microseconds; Linux:
51/// clock ticks since boot and 0; Windows: 100 ns ticks since 1601 and 0). Comparable only on
52/// the host that produced it.
53#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct StartToken {
55    pub major: u64,
56    pub minor: u64,
57}
58
59/// A pid together with the start time and executable path that make it a name for exactly one
60/// process. See the module documentation.
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct AdoptedIdentity {
63    pid: NonZeroU32,
64    start: StartToken,
65    exe: Option<PathBuf>,
66}
67
68/// What a pid names now, compared with a remembered identity.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum IdentityCheck {
71    /// The pid still names the remembered process.
72    Same,
73    /// No process has this pid.
74    Gone,
75    /// The pid names a different process.
76    Replaced,
77}
78
79/// How hard to stop a process. Windows has one way to stop a process, so both are the same
80/// there.
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum AdoptedSignal {
83    /// `SIGTERM`.
84    Graceful,
85    /// `SIGKILL`.
86    Force,
87}
88
89/// What `sys::signal` delivers: the public [`AdoptedSignal`] plus suspend and resume, which
90/// are separate methods so the public enum stays exhaustive and unchanged.
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92enum Delivery {
93    Graceful,
94    Force,
95    Stop,
96    Continue,
97}
98
99impl From<AdoptedSignal> for Delivery {
100    fn from(signal: AdoptedSignal) -> Self {
101        match signal {
102            AdoptedSignal::Graceful => Self::Graceful,
103            AdoptedSignal::Force => Self::Force,
104        }
105    }
106}
107
108/// The outcome of [`AdoptedIdentity::signal`].
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum SignalResult {
111    /// The identity matched and the signal was sent.
112    Delivered,
113    /// No process has the pid any more.
114    Gone,
115    /// The pid names a different process; nothing was sent.
116    IdentityMismatch,
117}
118
119/// The result of [`sweep`], as pids.
120#[derive(Clone, Debug, Default, Eq, PartialEq)]
121pub struct SweepReport {
122    /// Signalled and no longer present.
123    pub terminated: Vec<u32>,
124    /// Already absent when the sweep reached them.
125    pub gone: Vec<u32>,
126    /// The pid names a different process now; nothing was sent.
127    pub refused: Vec<u32>,
128    /// Signalled (forced) and still present at the end of the sweep.
129    pub survivors: Vec<u32>,
130    /// The signal could not be sent (for example, permission denied).
131    pub failed: Vec<u32>,
132}
133
134impl AdoptedIdentity {
135    /// Reads the identity of `pid`. `Ok(None)` when no process has that pid. The caller is
136    /// responsible for `pid` naming the intended process at this moment (it was just spawned
137    /// by a launcher, or it was just read from the owner's own record).
138    pub fn capture(pid: NonZeroU32) -> io::Result<Option<Self>> {
139        Ok(sys::read(pid.get())?.map(|seen| Self {
140            pid,
141            start: seen.start,
142            exe: seen.exe,
143        }))
144    }
145
146    /// Rebuilds an identity persisted earlier (a pid file that also records
147    /// [`start_token`](Self::start_token) and [`exe`](Self::exe)).
148    pub fn from_recorded(pid: NonZeroU32, start: StartToken, exe: Option<PathBuf>) -> Self {
149        Self { pid, start, exe }
150    }
151
152    pub fn pid(&self) -> u32 {
153        self.pid.get()
154    }
155
156    pub fn start_token(&self) -> StartToken {
157        self.start
158    }
159
160    pub fn exe(&self) -> Option<&std::path::Path> {
161        self.exe.as_deref()
162    }
163
164    fn matches(&self, seen: &sys::Seen) -> bool {
165        // pid plus start time is the identity. A process keeps both across `exec`, and the
166        // executable path changes then (on macOS `/bin/sh` and other launchers re-exec the
167        // real program after the pid was captured), so the path is recorded for the caller
168        // but never compared: a recycled pid has a different start time.
169        seen.start == self.start
170    }
171
172    /// Verifies a pid recorded by an earlier run of this program as Unix epoch milliseconds.
173    /// Returns the identity (with the host's exact start token) when the pid names a process
174    /// that started within `tolerance` of `recorded_unix_ms`, and `None` when the pid names no
175    /// process, names one that started at a different time (a recycled pid), or the host cannot
176    /// report start times. Epoch milliseconds are a coarse name, so the tolerance matters: a
177    /// start that is off by more than it is refused.
178    pub fn from_recorded_unix_ms(
179        pid: NonZeroU32,
180        recorded_unix_ms: u64,
181        tolerance: Duration,
182    ) -> io::Result<Option<Self>> {
183        let Some(current) = Self::capture(pid)? else {
184            return Ok(None);
185        };
186        let Some(started) = current.start_time() else {
187            return Ok(None);
188        };
189        let Some(recorded) =
190            SystemTime::UNIX_EPOCH.checked_add(Duration::from_millis(recorded_unix_ms))
191        else {
192            return Ok(None);
193        };
194        let gap = match started.duration_since(recorded) {
195            Ok(gap) => gap,
196            Err(early) => early.duration(),
197        };
198        Ok((gap <= tolerance).then_some(current))
199    }
200
201    /// When the process started, as a wall-clock time: exact on macOS (microseconds) and
202    /// Windows (100 ns); on Linux, the boot time plus the start ticks (one clock tick of
203    /// resolution). `None` where the host reports no start time or the value is out of range.
204    pub fn start_time(&self) -> Option<SystemTime> {
205        sys::start_time(self.start)
206    }
207
208    /// Whether the pid still names the remembered process.
209    pub fn verify(&self) -> io::Result<IdentityCheck> {
210        Ok(match sys::read(self.pid())? {
211            None => IdentityCheck::Gone,
212            Some(seen) if self.matches(&seen) => IdentityCheck::Same,
213            Some(_) => IdentityCheck::Replaced,
214        })
215    }
216
217    /// Whether the remembered process is still running. A zombie (exited, awaiting its own
218    /// parent's reap) is dead here although [`verify`](Self::verify) still names it `Same`;
219    /// this crate never reaps it. Windows has no zombies: an exited process already reads
220    /// `Gone`.
221    pub fn is_running(&self) -> io::Result<bool> {
222        Ok(self.verify()? == IdentityCheck::Same && !process_is_zombie(self.pid()))
223    }
224
225    /// Confirmed gone: the remembered process itself is not running any more (the pid names
226    /// nothing, names a different process, or is a dead zombie). Descendants and a process
227    /// group are separate observations: take [`descendants`](Self::descendants) while the
228    /// root lives and check each, or wait on the group with `wait_process_group_empty`.
229    pub fn confirmed_gone(&self) -> io::Result<bool> {
230        Ok(!self.is_running()?)
231    }
232
233    /// Waits at most `timeout` for [`confirmed_gone`](Self::confirmed_gone); `Ok(false)` when
234    /// the remembered process is still running then. Sends no signal.
235    pub fn wait_confirmed_gone(&self, timeout: Duration) -> io::Result<bool> {
236        let deadline = Instant::now().checked_add(timeout);
237        loop {
238            if self.confirmed_gone()? {
239                return Ok(true);
240            }
241            let Some(deadline) = deadline else {
242                return Ok(false);
243            };
244            let remaining = deadline.saturating_duration_since(Instant::now());
245            if remaining.is_zero() {
246                return Ok(false);
247            }
248            thread::sleep(SWEEP_POLL.min(remaining));
249        }
250    }
251
252    /// Identity-pinned graceful quit: one `Graceful` signal to exactly this process, then at
253    /// most `timeout` of waiting for it to be confirmed gone. Never escalates; a target that
254    /// stays up yields `Ok(false)`. A pid that changed hands is never signalled.
255    pub fn quit_gracefully(&self, timeout: Duration) -> io::Result<bool> {
256        match self.signal(AdoptedSignal::Graceful)? {
257            SignalResult::Gone | SignalResult::IdentityMismatch => self.confirmed_gone(),
258            SignalResult::Delivered => self.wait_confirmed_gone(timeout),
259        }
260    }
261
262    /// Sends `signal` only if the pid still names the remembered process. Never signals this
263    /// process: that is `InvalidInput`.
264    pub fn signal(&self, signal: AdoptedSignal) -> io::Result<SignalResult> {
265        if self.pid() == std::process::id() {
266            return Err(io::Error::new(
267                io::ErrorKind::InvalidInput,
268                "refusing to signal this process",
269            ));
270        }
271        sys::signal(self, signal.into())
272    }
273
274    /// Suspends the process (`SIGSTOP`) only if the pid still names the remembered process.
275    /// Unix only; Windows returns `Unsupported`. Never signals this process (`InvalidInput`).
276    pub fn suspend(&self) -> io::Result<SignalResult> {
277        self.deliver(Delivery::Stop)
278    }
279
280    /// Resumes a process stopped by [`suspend`](Self::suspend) (`SIGCONT`) only if the pid
281    /// still names the remembered process. Unix only; Windows returns `Unsupported`.
282    pub fn resume(&self) -> io::Result<SignalResult> {
283        self.deliver(Delivery::Continue)
284    }
285
286    fn deliver(&self, delivery: Delivery) -> io::Result<SignalResult> {
287        if self.pid() == std::process::id() {
288            return Err(io::Error::new(
289                io::ErrorKind::InvalidInput,
290                "refusing to signal this process",
291            ));
292        }
293        sys::signal(self, delivery)
294    }
295
296    /// The descendants of this process, parents before children, each with its own identity.
297    /// Empty when this process is gone or the pid now names a different one. A child is
298    /// accepted only if it did not start before its parent, so a stale parent link to a
299    /// recycled pid adds nothing.
300    pub fn descendants(&self) -> io::Result<Vec<AdoptedIdentity>> {
301        if self.verify()? != IdentityCheck::Same {
302            return Ok(Vec::new());
303        }
304        let pairs = sys::process_pairs()?;
305        let mut found: Vec<AdoptedIdentity> = Vec::new();
306        // (pid, start) of every process whose children are still to be looked up.
307        let mut frontier = vec![(self.pid(), self.start)];
308        let mut next = 0;
309        while next < frontier.len() {
310            let (parent, parent_start) = frontier[next];
311            next += 1;
312            for &(child, claimed_parent) in &pairs {
313                if claimed_parent != parent
314                    || child == parent
315                    || child == self.pid()
316                    || frontier.iter().any(|(pid, _)| *pid == child)
317                {
318                    continue;
319                }
320                let (Some(pid), Ok(Some(seen))) = (NonZeroU32::new(child), sys::read(child)) else {
321                    continue;
322                };
323                if seen.parent.is_some_and(|actual| actual != parent) || seen.start < parent_start {
324                    continue; // the pid changed hands since the table was read
325                }
326                frontier.push((child, seen.start));
327                found.push(AdoptedIdentity {
328                    pid,
329                    start: seen.start,
330                    exe: seen.exe,
331                });
332            }
333        }
334        // The root must have stayed itself for the whole scan.
335        if self.verify()? != IdentityCheck::Same {
336            return Ok(Vec::new());
337        }
338        Ok(found)
339    }
340}
341
342const SWEEP_POLL: Duration = Duration::from_millis(20);
343const FORCE_WAIT: Duration = Duration::from_millis(500);
344
345/// Stops every process in `targets` that is still the process it was remembered as: a graceful
346/// signal to each, up to `grace` for them to leave, then a forced signal to those still
347/// present. A target whose pid names a different process is `refused`, never signalled.
348/// Targets are signalled in the order given; pass leaves first (the reverse of
349/// [`AdoptedIdentity::descendants`]).
350pub fn sweep(targets: &[AdoptedIdentity], grace: Duration) -> io::Result<SweepReport> {
351    let mut report = SweepReport::default();
352    let mut pending: Vec<&AdoptedIdentity> = Vec::new();
353    for target in targets {
354        match target.signal(AdoptedSignal::Graceful) {
355            Ok(SignalResult::Delivered) => pending.push(target),
356            Ok(SignalResult::Gone) => report.gone.push(target.pid()),
357            Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
358            Err(_) => report.failed.push(target.pid()),
359        }
360    }
361    drain(&mut pending, grace, &mut report)?;
362    let mut forced: Vec<&AdoptedIdentity> = Vec::new();
363    for target in pending {
364        match target.signal(AdoptedSignal::Force) {
365            Ok(SignalResult::Delivered) => forced.push(target),
366            Ok(SignalResult::Gone) => report.terminated.push(target.pid()),
367            Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
368            Err(_) => report.failed.push(target.pid()),
369        }
370    }
371    drain(&mut forced, FORCE_WAIT, &mut report)?;
372    report
373        .survivors
374        .extend(forced.iter().map(|target| target.pid()));
375    Ok(report)
376}
377
378/// [`sweep`] of `root`'s current descendants, leaves first. The root itself is left alone.
379pub fn sweep_descendants(root: &AdoptedIdentity, grace: Duration) -> io::Result<SweepReport> {
380    let mut targets = root.descendants()?;
381    targets.reverse();
382    sweep(&targets, grace)
383}
384
385/// Whether `pid` names an exited-but-unreaped process (a zombie). Linux/Android read the
386/// state field of `/proc/<pid>/stat`; macOS reads `pbi_status` with `proc_pidinfo` in its
387/// zombies-allowed form. Anything unreadable, another Unix, and Windows (no zombies) read
388/// `false`: only a positively identified zombie is one, so callers fall back to existence.
389#[cfg(any(target_os = "linux", target_os = "android"))]
390pub(crate) fn process_is_zombie(pid: u32) -> bool {
391    let Ok(text) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
392        return false;
393    };
394    // `pid (comm) state ...`: comm may hold spaces and parentheses, so start after the last ')'.
395    text.rsplit_once(')')
396        .is_some_and(|(_, rest)| matches!(rest.trim_start().chars().next(), Some('Z' | 'X' | 'x')))
397}
398
399#[cfg(target_os = "macos")]
400pub(crate) fn process_is_zombie(pid: u32) -> bool {
401    /// `SZOMB` from `<sys/proc.h>`: process is a zombie state.
402    const SZOMB: u32 = 5;
403    // SAFETY: an all-zero `proc_bsdinfo` is a valid value (plain integers and char arrays).
404    let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
405    let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
406    // SAFETY: `info` is a writable buffer of exactly `size` bytes; arg 1 describes zombies.
407    let written = unsafe {
408        libc::proc_pidinfo(
409            pid as libc::pid_t,
410            libc::PROC_PIDTBSDINFO,
411            1,
412            (&mut info as *mut libc::proc_bsdinfo).cast(),
413            size,
414        )
415    };
416    written == size && info.pbi_pid == pid && info.pbi_status == SZOMB
417}
418
419#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
420pub(crate) fn process_is_zombie(_pid: u32) -> bool {
421    false
422}
423
424/// Observation-only existence probe for a Unix process group: sends signal 0 and nothing
425/// else. `EPERM` means members exist that this process may not signal, so the group is
426/// present. Group 1 (init's group) and any id that cannot name a group are refused with
427/// `InvalidInput`; `NonZeroU32` already excludes 0, which would name this process's own
428/// group. A group holding only zombies whose parent is elsewhere stays present until they
429/// are reaped: this is a group observation, not a per-member liveness check (use
430/// [`AdoptedIdentity::is_running`] for a remembered member).
431#[cfg(unix)]
432pub fn process_group_exists(pgid: NonZeroU32) -> io::Result<bool> {
433    let group = check_group_id(pgid)?;
434    loop {
435        // SAFETY: signal 0 performs only existence and permission checks; nothing is sent.
436        if unsafe { libc::killpg(group, 0) } == 0 {
437            return Ok(true);
438        }
439        let error = io::Error::last_os_error();
440        match error.raw_os_error() {
441            Some(libc::EINTR) => continue,
442            Some(libc::EPERM) => return Ok(true),
443            Some(libc::ESRCH) => return Ok(false),
444            _ => return Err(error),
445        }
446    }
447}
448
449/// Non-Unix hosts have no process groups to observe.
450#[cfg(not(unix))]
451pub fn process_group_exists(_pgid: NonZeroU32) -> io::Result<bool> {
452    Err(io::Error::from(io::ErrorKind::Unsupported))
453}
454
455/// Waits at most `timeout` for a process group to empty; `Ok(false)` when members remain
456/// then. Observation only (see [`process_group_exists`]); it never signals the group.
457pub fn wait_process_group_empty(pgid: NonZeroU32, timeout: Duration) -> io::Result<bool> {
458    #[cfg(not(unix))]
459    {
460        let _ = timeout;
461        // `Ok(true)` here means "empty", the inverse of `process_group_exists`.
462        process_group_exists(pgid).map(|exists| !exists)
463    }
464    #[cfg(unix)]
465    {
466        let deadline = Instant::now().checked_add(timeout);
467        loop {
468            if !process_group_exists(pgid)? {
469                return Ok(true);
470            }
471            let Some(deadline) = deadline else {
472                return Ok(false);
473            };
474            let remaining = deadline.saturating_duration_since(Instant::now());
475            if remaining.is_zero() {
476                return Ok(false);
477            }
478            thread::sleep(SWEEP_POLL.min(remaining));
479        }
480    }
481}
482
483/// A group id this crate will observe: never 0 (excluded by the type), 1 (init's group), or
484/// a value the kernel's signed pid type cannot hold.
485#[cfg(unix)]
486pub(crate) fn check_group_id(pgid: NonZeroU32) -> io::Result<libc::pid_t> {
487    if pgid.get() == 1 {
488        return Err(io::Error::new(
489            io::ErrorKind::InvalidInput,
490            "refusing to observe process group 1",
491        ));
492    }
493    libc::pid_t::try_from(pgid.get()).map_err(io::Error::other)
494}
495
496/// Suspends the current process with `SIGTSTP` (the terminal Ctrl-Z stop), so a shell or TUI
497/// can hand the terminal back and resume on `SIGCONT`. Returns once the process is continued.
498/// Unix only.
499#[cfg(unix)]
500pub fn suspend_current_process() -> io::Result<()> {
501    // SAFETY: raise takes a plain signal number.
502    if unsafe { libc::raise(libc::SIGTSTP) } == 0 {
503        Ok(())
504    } else {
505        Err(io::Error::last_os_error())
506    }
507}
508
509/// Windows has no `SIGTSTP`: returns `Unsupported` without stopping anything.
510#[cfg(not(unix))]
511pub fn suspend_current_process() -> io::Result<()> {
512    Err(io::Error::from(io::ErrorKind::Unsupported))
513}
514
515/// The Unix signal number for `signal`.
516#[cfg(unix)]
517fn unix_signal_number(delivery: Delivery) -> libc::c_int {
518    match delivery {
519        Delivery::Graceful => libc::SIGTERM,
520        Delivery::Force => libc::SIGKILL,
521        Delivery::Stop => libc::SIGSTOP,
522        Delivery::Continue => libc::SIGCONT,
523    }
524}
525
526/// The process groups that belong to session `sid` (the session id is the pid of its leader),
527/// one entry per group, excluding this process's own group. Found by one scan of the process
528/// table; a group that starts after the scan is not listed.
529#[cfg(unix)]
530pub(crate) fn session_groups(sid: NonZeroU32) -> io::Result<Vec<libc::pid_t>> {
531    let wanted = sid.get() as libc::pid_t;
532    // SAFETY: getpgrp takes no arguments.
533    let own_group = unsafe { libc::getpgrp() };
534    let mut groups: Vec<libc::pid_t> = Vec::new();
535    for pid in sys::list_pids()? {
536        let pid = pid as libc::pid_t;
537        // SAFETY: getsid and getpgid take a pid and return -1 when it names no process.
538        if unsafe { libc::getsid(pid) } != wanted {
539            continue;
540        }
541        let group = unsafe { libc::getpgid(pid) };
542        if group > 0 && group != own_group && !groups.contains(&group) {
543            groups.push(group);
544        }
545    }
546    Ok(groups)
547}
548
549/// Sends `signal` to every process group in session `sid` (see [`session_groups`]). Returns
550/// `true` when at least one group was found and reached. Unix only. Best effort: the groups are
551/// found by a process-table scan and signalled by group id, so a group that exits and whose id
552/// is reused between the scan and the signal can be hit. Use it for a session the caller
553/// created (a pseudo-terminal leader), not for an arbitrary session.
554#[cfg(unix)]
555pub fn signal_session(sid: NonZeroU32, signal: AdoptedSignal) -> io::Result<bool> {
556    signal_session_with(sid, signal.into())
557}
558
559/// Suspends (`SIGSTOP`) every process group in session `sid`; same contract as
560/// [`signal_session`]. Unix only.
561#[cfg(unix)]
562pub fn suspend_session(sid: NonZeroU32) -> io::Result<bool> {
563    signal_session_with(sid, Delivery::Stop)
564}
565
566/// Resumes (`SIGCONT`) every process group in session `sid`; same contract as
567/// [`signal_session`]. Unix only.
568#[cfg(unix)]
569pub fn resume_session(sid: NonZeroU32) -> io::Result<bool> {
570    signal_session_with(sid, Delivery::Continue)
571}
572
573#[cfg(unix)]
574fn signal_session_with(sid: NonZeroU32, delivery: Delivery) -> io::Result<bool> {
575    let number = unix_signal_number(delivery);
576    let mut reached = false;
577    for group in session_groups(sid)? {
578        // SAFETY: kill takes two integers; a negative pid names a process group.
579        if unsafe { libc::kill(-group, number) } == 0 {
580            reached = true;
581            continue;
582        }
583        let error = io::Error::last_os_error();
584        if error.raw_os_error() != Some(libc::ESRCH) {
585            return Err(error);
586        }
587    }
588    Ok(reached)
589}
590
591/// Waits until `deadline` for each pending target to stop being the remembered process; those
592/// that did are moved to `report.terminated`.
593fn drain(
594    pending: &mut Vec<&AdoptedIdentity>,
595    wait: Duration,
596    report: &mut SweepReport,
597) -> io::Result<()> {
598    let deadline = Instant::now().checked_add(wait);
599    loop {
600        let mut still = Vec::new();
601        for target in pending.drain(..) {
602            // Zombie-aware: a foreign zombie is dead though its identity still verifies;
603            // waiting for its (foreign) parent to reap it could wait for ever, and this
604            // crate must not reap it.
605            if target.is_running()? {
606                still.push(target);
607            } else {
608                report.terminated.push(target.pid());
609            }
610        }
611        *pending = still;
612        let expired = deadline.is_none_or(|limit| Instant::now() >= limit);
613        if pending.is_empty() || expired {
614            return Ok(());
615        }
616        thread::sleep(SWEEP_POLL);
617    }
618}
619
620mod sys {
621    use super::StartToken;
622    #[cfg(not(windows))]
623    use super::{AdoptedIdentity, SignalResult};
624    #[cfg(not(windows))]
625    use std::io;
626    use std::path::PathBuf;
627
628    /// One look at a process.
629    pub(super) struct Seen {
630        pub(super) start: StartToken,
631        pub(super) exe: Option<PathBuf>,
632        /// The parent pid when the host reports it with the process (Unix); Windows reads it
633        /// from the process table only.
634        pub(super) parent: Option<u32>,
635    }
636
637    /// Converts a host start token to wall-clock time (see `AdoptedIdentity::start_time`).
638    #[cfg(target_os = "macos")]
639    pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
640        // macOS tokens are the kernel's start time: seconds and microseconds since the epoch.
641        let micros = u32::try_from(start.minor).ok()?;
642        std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
643            start.major,
644            micros.checked_mul(1000)?,
645        ))
646    }
647
648    /// Linux tokens are clock ticks since boot: boot time (`btime` in /proc/stat) plus ticks.
649    #[cfg(any(target_os = "linux", target_os = "android"))]
650    pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
651        // SAFETY: sysconf takes a plain name constant and returns a value.
652        let hertz = u64::try_from(unsafe { libc::sysconf(libc::_SC_CLK_TCK) }).ok()?;
653        if hertz == 0 {
654            return None;
655        }
656        let boot = std::fs::read_to_string("/proc/stat")
657            .ok()?
658            .lines()
659            .find_map(|line| line.strip_prefix("btime "))
660            .and_then(|value| value.trim().parse::<u64>().ok())?;
661        let since_boot = std::time::Duration::new(
662            start.major / hertz,
663            u32::try_from((start.major % hertz) * 1_000_000_000 / hertz).ok()?,
664        );
665        std::time::SystemTime::UNIX_EPOCH
666            .checked_add(std::time::Duration::from_secs(boot))?
667            .checked_add(since_boot)
668    }
669
670    #[cfg(all(
671        unix,
672        not(any(target_os = "macos", target_os = "linux", target_os = "android"))
673    ))]
674    pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
675        None
676    }
677
678    /// Windows tokens are FILETIME: 100 ns ticks since 1601-01-01.
679    #[cfg(windows)]
680    pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
681        /// 1601-01-01 to 1970-01-01 in 100 ns ticks.
682        const EPOCH_OFFSET_TICKS: u64 = 116_444_736_000_000_000;
683        let since_unix = start.major.checked_sub(EPOCH_OFFSET_TICKS)?;
684        std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
685            since_unix / 10_000_000,
686            u32::try_from((since_unix % 10_000_000) * 100).ok()?,
687        ))
688    }
689
690    #[cfg(not(any(unix, windows)))]
691    pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
692        None
693    }
694
695    #[cfg(unix)]
696    pub(super) fn read(pid: u32) -> io::Result<Option<Seen>> {
697        Ok(crate::identity::describe(pid)?.map(|identity| Seen {
698            start: StartToken {
699                major: identity.started_sec,
700                minor: identity.started_usec,
701            },
702            exe: exe_path(pid),
703            parent: Some(identity.parent),
704        }))
705    }
706
707    #[cfg(target_os = "macos")]
708    fn exe_path(pid: u32) -> Option<PathBuf> {
709        use std::os::unix::ffi::OsStringExt;
710        // PROC_PIDPATHINFO_MAXSIZE is 4 * MAXPATHLEN.
711        let mut buffer = vec![0u8; 4096];
712        // SAFETY: the buffer is writable for exactly `buffer.len()` bytes.
713        let written = unsafe {
714            libc::proc_pidpath(
715                pid as libc::c_int,
716                buffer.as_mut_ptr().cast(),
717                buffer.len() as u32,
718            )
719        };
720        if written <= 0 {
721            return None;
722        }
723        buffer.truncate(written as usize);
724        Some(PathBuf::from(std::ffi::OsString::from_vec(buffer)))
725    }
726
727    #[cfg(any(target_os = "linux", target_os = "android"))]
728    fn exe_path(pid: u32) -> Option<PathBuf> {
729        std::fs::read_link(format!("/proc/{pid}/exe")).ok()
730    }
731
732    #[cfg(all(
733        unix,
734        not(any(target_os = "macos", target_os = "linux", target_os = "android"))
735    ))]
736    fn exe_path(_pid: u32) -> Option<PathBuf> {
737        None
738    }
739
740    #[cfg(unix)]
741    pub(super) fn signal(
742        identity: &AdoptedIdentity,
743        signal: super::Delivery,
744    ) -> io::Result<SignalResult> {
745        use super::IdentityCheck;
746        match identity.verify()? {
747            IdentityCheck::Gone => return Ok(SignalResult::Gone),
748            IdentityCheck::Replaced => return Ok(SignalResult::IdentityMismatch),
749            IdentityCheck::Same => {}
750        }
751        if super::process_is_zombie(identity.pid()) {
752            return Ok(SignalResult::Gone);
753        }
754        let pid = i32::try_from(identity.pid()).map_err(io::Error::other)?;
755        let number = super::unix_signal_number(signal);
756        // SAFETY: kill takes two integers; the pid was proven to be the remembered process
757        // just above (see the module documentation for the residual gap).
758        if unsafe { libc::kill(pid, number) } == 0 {
759            return Ok(SignalResult::Delivered);
760        }
761        let error = io::Error::last_os_error();
762        match error.raw_os_error() {
763            Some(libc::ESRCH) => Ok(SignalResult::Gone),
764            _ => Err(error),
765        }
766    }
767
768    /// Every (pid, parent pid) pair in the process table.
769    #[cfg(unix)]
770    pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
771        let mut pairs = Vec::new();
772        for pid in list_pids()? {
773            if let Ok(Some(identity)) = crate::identity::describe(pid) {
774                pairs.push((pid, identity.parent));
775            }
776        }
777        Ok(pairs)
778    }
779
780    #[cfg(target_os = "macos")]
781    pub(super) fn list_pids() -> io::Result<Vec<u32>> {
782        /// `PROC_ALL_PIDS` from `<libproc.h>`.
783        const PROC_ALL_PIDS: u32 = 1;
784        let size = std::mem::size_of::<libc::pid_t>();
785        // SAFETY: a null buffer asks only for the size needed.
786        let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
787        if needed <= 0 {
788            return Err(io::Error::last_os_error());
789        }
790        // Room for processes that start between the two calls.
791        let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
792        // SAFETY: the buffer holds `pids.len()` pids and the byte size passed says so.
793        let written = unsafe {
794            libc::proc_listpids(
795                PROC_ALL_PIDS,
796                0,
797                pids.as_mut_ptr().cast(),
798                (pids.len() * size) as libc::c_int,
799            )
800        };
801        if written <= 0 {
802            return Err(io::Error::last_os_error());
803        }
804        pids.truncate(written as usize / size);
805        Ok(pids
806            .into_iter()
807            .filter(|pid| *pid > 0)
808            .map(|pid| pid as u32)
809            .collect())
810    }
811
812    #[cfg(any(target_os = "linux", target_os = "android"))]
813    pub(super) fn list_pids() -> io::Result<Vec<u32>> {
814        Ok(std::fs::read_dir("/proc")?
815            .flatten()
816            .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
817            .collect())
818    }
819
820    #[cfg(all(
821        unix,
822        not(any(target_os = "macos", target_os = "linux", target_os = "android"))
823    ))]
824    pub(super) fn list_pids() -> io::Result<Vec<u32>> {
825        Err(io::Error::from(io::ErrorKind::Unsupported))
826    }
827
828    #[cfg(windows)]
829    mod windows_impl {
830        use super::super::{AdoptedIdentity, SignalResult, StartToken};
831        use super::Seen;
832        use crate::process_windows::os_error;
833        use std::{io, path::PathBuf};
834        use windows::{
835            core::PWSTR,
836            Win32::{
837                Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE},
838                System::{
839                    Diagnostics::ToolHelp::{
840                        CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
841                        TH32CS_SNAPPROCESS,
842                    },
843                    Threading::{
844                        GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
845                        WaitForSingleObject, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
846                        PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
847                    },
848                },
849            },
850        };
851
852        /// A kernel handle closed on drop.
853        struct Owned(HANDLE);
854        impl Drop for Owned {
855            fn drop(&mut self) {
856                // SAFETY: the handle came from a successful open or snapshot and is closed once.
857                let _ = unsafe { CloseHandle(self.0) };
858            }
859        }
860
861        fn open(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Option<Owned>> {
862            // SAFETY: OpenProcess takes plain values and returns a new handle.
863            match unsafe { OpenProcess(access, false, pid) } {
864                Ok(handle) => Ok(Some(Owned(handle))),
865                Err(error) if error.code() == ERROR_INVALID_PARAMETER.to_hresult() => Ok(None),
866                Err(error) => Err(os_error(error)),
867            }
868        }
869
870        /// `None` once the process has exited (its handle can outlive it).
871        fn observe(process: &Owned) -> io::Result<Option<Seen>> {
872            // SAFETY: the handle is open with SYNCHRONIZE.
873            if unsafe { WaitForSingleObject(process.0, 0) }.0 == 0 {
874                return Ok(None);
875            }
876            let (mut created, mut exited, mut kernel, mut user) = (
877                FILETIME::default(),
878                FILETIME::default(),
879                FILETIME::default(),
880                FILETIME::default(),
881            );
882            // SAFETY: the handle is open with QUERY_LIMITED_INFORMATION; the outputs are valid.
883            unsafe {
884                GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user)
885            }
886            .map_err(os_error)?;
887            let ticks =
888                (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
889            let mut buffer = vec![0u16; 32768];
890            let mut length = buffer.len() as u32;
891            // SAFETY: the buffer holds `length` UTF-16 units.
892            let exe = unsafe {
893                QueryFullProcessImageNameW(
894                    process.0,
895                    PROCESS_NAME_WIN32,
896                    PWSTR(buffer.as_mut_ptr()),
897                    &mut length,
898                )
899            }
900            .ok()
901            .map(|()| PathBuf::from(String::from_utf16_lossy(&buffer[..length as usize])));
902            Ok(Some(Seen {
903                start: StartToken {
904                    major: ticks,
905                    minor: 0,
906                },
907                exe,
908                parent: None,
909            }))
910        }
911
912        pub(in super::super) fn read(pid: u32) -> io::Result<Option<Seen>> {
913            let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
914            match open(pid, access)? {
915                Some(process) => observe(&process),
916                None => Ok(None),
917            }
918        }
919
920        pub(in super::super) fn signal(
921            identity: &AdoptedIdentity,
922            signal: super::super::Delivery,
923        ) -> io::Result<SignalResult> {
924            if matches!(
925                signal,
926                super::super::Delivery::Stop | super::super::Delivery::Continue
927            ) {
928                // Windows has no portable suspend or resume of another process.
929                return Err(io::Error::from(io::ErrorKind::Unsupported));
930            }
931            let access =
932                PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
933            let Some(process) = open(identity.pid(), access)? else {
934                return Ok(SignalResult::Gone);
935            };
936            // Proof and act use the same handle: the pid cannot change hands in between.
937            let Some(seen) = observe(&process)? else {
938                return Ok(SignalResult::Gone);
939            };
940            if !identity.matches(&seen) {
941                return Ok(SignalResult::IdentityMismatch);
942            }
943            // SAFETY: the handle is open with PROCESS_TERMINATE.
944            unsafe { TerminateProcess(process.0, 1) }.map_err(os_error)?;
945            Ok(SignalResult::Delivered)
946        }
947
948        pub(in super::super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
949            // SAFETY: a snapshot request takes plain values.
950            let snapshot = Owned(
951                unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?,
952            );
953            // SAFETY: PROCESSENTRY32W is plain data; dwSize is set below as the API requires.
954            let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
955            entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
956            // SAFETY: the snapshot is open and `entry` is a valid, sized entry.
957            unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
958            let mut pairs = Vec::new();
959            loop {
960                pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
961                // SAFETY: as above.
962                if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
963                    break;
964                }
965            }
966            Ok(pairs)
967        }
968    }
969
970    #[cfg(windows)]
971    pub(super) use windows_impl::{process_pairs, read, signal};
972
973    #[cfg(not(any(unix, windows)))]
974    pub(super) fn read(_pid: u32) -> io::Result<Option<Seen>> {
975        Err(io::Error::from(io::ErrorKind::Unsupported))
976    }
977    #[cfg(not(any(unix, windows)))]
978    pub(super) fn signal(_: &AdoptedIdentity, _: super::Delivery) -> io::Result<SignalResult> {
979        Err(io::Error::from(io::ErrorKind::Unsupported))
980    }
981    #[cfg(not(any(unix, windows)))]
982    pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
983        Err(io::Error::from(io::ErrorKind::Unsupported))
984    }
985}
986
987#[cfg(all(test, unix))]
988mod suspend_resume_tests {
989    use super::{AdoptedIdentity, SignalResult};
990    use std::{
991        num::NonZeroU32,
992        process::{Command, Stdio},
993        thread,
994        time::Duration,
995    };
996
997    fn state(pid: u32) -> String {
998        let out = Command::new("ps")
999            .args(["-o", "stat=", "-p", &pid.to_string()])
1000            .output()
1001            .expect("ps");
1002        String::from_utf8_lossy(&out.stdout).trim().to_string()
1003    }
1004
1005    fn wait_for(pid: u32, stopped: bool) -> bool {
1006        for _ in 0..100 {
1007            if state(pid).starts_with('T') == stopped {
1008                return true;
1009            }
1010            thread::sleep(Duration::from_millis(20));
1011        }
1012        false
1013    }
1014
1015    #[test]
1016    fn suspend_then_resume_stops_and_continues_the_identified_process() {
1017        let mut child = Command::new("sleep")
1018            .arg("30")
1019            .stdin(Stdio::null())
1020            .spawn()
1021            .expect("spawn sleep");
1022        let pid = NonZeroU32::new(child.id()).unwrap();
1023        let identity = AdoptedIdentity::capture(pid)
1024            .expect("capture")
1025            .expect("identity");
1026        assert_eq!(identity.suspend().unwrap(), SignalResult::Delivered);
1027        assert!(wait_for(pid.get(), true), "process did not stop");
1028        assert_eq!(identity.resume().unwrap(), SignalResult::Delivered);
1029        assert!(wait_for(pid.get(), false), "process did not continue");
1030        child.kill().expect("kill");
1031        let _ = child.wait();
1032        assert_eq!(identity.suspend().unwrap(), SignalResult::Gone);
1033    }
1034
1035    #[test]
1036    fn suspend_and_resume_refuse_this_process() {
1037        let pid = NonZeroU32::new(std::process::id()).unwrap();
1038        let identity = AdoptedIdentity::capture(pid)
1039            .expect("capture")
1040            .expect("identity");
1041        assert_eq!(
1042            identity.suspend().unwrap_err().kind(),
1043            std::io::ErrorKind::InvalidInput
1044        );
1045        assert_eq!(
1046            identity.resume().unwrap_err().kind(),
1047            std::io::ErrorKind::InvalidInput
1048        );
1049    }
1050}