Skip to main content

proc_tree/
proc.rs

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