1use core::time::Duration;
10use std::time::SystemTime;
11
12use crate::model::{MetricState, ProcessIdentity, UserIdentity};
13use crate::units::{Percent, Rate};
14
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
19pub enum ProcessState {
20 Running,
22 Sleeping,
24 UninterruptibleSleep,
29 Zombie,
33 Stopped,
35 Traced,
37 Idle,
40 Dead,
42 #[default]
44 Unknown,
45}
46
47impl ProcessState {
48 #[must_use]
52 pub const fn code(self) -> char {
53 match self {
54 Self::Running => 'R',
55 Self::Sleeping => 'S',
56 Self::UninterruptibleSleep => 'D',
57 Self::Zombie => 'Z',
58 Self::Stopped => 'T',
59 Self::Traced => 't',
60 Self::Idle => 'I',
61 Self::Dead => 'X',
62 Self::Unknown => '?',
63 }
64 }
65
66 #[must_use]
68 pub const fn label(self) -> &'static str {
69 match self {
70 Self::Running => "running",
71 Self::Sleeping => "sleeping",
72 Self::UninterruptibleSleep => "uninterruptible sleep",
73 Self::Zombie => "zombie",
74 Self::Stopped => "stopped",
75 Self::Traced => "traced",
76 Self::Idle => "idle",
77 Self::Dead => "dead",
78 Self::Unknown => "unknown",
79 }
80 }
81
82 #[must_use]
84 pub const fn is_notable(self) -> bool {
85 matches!(self, Self::Zombie | Self::UninterruptibleSleep)
86 }
87
88 #[must_use]
93 pub const fn is_signalable(self) -> bool {
94 !matches!(self, Self::Zombie | Self::Dead)
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize))]
101pub struct ProcessMemory {
102 pub rss_bytes: MetricState<u64>,
104 pub virtual_bytes: MetricState<u64>,
109 pub share_of_total: MetricState<Percent>,
111}
112
113impl ProcessMemory {
114 pub const WARMING_UP: Self = Self {
116 rss_bytes: MetricState::WarmingUp,
117 virtual_bytes: MetricState::WarmingUp,
118 share_of_total: MetricState::WarmingUp,
119 };
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize))]
129pub struct ProcessIo {
130 pub read: MetricState<Rate>,
132 pub write: MetricState<Rate>,
134 pub read_total_bytes: MetricState<u64>,
136 pub write_total_bytes: MetricState<u64>,
138}
139
140impl ProcessIo {
141 pub const UNSUPPORTED: Self = Self {
143 read: MetricState::Unsupported,
144 write: MetricState::Unsupported,
145 read_total_bytes: MetricState::Unsupported,
146 write_total_bytes: MetricState::Unsupported,
147 };
148
149 pub const WARMING_UP: Self = Self {
151 read: MetricState::WarmingUp,
152 write: MetricState::WarmingUp,
153 read_total_bytes: MetricState::WarmingUp,
154 write_total_bytes: MetricState::WarmingUp,
155 };
156}
157
158#[derive(Clone, Debug, PartialEq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize))]
161pub struct ProcessSnapshot {
162 pub identity: ProcessIdentity,
164 pub parent_pid: Option<u32>,
170 pub name: Box<str>,
172 pub command: Box<str>,
178 pub exe: Option<Box<str>>,
180 pub user: MetricState<UserIdentity>,
182 pub state: ProcessState,
184 pub cpu: MetricState<Percent>,
186 pub memory: ProcessMemory,
188 pub io: ProcessIo,
190 pub threads: MetricState<u32>,
192 pub age: MetricState<Duration>,
194 pub started_at: MetricState<SystemTime>,
196 pub is_kernel_thread: bool,
198}
199
200impl ProcessSnapshot {
201 #[must_use]
206 pub fn command_or_name(&self) -> &str {
207 if self.command.is_empty() {
208 &self.name
209 } else {
210 &self.command
211 }
212 }
213
214 #[must_use]
219 pub fn redacted_command(&self) -> &str {
220 let command = self.command_or_name();
221 match command.split_once(' ') {
222 Some((program, _)) => program,
223 None => command,
224 }
225 }
226}
227
228#[derive(Clone, Debug, PartialEq)]
230#[cfg_attr(feature = "serde", derive(serde::Serialize))]
231pub struct AncestorEntry {
232 pub identity: ProcessIdentity,
234 pub name: Box<str>,
236}
237
238#[derive(Clone, Debug, PartialEq)]
245#[cfg_attr(feature = "serde", derive(serde::Serialize))]
246pub struct ProcessDetail {
247 pub identity: ProcessIdentity,
253 pub working_directory: MetricState<Box<str>>,
255 pub root: MetricState<Box<str>>,
257 pub open_files: MetricState<u32>,
259 pub sockets: MetricState<u32>,
261 pub ancestry: MetricState<Vec<AncestorEntry>>,
263 pub children: MetricState<Vec<ProcessIdentity>>,
265 pub descendants: MetricState<u32>,
267 pub nice: MetricState<i32>,
269 pub cgroup: MetricState<Box<str>>,
271 pub container: MetricState<Box<str>>,
273 pub collected_at: SystemTime,
275}
276
277impl ProcessDetail {
278 #[must_use]
280 pub fn pending(identity: ProcessIdentity, collected_at: SystemTime) -> Self {
281 Self {
282 identity,
283 working_directory: MetricState::WarmingUp,
284 root: MetricState::WarmingUp,
285 open_files: MetricState::WarmingUp,
286 sockets: MetricState::WarmingUp,
287 ancestry: MetricState::WarmingUp,
288 children: MetricState::WarmingUp,
289 descendants: MetricState::WarmingUp,
290 nice: MetricState::WarmingUp,
291 cgroup: MetricState::WarmingUp,
292 container: MetricState::WarmingUp,
293 collected_at,
294 }
295 }
296}
297
298#[derive(Clone, Debug, PartialEq)]
300pub enum ProcessDetailResult {
301 Loaded(Box<ProcessDetail>),
303 Vanished(ProcessIdentity),
307 Reused {
309 requested: ProcessIdentity,
311 found: ProcessIdentity,
313 },
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn sample(command: &str, name: &str) -> ProcessSnapshot {
321 ProcessSnapshot {
322 identity: ProcessIdentity::new(31_842, 900_100),
323 parent_pid: Some(1),
324 name: name.into(),
325 command: command.into(),
326 exe: None,
327 user: MetricState::Unsupported,
328 state: ProcessState::Running,
329 cpu: MetricState::WarmingUp,
330 memory: ProcessMemory::WARMING_UP,
331 io: ProcessIo::UNSUPPORTED,
332 threads: MetricState::Unsupported,
333 age: MetricState::Unsupported,
334 started_at: MetricState::Unsupported,
335 is_kernel_thread: false,
336 }
337 }
338
339 #[test]
340 fn notable_states_are_exactly_zombie_and_uninterruptible_sleep() {
341 assert!(ProcessState::Zombie.is_notable());
342 assert!(ProcessState::UninterruptibleSleep.is_notable());
343 for state in [
344 ProcessState::Running,
345 ProcessState::Sleeping,
346 ProcessState::Stopped,
347 ProcessState::Traced,
348 ProcessState::Idle,
349 ProcessState::Dead,
350 ProcessState::Unknown,
351 ] {
352 assert!(!state.is_notable(), "{state:?}");
353 }
354 }
355
356 #[test]
357 fn state_codes_are_unique_so_the_column_is_unambiguous() {
358 let states = [
359 ProcessState::Running,
360 ProcessState::Sleeping,
361 ProcessState::UninterruptibleSleep,
362 ProcessState::Zombie,
363 ProcessState::Stopped,
364 ProcessState::Traced,
365 ProcessState::Idle,
366 ProcessState::Dead,
367 ProcessState::Unknown,
368 ];
369 let mut codes: Vec<char> = states.iter().map(|s| s.code()).collect();
370 codes.sort_unstable();
371 codes.dedup();
372 assert_eq!(codes.len(), states.len());
373 }
374
375 #[test]
376 fn already_exited_processes_are_not_signalable() {
377 assert!(!ProcessState::Zombie.is_signalable());
378 assert!(!ProcessState::Dead.is_signalable());
379 assert!(ProcessState::Running.is_signalable());
380 assert!(ProcessState::UninterruptibleSleep.is_signalable());
381 }
382
383 #[test]
384 fn an_empty_command_falls_back_to_the_process_name() {
385 let kernel_thread = sample("", "kworker/2:1");
386 assert_eq!(kernel_thread.command_or_name(), "kworker/2:1");
387 let normal = sample("cargo build --release", "cargo");
388 assert_eq!(normal.command_or_name(), "cargo build --release");
389 }
390
391 #[test]
392 fn redaction_strips_arguments_which_may_contain_secrets() {
393 let process = sample("psql postgres://user:hunter2@db/prod", "psql");
394 assert_eq!(process.redacted_command(), "psql");
395 assert!(!process.redacted_command().contains("hunter2"));
396 }
397
398 #[test]
399 fn redaction_of_a_bare_program_keeps_the_program() {
400 assert_eq!(sample("rustc", "rustc").redacted_command(), "rustc");
401 assert_eq!(sample("", "kworker/2:1").redacted_command(), "kworker/2:1");
402 }
403
404 #[test]
405 fn a_pending_detail_reports_nothing_as_measured() {
406 let identity = ProcessIdentity::new(1, 2);
407 let detail = ProcessDetail::pending(identity, SystemTime::UNIX_EPOCH);
408 assert_eq!(detail.identity, identity);
409 assert!(detail.working_directory.is_warming_up());
410 assert!(detail.open_files.fresh().is_none());
411 }
412}