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}