1use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use arrayvec::ArrayString;
13
14fn clock_ticks_per_sec() -> i64 {
19 static TICKS: OnceLock<i64> = OnceLock::new();
20 *TICKS.get_or_init(|| {
21 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
25 if ticks <= 0 { 100 } else { ticks }
26 })
27}
28
29pub 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
48fn 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
56pub 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 let after_comm = match stat.rfind(") ") {
74 Some(pos) => pos + 2,
75 None => return 0,
76 };
77 let mut rest = &stat[after_comm..];
78 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
99fn 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(); 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
122pub fn read_proc_cmdline(pid: u32) -> Option<String> {
128 let path = proc_path(pid, "cmdline");
129 let bytes = std::fs::read(path.as_str()).ok()?;
130 if bytes.is_empty() {
131 return None;
132 }
133 let s = String::from_utf8_lossy(&bytes);
135 let trimmed = s.trim_end_matches('\0');
136 Some(trimmed.replace('\0', " "))
137}
138
139pub fn parse_proc_entry(pid: u32) -> Option<crate::types::ProcessInfo> {
156 let path = proc_path(pid, "status");
157 let status = std::fs::read_to_string(path.as_str()).ok()?;
158 let mut ppid = 0u32;
159 let mut name = String::new();
160 let mut user = String::new();
161 let mut tgid = 0u32;
162 for line in status.lines() {
163 if let Some(val) = line.strip_prefix("PPid:") {
164 ppid = val.trim().parse().unwrap_or(0);
165 } else if let Some(val) = line.strip_prefix("Name:") {
166 name = val.trim().to_string();
167 } else if let Some(val) = line.strip_prefix("Uid:") {
168 if let Some(uid_str) = val.split_whitespace().next()
169 && let Ok(uid) = uid_str.parse::<u32>()
170 {
171 user = uid_to_username(uid).unwrap_or_else(|| "unknown".to_string());
172 } else {
173 user = "unknown".to_string();
174 }
175 } else if let Some(val) = line.strip_prefix("Tgid:") {
176 tgid = val.trim().parse().unwrap_or(0);
177 }
178 }
179 let cmd = read_proc_cmdline(pid).unwrap_or(name);
182 let start_time_ns = read_proc_start_time_ns(pid);
183 Some(crate::types::ProcessInfo {
184 cmd,
185 user,
186 ppid,
187 tgid,
188 start_time_ns,
189 })
190}
191
192pub fn uid_to_username(uid: u32) -> Option<String> {
202 uid_passwd_map().get(&uid).cloned()
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_read_proc_comm_pid1() {
211 let comm = read_proc_comm(1);
212 assert!(comm.is_some(), "PID 1 should exist");
213 assert!(!comm.unwrap().is_empty());
214 }
215
216 #[test]
217 fn test_read_proc_comm_nonexistent() {
218 assert!(read_proc_comm(0x7FFFFFFF).is_none());
219 }
220
221 #[test]
222 fn test_read_proc_start_time_ns_pid1() {
223 let ns = read_proc_start_time_ns(1);
224 assert!(ns > 0, "PID 1 start_time_ns should be > 0, got {ns}");
225 }
226
227 #[test]
228 fn test_read_proc_start_time_ns_nonexistent() {
229 assert_eq!(read_proc_start_time_ns(0x7FFFFFFF), 0);
230 }
231
232 #[test]
233 fn test_uid_to_username_root() {
234 let name = uid_to_username(0);
236 assert_eq!(name.as_deref(), Some("root"));
237 }
238
239 #[test]
240 fn test_uid_to_username_nonexistent() {
241 assert!(uid_to_username(0xFFFFFFFF).is_none());
243 }
244
245 #[test]
246 fn test_read_proc_cmdline_pid1() {
247 let cmdline = read_proc_cmdline(1);
248 assert!(cmdline.is_some(), "PID 1 should have a cmdline");
249 let s = cmdline.unwrap();
250 assert!(!s.is_empty());
251 assert!(
253 !s.contains('\0'),
254 "cmdline should not contain NUL bytes: {:?}",
255 s
256 );
257 }
258
259 #[test]
260 fn test_read_proc_cmdline_nonexistent() {
261 assert!(read_proc_cmdline(0x7FFFFFFF).is_none());
262 }
263
264 #[test]
265 fn test_read_proc_cmdline_kernel_thread() {
266 let cmdline = read_proc_cmdline(2);
268 assert!(
270 cmdline.is_none(),
271 "kernel thread PID 2 should have empty cmdline"
272 );
273 }
274
275 #[test]
276 fn test_parse_proc_entry_uses_full_cmdline() {
277 let info = parse_proc_entry(1).expect("PID 1 should exist");
279 assert!(
282 info.cmd.len() > 15 || !info.cmd.contains(' '),
283 "PID 1 cmd should be full cmdline, got: {:?}",
284 info.cmd
285 );
286 }
287}