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