1use std::collections::HashMap;
7use std::sync::OnceLock;
8
9use arrayvec::ArrayString;
10
11fn clock_ticks_per_sec() -> i64 {
16 static TICKS: OnceLock<i64> = OnceLock::new();
17 *TICKS.get_or_init(|| {
18 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
22 if ticks <= 0 { 100 } else { ticks }
23 })
24}
25
26pub 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
47fn 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
55pub 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
85pub 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 let after_comm = match stat.rfind(") ") {
107 Some(pos) => pos + 2,
108 None => return 0,
109 };
110 let mut rest = &stat[after_comm..];
111 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
132fn 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(); 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
155pub 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
198pub 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 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 assert!(uid_to_username(0xFFFFFFFF).is_none());
261 }
262}