Skip to main content

proc_tree/
proc.rs

1//! Raw /proc reading for process tree construction.
2//!
3//! These are internal functions used by the process tree operations.
4//! They are not part of the public API.
5//!
6//! Contains functions needed to build and maintain the process tree:
7//! comm (cmd name), status (ppid/user/tgid), stat (start_time), uid lookup.
8
9use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use arrayvec::ArrayString;
13
14/// Clock ticks per second (POSIX `sysconf(_SC_CLK_TCK)`).
15///
16/// Returns 100 as fallback — the overwhelmingly common value on Linux.
17/// Cached after the first call since the value never changes at runtime.
18fn clock_ticks_per_sec() -> i64 {
19    static TICKS: OnceLock<i64> = OnceLock::new();
20    *TICKS.get_or_init(|| {
21        // SAFETY: sysconf(_SC_CLK_TCK) is a pure read-only query with no
22        // side effects, cannot fail or cause UB. It returns a system-wide
23        // constant that is set at boot and never changes.
24        let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
25        if ticks <= 0 { 100 } else { ticks }
26    })
27}
28
29/// Read the command name for a PID from `/proc/{pid}/comm`.
30///
31/// Returns `None` if the process doesn't exist or the file can't be read.
32///
33/// # Internal Usage
34///
35/// This function is used internally by `ops::resolve` and
36/// `ops::get_cmd` as a fallback when the command name is not
37/// in the store.
38pub fn read_proc_comm(pid: u32) -> Option<String> {
39    let path = proc_path(pid, "comm");
40    let mut buf = [0u8; 64];
41    let mut file = std::fs::File::open(path.as_str()).ok()?;
42    use std::io::Read;
43    let n = file.read(&mut buf).ok()?;
44    let s = std::str::from_utf8(&buf[..n]).ok()?;
45    Some(s.trim().to_string())
46}
47
48/// Format `/proc/{pid}/{suffix}` into a stack-allocated string.
49fn proc_path(pid: u32, suffix: &str) -> ArrayString<32> {
50    use std::fmt::Write;
51    let mut buf = ArrayString::new();
52    write!(buf, "/proc/{pid}/{suffix}").unwrap();
53    buf
54}
55
56/// Read the process start time in nanoseconds from `/proc/{pid}/stat`.
57///
58/// Returns 0 if the process doesn't exist or parsing fails.
59/// The value is jiffies-since-boot converted to nanoseconds.
60///
61/// # Internal Usage
62///
63/// This function is used internally by [`parse_proc_entry`] to populate
64/// the `start_time_ns` field of [`super::types::ProcessInfo`].
65pub fn read_proc_start_time_ns(pid: u32) -> u64 {
66    let path = proc_path(pid, "stat");
67    let stat = match std::fs::read_to_string(path.as_str()) {
68        Ok(s) => s,
69        Err(_) => return 0,
70    };
71    // Skip comm field (which may contain spaces and ')') by finding the
72    // last ')' followed by a space — this is the standard Linux convention.
73    let after_comm = match stat.rfind(") ") {
74        Some(pos) => pos + 2,
75        None => return 0,
76    };
77    let mut rest = &stat[after_comm..];
78    // Fields after comm: state, ppid, pgrp, session, tty_nr, tpgid,
79    // flags, minflt, cminflt, majflt, cmajflt, utime, stime, cutime,
80    // cstime, priority, nice, num_threads, itrealvalue, starttime
81    // That's 19 fields to skip (indices 3..22, 0-indexed from after comm).
82    for _ in 0..19 {
83        if let Some(pos) = rest.find(' ') {
84            rest = &rest[pos + 1..];
85        } else {
86            return 0;
87        }
88    }
89    let starttime_jiffies: u64 = match rest.split_whitespace().next() {
90        Some(s) => s.parse().unwrap_or(0),
91        None => return 0,
92    };
93    if starttime_jiffies == 0 {
94        return 0;
95    }
96    (starttime_jiffies as u128 * 1_000_000_000 / clock_ticks_per_sec() as u128) as u64
97}
98
99// ---- UID → username lookup ----
100
101fn uid_passwd_map() -> &'static HashMap<u32, String> {
102    static MAP: OnceLock<HashMap<u32, String>> = OnceLock::new();
103    MAP.get_or_init(|| {
104        let mut map = HashMap::new();
105        if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
106            for entry in passwd.lines() {
107                let mut parts = entry.splitn(4, ':');
108                let name = parts.next();
109                let _shell = parts.next(); // password field
110                let uid_str = parts.next();
111                if let (Some(name), Some(uid_str)) = (name, uid_str)
112                    && let Ok(uid) = uid_str.parse::<u32>()
113                {
114                    map.insert(uid, name.to_string());
115                }
116            }
117        }
118        map
119    })
120}
121
122/// Parse `/proc/{pid}/status` into a `ProcessInfo`.
123///
124/// Returns `None` if the process doesn't exist or the status file can't be read.
125///
126/// # Internal Usage
127///
128/// This function is used internally by:
129/// - [`super::ops::snapshot`] to populate the store
130/// - [`super::ops::resolve`] as a fallback
131/// - [`super::ops::handle_event`] for Exec events
132/// - [`super::ops::build_chain_links`] for chain lookups
133/// - [`super::ops::find_by_cmd`] and [`super::ops::find_by_user`] for fallback
134pub fn parse_proc_entry(pid: u32) -> Option<crate::types::ProcessInfo> {
135    let path = proc_path(pid, "status");
136    let status = std::fs::read_to_string(path.as_str()).ok()?;
137    let mut ppid = 0u32;
138    let mut cmd = String::new();
139    let mut user = String::new();
140    let mut tgid = 0u32;
141    for line in status.lines() {
142        if let Some(val) = line.strip_prefix("PPid:") {
143            ppid = val.trim().parse().unwrap_or(0);
144        } else if let Some(val) = line.strip_prefix("Name:") {
145            cmd = val.trim().to_string();
146        } else if let Some(val) = line.strip_prefix("Uid:") {
147            if let Some(uid_str) = val.split_whitespace().next()
148                && let Ok(uid) = uid_str.parse::<u32>()
149            {
150                user = uid_to_username(uid).unwrap_or_else(|| "unknown".to_string());
151            } else {
152                user = "unknown".to_string();
153            }
154        } else if let Some(val) = line.strip_prefix("Tgid:") {
155            tgid = val.trim().parse().unwrap_or(0);
156        }
157    }
158    let start_time_ns = read_proc_start_time_ns(pid);
159    Some(crate::types::ProcessInfo {
160        cmd,
161        user,
162        ppid,
163        tgid,
164        start_time_ns,
165    })
166}
167
168/// Convert a UID to a username by looking up `/etc/passwd`.
169///
170/// Results are cached after the first call. Returns `None` if the UID
171/// is not found in `/etc/passwd`.
172///
173/// # Internal Usage
174///
175/// This function is used internally by [`parse_proc_entry`] to populate
176/// the `user` field of [`super::types::ProcessInfo`].
177pub fn uid_to_username(uid: u32) -> Option<String> {
178    uid_passwd_map().get(&uid).cloned()
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_read_proc_comm_pid1() {
187        let comm = read_proc_comm(1);
188        assert!(comm.is_some(), "PID 1 should exist");
189        assert!(!comm.unwrap().is_empty());
190    }
191
192    #[test]
193    fn test_read_proc_comm_nonexistent() {
194        assert!(read_proc_comm(0x7FFFFFFF).is_none());
195    }
196
197    #[test]
198    fn test_read_proc_start_time_ns_pid1() {
199        let ns = read_proc_start_time_ns(1);
200        assert!(ns > 0, "PID 1 start_time_ns should be > 0, got {ns}");
201    }
202
203    #[test]
204    fn test_read_proc_start_time_ns_nonexistent() {
205        assert_eq!(read_proc_start_time_ns(0x7FFFFFFF), 0);
206    }
207
208    #[test]
209    fn test_uid_to_username_root() {
210        // root (UID 0) should always exist on Linux
211        let name = uid_to_username(0);
212        assert_eq!(name.as_deref(), Some("root"));
213    }
214
215    #[test]
216    fn test_uid_to_username_nonexistent() {
217        // UID 0xFFFFFFFF almost certainly doesn't exist
218        assert!(uid_to_username(0xFFFFFFFF).is_none());
219    }
220}