Skip to main content

monitrs_core/model/
identity.rs

1//! Stable process identity.
2//!
3//! §26: *PID alone is not a stable process identity.* A PID is reused by the
4//! kernel, so pinning, selection, history attribution, and — most importantly —
5//! signal delivery must all key on a value that changes when the process behind
6//! a PID changes.
7
8use core::fmt;
9
10/// A process identity that survives PID reuse.
11///
12/// `start_key` is an opaque, platform-supplied value derived from the process
13/// start time. It is deliberately *not* a `SystemTime`: on Linux it comes from
14/// field 22 of `/proc/<pid>/stat` in clock ticks since boot, and on macOS from
15/// the `kp_proc.p_starttime` timeval. Both are stable for the life of the
16/// process and change on reuse, which is the only property this type needs.
17///
18/// Two identities compare equal only when both the PID and the start key match,
19/// which is what makes [`crate::model::ProcessIdentity`] safe to attach to a
20/// pending signal (§6.2, §15.1).
21#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct ProcessIdentity {
24    /// The OS process identifier.
25    pub pid: u32,
26    /// An opaque platform value that changes when this PID is reused.
27    pub start_key: u64,
28}
29
30impl ProcessIdentity {
31    /// Builds an identity from a PID and a platform start key.
32    #[must_use]
33    pub const fn new(pid: u32, start_key: u64) -> Self {
34        Self { pid, start_key }
35    }
36
37    /// Whether `other` is the same PID but a *different* process.
38    ///
39    /// The signal path calls this after re-reading the live process table: a
40    /// `true` result means the PID was reused and the pending action must abort
41    /// rather than signal an unrelated process (§6.2).
42    #[must_use]
43    pub const fn is_reuse_of(&self, other: &Self) -> bool {
44        self.pid == other.pid && self.start_key != other.start_key
45    }
46}
47
48impl fmt::Display for ProcessIdentity {
49    /// Renders just the PID: the start key is an internal correctness device and
50    /// would be noise in the UI.
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{}", self.pid)
53    }
54}
55
56/// The owning user of a process.
57#[derive(Clone, Debug, Eq, PartialEq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct UserIdentity {
60    /// Numeric user id.
61    pub uid: u32,
62    /// Resolved user name, when the OS lets us look it up.
63    ///
64    /// Name resolution can fail or be denied for another user's process, which
65    /// is why the numeric id is always present and the name is not.
66    pub name: Option<Box<str>>,
67}
68
69impl UserIdentity {
70    /// The name if resolved, otherwise the numeric id rendered as text.
71    #[must_use]
72    pub fn display_name(&self) -> String {
73        match &self.name {
74            Some(name) => name.to_string(),
75            None => self.uid.to_string(),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn identity_requires_both_pid_and_start_key_to_match() {
86        let a = ProcessIdentity::new(31842, 900_100);
87        let b = ProcessIdentity::new(31842, 900_100);
88        let recycled = ProcessIdentity::new(31842, 977_400);
89        let other = ProcessIdentity::new(1221, 900_100);
90
91        assert_eq!(a, b);
92        assert_ne!(a, recycled);
93        assert_ne!(a, other);
94    }
95
96    #[test]
97    fn pid_reuse_is_detected_and_a_different_pid_is_not_reuse() {
98        let pinned = ProcessIdentity::new(31842, 900_100);
99        let recycled = ProcessIdentity::new(31842, 977_400);
100        let unrelated = ProcessIdentity::new(1221, 977_400);
101
102        assert!(
103            recycled.is_reuse_of(&pinned),
104            "same PID, different start key"
105        );
106        assert!(!pinned.is_reuse_of(&pinned), "identical is not reuse");
107        assert!(
108            !unrelated.is_reuse_of(&pinned),
109            "different PID is not reuse"
110        );
111    }
112
113    #[test]
114    fn display_shows_only_the_pid() {
115        assert_eq!(ProcessIdentity::new(31842, 900_100).to_string(), "31842");
116    }
117
118    #[test]
119    fn unresolvable_user_names_fall_back_to_the_numeric_id() {
120        let named = UserIdentity {
121            uid: 501,
122            name: Some("gabor".into()),
123        };
124        let anonymous = UserIdentity {
125            uid: 501,
126            name: None,
127        };
128        assert_eq!(named.display_name(), "gabor");
129        assert_eq!(anonymous.display_name(), "501");
130    }
131}