1use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use arrayvec::ArrayString;
13
14const UNKNOWN: &str = "unknown";
15
16fn clock_ticks_per_sec() -> i64 {
21 static TICKS: OnceLock<i64> = OnceLock::new();
22 *TICKS.get_or_init(|| {
23 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
27 if ticks <= 0 { 100 } else { ticks }
28 })
29}
30
31pub fn read_proc_comm(pid: u32) -> Option<String> {
41 let path = proc_path(pid, "comm");
42 let mut buf = [0u8; 64];
43 let mut file = std::fs::File::open(path.as_str()).ok()?;
44 use std::io::Read;
45 let n = file.read(&mut buf).ok()?;
46 let s = std::str::from_utf8(&buf[..n]).ok()?;
47 Some(s.trim().to_string())
48}
49
50fn proc_path(pid: u32, suffix: &str) -> ArrayString<32> {
52 use std::fmt::Write;
53 let mut buf = ArrayString::new();
54 write!(buf, "/proc/{pid}/{suffix}").unwrap();
55 buf
56}
57
58pub fn read_proc_start_time_ns(pid: u32) -> u64 {
68 let path = proc_path(pid, "stat");
69 let stat = match std::fs::read_to_string(path.as_str()) {
70 Ok(s) => s,
71 Err(_) => return 0,
72 };
73 let after_comm = match stat.rfind(") ") {
76 Some(pos) => pos + 2,
77 None => return 0,
78 };
79 let mut rest = &stat[after_comm..];
80 for _ in 0..19 {
85 if let Some(pos) = rest.find(' ') {
86 rest = &rest[pos + 1..];
87 } else {
88 return 0;
89 }
90 }
91 let starttime_jiffies: u64 = match rest.split_whitespace().next() {
92 Some(s) => s.parse().unwrap_or(0),
93 None => return 0,
94 };
95 if starttime_jiffies == 0 {
96 return 0;
97 }
98 (starttime_jiffies as u128 * 1_000_000_000 / clock_ticks_per_sec() as u128) as u64
99}
100
101fn uid_passwd_map() -> &'static HashMap<u32, String> {
104 static MAP: OnceLock<HashMap<u32, String>> = OnceLock::new();
105 MAP.get_or_init(|| {
106 let mut map = HashMap::new();
107 if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
108 for entry in passwd.lines() {
109 let mut parts = entry.splitn(4, ':');
110 let name = parts.next();
111 let _password = parts.next();
112 let uid_str = parts.next();
113 if let (Some(name), Some(uid_str)) = (name, uid_str)
114 && let Ok(uid) = uid_str.parse::<u32>()
115 {
116 map.insert(uid, name.to_string());
117 }
118 }
119 }
120 map
121 })
122}
123
124pub fn read_proc_cmdline(pid: u32) -> Option<String> {
130 let path = proc_path(pid, "cmdline");
131 let bytes = std::fs::read(path.as_str()).ok()?;
132 if bytes.is_empty() {
133 return None;
134 }
135 let s = String::from_utf8_lossy(&bytes);
137 let trimmed = s.trim_end_matches('\0');
138 Some(trimmed.replace('\0', " "))
139}
140
141pub fn parse_proc_entry(pid: u32) -> Option<crate::types::ProcessInfo> {
158 let path = proc_path(pid, "status");
159 let status = std::fs::read_to_string(path.as_str()).ok()?;
160 let mut ppid = 0u32;
161 let mut name = String::new();
162 let mut user = String::new();
163 let mut tgid = 0u32;
164 for line in status.lines() {
165 if let Some(val) = line.strip_prefix("PPid:") {
166 ppid = val.trim().parse().unwrap_or(0);
167 } else if let Some(val) = line.strip_prefix("Name:") {
168 name = val.trim().to_string();
169 } else if let Some(val) = line.strip_prefix("Uid:") {
170 if let Some(uid_str) = val.split_whitespace().next()
171 && let Ok(uid) = uid_str.parse::<u32>()
172 {
173 user = uid_to_username(uid).unwrap_or(UNKNOWN).to_owned();
174 } else {
175 user = UNKNOWN.to_string();
176 }
177 } else if let Some(val) = line.strip_prefix("Tgid:") {
178 tgid = val.trim().parse().unwrap_or(0);
179 }
180 }
181 let cmd = read_proc_cmdline(pid).unwrap_or(name);
184 let start_time_ns = read_proc_start_time_ns(pid);
185 Some(crate::types::ProcessInfo {
186 cmd,
187 user,
188 ppid,
189 tgid,
190 start_time_ns,
191 })
192}
193
194pub fn uid_to_username(uid: u32) -> Option<&'static str> {
204 uid_passwd_map().get(&uid).map(|s| s.as_str())
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_read_proc_comm_pid1() {
213 let comm = read_proc_comm(1);
214 assert!(comm.is_some(), "PID 1 should exist");
215 assert!(!comm.unwrap().is_empty());
216 }
217
218 #[test]
219 fn test_read_proc_comm_nonexistent() {
220 assert!(read_proc_comm(0x7FFFFFFF).is_none());
221 }
222
223 #[test]
224 fn test_read_proc_start_time_ns_pid1() {
225 let ns = read_proc_start_time_ns(1);
226 assert!(ns > 0, "PID 1 start_time_ns should be > 0, got {ns}");
227 }
228
229 #[test]
230 fn test_read_proc_start_time_ns_nonexistent() {
231 assert_eq!(read_proc_start_time_ns(0x7FFFFFFF), 0);
232 }
233
234 #[test]
235 fn test_uid_to_username_root() {
236 let name = uid_to_username(0);
238 assert_eq!(name, Some("root"));
239 }
240
241 #[test]
242 fn test_uid_to_username_nonexistent() {
243 assert!(uid_to_username(0xFFFFFFFF).is_none());
245 }
246
247 #[test]
248 fn test_read_proc_cmdline_pid1() {
249 let cmdline = read_proc_cmdline(1);
250 assert!(cmdline.is_some(), "PID 1 should have a cmdline");
251 let s = cmdline.unwrap();
252 assert!(!s.is_empty());
253 assert!(
255 !s.contains('\0'),
256 "cmdline should not contain NUL bytes: {:?}",
257 s
258 );
259 }
260
261 #[test]
262 fn test_read_proc_cmdline_nonexistent() {
263 assert!(read_proc_cmdline(0x7FFFFFFF).is_none());
264 }
265
266 #[test]
267 fn test_read_proc_cmdline_kernel_thread() {
268 let cmdline = read_proc_cmdline(2);
270 assert!(
272 cmdline.is_none(),
273 "kernel thread PID 2 should have empty cmdline"
274 );
275 }
276
277 #[test]
278 fn test_parse_proc_entry_uses_full_cmdline() {
279 let info = parse_proc_entry(1).expect("PID 1 should exist");
281 assert!(
284 info.cmd.len() > 15 || !info.cmd.contains(' '),
285 "PID 1 cmd should be full cmdline, got: {:?}",
286 info.cmd
287 );
288 }
289}