Skip to main content

proc_tree/
tree.rs

1//! Process tree types: events, links, and node definitions.
2
3use std::fmt;
4
5use crate::traits::ProcessStore;
6
7// ---- Process events (decoupled from proc-connector) ----
8
9/// A process lifecycle event. Decoupled from any specific event source
10/// (proc-connector, audit, etc.) so users can adapt their own events.
11#[derive(Debug, Clone)]
12pub enum ProcEvent {
13    /// A new process was created. `parent_pid` is the parent.
14    Fork {
15        child_pid: u32,
16        parent_pid: u32,
17        timestamp_ns: u64,
18    },
19    /// A process executed a new program. Its cmd/user may have changed.
20    Exec { pid: u32, timestamp_ns: u64 },
21    /// A process exited. The node is preserved for historical chain lookups.
22    Exit { pid: u32 },
23}
24
25// ---- ExitedProcess (explicit removal handle) ----
26
27/// An exited process awaiting explicit removal from the store.
28///
29/// Returned by [`handle_event`](crate::handle_event) and [`handle_events`](crate::handle_events)
30/// for Exit events. The process info **stays in the store** until
31/// [`remove`](ExitedProcess::remove) is called, allowing late-arriving events
32/// to still look up process info.
33///
34/// # Example
35///
36/// ```
37/// use proc_tree::{DefaultStore, handle_event, ProcEvent, ExitedProcess, ProcessStore};
38///
39/// let store = DefaultStore::new(0);
40/// handle_event(&store, &ProcEvent::Fork { child_pid: 100, parent_pid: 1, timestamp_ns: 0 });
41///
42/// let exited = handle_event(&store, &ProcEvent::Exit { pid: 100 }).unwrap();
43/// assert_eq!(exited.pid, 100);
44///
45/// // Process still in store — caller can still query it
46/// assert!(store.get_process(100).is_some());
47///
48/// // Explicitly remove when done
49/// exited.remove(&store);
50/// assert!(store.get_process(100).is_none());
51/// ```
52#[derive(Debug, Clone, PartialEq, Eq)]
53#[must_use = "call .remove(&store) after processing related events, or the process stays in store until TTL expires"]
54pub struct ExitedProcess {
55    /// The PID of the exited process.
56    pub pid: u32,
57}
58
59impl ExitedProcess {
60    /// Get the PID of the exited process.
61    pub fn pid(&self) -> u32 {
62        self.pid
63    }
64
65    /// Remove this process from the store.
66    ///
67    /// Call this after all related events have been processed.
68    /// The process info is no longer accessible after this call.
69    pub fn remove<S: ProcessStore>(self, store: &S) {
70        store.remove_process(self.pid);
71    }
72}
73
74// ---- ProcessLink (structured chain element) ----
75
76/// A single entry in a process ancestry chain.
77///
78/// Displayed as `"pid|cmd|user"` by the `Display` impl.
79///
80/// ```
81/// use proc_tree::ProcessLink;
82///
83/// let link = ProcessLink::new(102, "touch".into(), "touch /tmp/foo".into(), "root".into());
84/// assert_eq!(link.to_string(), "102|touch /tmp/foo|root");
85/// assert_eq!(link.pid(), 102);
86/// assert_eq!(link.comm(), "touch");
87/// assert_eq!(link.cmd(), "touch /tmp/foo");
88/// assert_eq!(link.user(), "root");
89/// ```
90///
91/// A chain is a `Vec<ProcessLink>` ordered from child to ancestor.
92#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
93pub struct ProcessLink {
94    pid: u32,
95    comm: String,
96    cmd: String,
97    user: String,
98}
99
100impl ProcessLink {
101    /// Create a new `ProcessLink`.
102    pub fn new(pid: u32, comm: String, cmd: String, user: String) -> Self {
103        Self {
104            pid,
105            comm,
106            cmd,
107            user,
108        }
109    }
110
111    /// The PID of the process.
112    pub fn pid(&self) -> u32 {
113        self.pid
114    }
115
116    /// Binary name (from `/proc/{pid}/comm`).
117    pub fn comm(&self) -> &str {
118        &self.comm
119    }
120
121    /// The command name of the process.
122    pub fn cmd(&self) -> &str {
123        &self.cmd
124    }
125
126    /// The username of the process.
127    pub fn user(&self) -> &str {
128        &self.user
129    }
130}
131
132impl fmt::Display for ProcessLink {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}|{}|{}", self.pid, self.cmd, self.user)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn process_link_display_format() {
144        let link = ProcessLink::new(42, "bash".into(), "bash".into(), "root".into());
145        assert_eq!(link.to_string(), "42|bash|root");
146    }
147
148    #[test]
149    fn process_link_clone() {
150        let link = ProcessLink::new(1, "init".into(), "init".into(), "root".into());
151        let link2 = link.clone();
152        assert_eq!(link.pid(), link2.pid());
153        assert_eq!(link.cmd(), link2.cmd());
154        assert_eq!(link.user(), link2.user());
155    }
156
157    #[test]
158    fn proc_event_clone() {
159        let e = ProcEvent::Fork {
160            child_pid: 100,
161            parent_pid: 1,
162            timestamp_ns: 42,
163        };
164        let e2 = e.clone();
165        match e2 {
166            ProcEvent::Fork {
167                child_pid,
168                parent_pid,
169                timestamp_ns,
170            } => {
171                assert_eq!(child_pid, 100);
172                assert_eq!(parent_pid, 1);
173                assert_eq!(timestamp_ns, 42);
174            }
175            _ => panic!("expected Fork"),
176        }
177    }
178}