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