Skip to main content

processkit/
member.rs

1//! [`MemberInfo`] — an enriched, point-in-time snapshot of one process in a
2//! [`ProcessGroup`](crate::ProcessGroup)'s tree.
3
4/// An enriched snapshot of one member of a [`ProcessGroup`](crate::ProcessGroup)
5/// — its pid plus best-effort metadata (parent pid, image name, start time).
6///
7/// Produced two ways, both filling the same fields the same way: by
8/// [`ProcessGroup::members_info`](crate::ProcessGroup::members_info) — the
9/// metadata-carrying companion to [`members`](crate::ProcessGroup::members) (which
10/// returns bare pids) — for a *member* of a group, and by the free-standing
11/// [`process_info`](crate::process_info) query for an **arbitrary** pid the caller
12/// holds *outside* any group. From `members_info`, *which* processes appear follows
13/// the **same** platform matrix as `members` — the whole tree on Windows and
14/// Linux-cgroup, the tracked group *leaders* on the POSIX process-group fallback
15/// (macOS/BSD and Linux without a usable cgroup). The enriching fields beyond
16/// [`pid`](Self::pid) are each independently `Option` and are `None` wherever the
17/// platform can't report them — never a fabricated value.
18///
19/// # Field availability by platform
20///
21/// | field                        | Windows | Linux (cgroup / fallback) | macOS  | the BSDs |
22/// |------------------------------|---------|---------------------------|--------|----------|
23/// | [`pid`](Self::pid)           | yes     | yes                       | yes    | yes      |
24/// | [`ppid`](Self::ppid)         | yes     | yes                       | yes    | `None`   |
25/// | [`exe_name`](Self::exe_name) | yes     | yes                       | yes    | `None`   |
26/// | [`start_time`](Self::start_time) | yes | yes                    | yes    | `None`   |
27///
28/// On the "bare" BSDs the crate wires up no per-process introspection (see the
29/// note on [`start_time`](Self::start_time) for why), so every enriching field is
30/// honestly `None` while the pid is still reported — that is a correct result, not
31/// an error.
32///
33/// # No command line
34///
35/// The raw argv / environment of a member is **deliberately never** included, on
36/// any platform: a command line routinely carries secrets, and redaction or
37/// hashing is a policy the *consumer* must own — the same "never log argv/env"
38/// stance the crate takes in its `tracing` output. This will not change.
39///
40/// # Racing a member that exits
41///
42/// The list is a point-in-time snapshot taken per pid: if a process exits between
43/// when its pid is enumerated and when its metadata is read, that pid is simply
44/// **omitted** from the returned `Vec` — a vanished member is never reported with
45/// fabricated fields, and its disappearance never fails the whole call. See
46/// [`ProcessGroup::members_info`](crate::ProcessGroup::members_info) for the exact
47/// error contract.
48///
49/// Non-exhaustive and accessor-only: a read-only snapshot the crate produces, so
50/// new metadata can be added without a breaking change, and each field is exposed
51/// through a method — documenting its own platform caveats — rather than a public
52/// struct field.
53#[non_exhaustive]
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct MemberInfo {
56    pid: u32,
57    ppid: Option<u32>,
58    exe_name: Option<String>,
59    start_time: Option<u64>,
60}
61
62impl MemberInfo {
63    /// Assemble one snapshot record. Called only by the platform backends, which
64    /// fill each field to whatever the OS could report (`None` where it can't).
65    pub(crate) fn new(
66        pid: u32,
67        ppid: Option<u32>,
68        exe_name: Option<String>,
69        start_time: Option<u64>,
70    ) -> Self {
71        Self {
72            pid,
73            ppid,
74            exe_name,
75            start_time,
76        }
77    }
78
79    /// The member's process id.
80    ///
81    /// Always present (it is the key the record is built around). Point-in-time,
82    /// exactly like a pid from [`members`](crate::ProcessGroup::members): the
83    /// process may exit immediately afterwards, and the number is only as stable
84    /// as the OS's reuse policy — pair it with [`start_time`](Self::start_time) to
85    /// tell a recycled number apart from the original process.
86    pub fn pid(&self) -> u32 {
87        self.pid
88    }
89
90    /// The member's parent process id, or `None` where the platform can't report
91    /// one.
92    ///
93    /// - **Windows** — `th32ParentProcessID` from a `Toolhelp32` process snapshot.
94    ///   Windows does not reparent orphans, so if the parent already exited this
95    ///   pid may name nothing (or, after reuse, an unrelated process).
96    /// - **Linux** — field 4 of `/proc/<pid>/stat`.
97    /// - **macOS** — `proc_pidinfo(PROC_PIDTBSDINFO)`'s `pbi_ppid`.
98    /// - **the BSDs** — always `None` (no wired-up reader).
99    pub fn ppid(&self) -> Option<u32> {
100        self.ppid
101    }
102
103    /// The member's image (executable) name, or `None` where the platform can't
104    /// report one.
105    ///
106    /// A short **base name** — never a full path, and never a command line (see
107    /// the type-level "No command line" note):
108    /// - **Windows** — the executable file name from the `Toolhelp32` snapshot
109    ///   (`szExeFile`, e.g. `worker.exe`).
110    /// - **Linux** — the kernel `comm` (field 2 of `/proc/<pid>/stat`): truncated
111    ///   to 15 bytes and mutable via `prctl(PR_SET_NAME)`, so it is the process's
112    ///   current name (usually, but not guaranteed to be, the exec base name)
113    ///   rather than a canonical path. It is read from the *same single*
114    ///   `/proc/<pid>/stat` line as [`ppid`](Self::ppid) and
115    ///   [`start_time`](Self::start_time), so the three describe one consistent
116    ///   instant. (The full path via a `/proc/<pid>/exe` `readlink` is deliberately
117    ///   not used — it needs a second syscall and is denied without ptrace-class
118    ///   access after a uid change.)
119    /// - **macOS** — `proc_bsdinfo::pbi_comm`, likewise a short truncated `comm`.
120    /// - **the BSDs** — always `None`.
121    pub fn exe_name(&self) -> Option<&str> {
122        self.exe_name.as_deref()
123    }
124
125    /// The member's start-time token, or `None` where the platform can't report
126    /// one — an **opaque identity anchor, not a wall-clock timestamp**.
127    ///
128    /// Its sole purpose is telling a recycled pid apart from the original: it is
129    /// fixed at process creation and differs for a later process that reuses the
130    /// number, so two snapshots whose [`pid`](Self::pid) **and** `start_time` both
131    /// match name the same process instance. **Do not interpret the number, and do
132    /// not compare it across platforms** — the unit and epoch are platform-specific:
133    /// - **Windows** — the process-creation `FILETIME`: 100-nanosecond intervals
134    ///   since 1601-01-01 UTC.
135    /// - **Linux** — `/proc/<pid>/stat` field 22 (`starttime`): clock ticks
136    ///   (`sysconf(_SC_CLK_TCK)`, typically 100 Hz) since system boot.
137    /// - **macOS** — start time in **microseconds since the Unix epoch**
138    ///   (`proc_bsdinfo`'s `pbi_start_tvsec`·10⁶ + `pbi_start_tvusec`).
139    /// - **the BSDs** — always `None`: the start time lives in `kinfo_proc`,
140    ///   reachable only through per-OS `sysctl(KERN_PROC)` layouts with no hosted
141    ///   CI runner to verify a reader, so the crate ships none rather than an
142    ///   unverifiable one (the same reason the pgroup backend reads no identity
143    ///   token there).
144    pub fn start_time(&self) -> Option<u64> {
145        self.start_time
146    }
147}