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 user, ppid, tgid from `/proc/{pid}/status` in one pass.
56///
57/// Returns `None` if the process doesn't exist or parsing fails.
58///
59/// ```no_run
60/// use proc_tree::proc::read_proc_status_fields;
61///
62/// let (user, ppid, tgid) = read_proc_status_fields(1).unwrap();
63/// assert_eq!(user, "root");
64/// assert_eq!(ppid, 0); // PID 1 has no parent
65/// ```
66pub fn read_proc_status_fields(pid: u32) -> Option<(String, u32, u32)> {
67    let path = proc_path(pid, "status");
68    let status = std::fs::read_to_string(path.as_str()).ok()?;
69    let mut user = String::new();
70    let mut ppid = 0u32;
71    let mut tgid = 0u32;
72    for line in status.lines() {
73        if let Some(val) = line.strip_prefix("Uid:") {
74            let uid: u32 = val.split_whitespace().next()?.parse().ok()?;
75            user = uid_to_username(uid).unwrap_or_else(|| "unknown".to_string());
76        } else if let Some(val) = line.strip_prefix("PPid:") {
77            ppid = val.trim().parse().ok()?;
78        } else if let Some(val) = line.strip_prefix("Tgid:") {
79            tgid = val.trim().parse().ok()?;
80        }
81    }
82    Some((user, ppid, tgid))
83}
84
85/// Read the process start time in nanoseconds from `/proc/{pid}/stat`.
86///
87/// Returns 0 if the process doesn't exist or parsing fails.
88/// The value is jiffies-since-boot converted to nanoseconds.
89///
90/// ```no_run
91/// use proc_tree::proc::read_proc_start_time_ns;
92///
93/// let ns = read_proc_start_time_ns(1);
94/// assert!(ns > 0); // PID 1 always has a start time
95///
96/// assert_eq!(read_proc_start_time_ns(0xFFFF_FFFF), 0); // nonexistent
97/// ```
98pub fn read_proc_start_time_ns(pid: u32) -> u64 {
99    let path = proc_path(pid, "stat");
100    let stat = match std::fs::read_to_string(path.as_str()) {
101        Ok(s) => s,
102        Err(_) => return 0,
103    };
104    // Skip comm field (which may contain spaces and ')') by finding the
105    // last ')' followed by a space — this is the standard Linux convention.
106    let after_comm = match stat.rfind(") ") {
107        Some(pos) => pos + 2,
108        None => return 0,
109    };
110    let mut rest = &stat[after_comm..];
111    // Fields after comm: state, ppid, pgrp, session, tty_nr, tpgid,
112    // flags, minflt, cminflt, majflt, cmajflt, utime, stime, cutime,
113    // cstime, priority, nice, num_threads, itrealvalue, starttime
114    // That's 19 fields to skip (indices 3..22, 0-indexed from after comm).
115    for _ in 0..19 {
116        if let Some(pos) = rest.find(' ') {
117            rest = &rest[pos + 1..];
118        } else {
119            return 0;
120        }
121    }
122    let starttime_jiffies: u64 = match rest.split_whitespace().next() {
123        Some(s) => s.parse().unwrap_or(0),
124        None => return 0,
125    };
126    if starttime_jiffies == 0 {
127        return 0;
128    }
129    (starttime_jiffies as u128 * 1_000_000_000 / clock_ticks_per_sec() as u128) as u64
130}
131
132// ---- UID → username lookup ----
133
134fn uid_passwd_map() -> &'static HashMap<u32, String> {
135    static MAP: OnceLock<HashMap<u32, String>> = OnceLock::new();
136    MAP.get_or_init(|| {
137        let mut map = HashMap::new();
138        if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
139            for entry in passwd.lines() {
140                let mut parts = entry.splitn(4, ':');
141                let name = parts.next();
142                let _shell = parts.next(); // password field
143                let uid_str = parts.next();
144                if let (Some(name), Some(uid_str)) = (name, uid_str)
145                    && let Ok(uid) = uid_str.parse::<u32>()
146                {
147                    map.insert(uid, name.to_string());
148                }
149            }
150        }
151        map
152    })
153}
154
155/// Parse `/proc/{pid}/status` into a `(PidNode, ProcInfo)` pair.
156///
157/// Returns `None` if the process doesn't exist or the status file can't be read.
158pub fn parse_proc_entry(pid: u32) -> Option<(crate::types::PidNode, crate::types::ProcInfo)> {
159    let path = proc_path(pid, "status");
160    let status = std::fs::read_to_string(path.as_str()).ok()?;
161    let mut ppid = 0u32;
162    let mut cmd = String::new();
163    let mut user = String::new();
164    let mut tgid = 0u32;
165    for line in status.lines() {
166        if let Some(val) = line.strip_prefix("PPid:") {
167            ppid = val.trim().parse().unwrap_or(0);
168        } else if let Some(val) = line.strip_prefix("Name:") {
169            cmd = val.trim().to_string();
170        } else if let Some(val) = line.strip_prefix("Uid:") {
171            if let Some(uid_str) = val.split_whitespace().next()
172                && let Ok(uid) = uid_str.parse::<u32>()
173            {
174                user = uid_to_username(uid).unwrap_or_else(|| "unknown".to_string());
175            } else {
176                user = "unknown".to_string();
177            }
178        } else if let Some(val) = line.strip_prefix("Tgid:") {
179            tgid = val.trim().parse().unwrap_or(0);
180        }
181    }
182    let start_time_ns = read_proc_start_time_ns(pid);
183    Some((
184        crate::types::PidNode {
185            ppid,
186            cmd: cmd.clone(),
187        },
188        crate::types::ProcInfo {
189            cmd,
190            user,
191            ppid,
192            tgid,
193            start_time_ns,
194        },
195    ))
196}
197
198/// Convert a UID to a username by looking up `/etc/passwd`.
199///
200/// Results are cached after the first call. Returns `None` if the UID
201/// is not found in `/etc/passwd`.
202///
203/// ```no_run
204/// use proc_tree::proc::uid_to_username;
205///
206/// assert_eq!(uid_to_username(0).as_deref(), Some("root"));
207/// assert!(uid_to_username(0xFFFF_FFFF).is_none());
208/// ```
209pub fn uid_to_username(uid: u32) -> Option<String> {
210    uid_passwd_map().get(&uid).cloned()
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_read_proc_comm_pid1() {
219        let comm = read_proc_comm(1);
220        assert!(comm.is_some(), "PID 1 should exist");
221        assert!(!comm.unwrap().is_empty());
222    }
223
224    #[test]
225    fn test_read_proc_comm_nonexistent() {
226        assert!(read_proc_comm(0x7FFFFFFF).is_none());
227    }
228
229    #[test]
230    fn test_read_proc_status_fields_pid1() {
231        let result = read_proc_status_fields(1);
232        assert!(result.is_some(), "PID 1 should have status");
233        let (user, ppid, tgid) = result.unwrap();
234        assert!(!user.is_empty());
235        assert_eq!(ppid, 0, "PID 1's ppid should be 0");
236        assert_eq!(tgid, 1, "PID 1's tgid should be 1");
237    }
238
239    #[test]
240    fn test_read_proc_start_time_ns_pid1() {
241        let ns = read_proc_start_time_ns(1);
242        assert!(ns > 0, "PID 1 start_time_ns should be > 0, got {ns}");
243    }
244
245    #[test]
246    fn test_read_proc_start_time_ns_nonexistent() {
247        assert_eq!(read_proc_start_time_ns(0x7FFFFFFF), 0);
248    }
249
250    #[test]
251    fn test_uid_to_username_root() {
252        // root (UID 0) should always exist on Linux
253        let name = uid_to_username(0);
254        assert_eq!(name.as_deref(), Some("root"));
255    }
256
257    #[test]
258    fn test_uid_to_username_nonexistent() {
259        // UID 0xFFFFFFFF almost certainly doesn't exist
260        assert!(uid_to_username(0xFFFFFFFF).is_none());
261    }
262}