1#![allow(unsafe_code)]
7#![allow(rustdoc::invalid_html_tags)]
8
9use super::CpuTime;
10use std::fs;
11
12#[derive(Debug, Clone, Copy, Default)]
14pub struct DiskIo {
15 pub read_bytes: u64,
17 pub write_bytes: u64,
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct AllStats {
24 pub cpu_time: CpuTime,
25 pub memory_rss: u64,
26 pub disk_io: DiskIo,
27}
28
29fn clock_ticks_per_sec() -> u64 {
31 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
33 if ticks <= 0 { 100 } else { ticks as u64 }
34}
35
36pub fn get_cpu_time(pid: i32) -> Option<CpuTime> {
42 get_cpu_time_inner(pid, false)
43}
44
45pub fn get_cpu_time_with_children(pid: i32) -> Option<CpuTime> {
51 get_cpu_time_inner(pid, true)
52}
53
54fn get_cpu_time_inner(pid: i32, include_children: bool) -> Option<CpuTime> {
55 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
56
57 let comm_end = stat.rfind(')')?;
63 let fields: Vec<&str> = stat[comm_end + 2..].split_whitespace().collect();
64
65 if fields.len() < 15 {
67 return None;
68 }
69
70 let utime_ticks: u64 = fields[11].parse().ok()?;
71 let stime_ticks: u64 = fields[12].parse().ok()?;
72
73 let (user_ticks, system_ticks) = if include_children {
74 let cutime_ticks: i64 = fields[13].parse().ok()?; let cstime_ticks: i64 = fields[14].parse().ok()?; (
77 utime_ticks.saturating_add(cutime_ticks.max(0) as u64),
78 stime_ticks.saturating_add(cstime_ticks.max(0) as u64),
79 )
80 } else {
81 (utime_ticks, stime_ticks)
82 };
83
84 let ticks_per_sec = clock_ticks_per_sec();
85 let ns_per_tick = 1_000_000_000 / ticks_per_sec;
86
87 Some(CpuTime {
88 user_ns: user_ticks * ns_per_tick,
89 system_ns: system_ticks * ns_per_tick,
90 })
91}
92
93pub fn get_memory(pid: i32) -> Option<u64> {
98 get_statm(pid).map(|(rss, _vsz)| rss)
99}
100
101pub fn get_memory_virtual(pid: i32) -> Option<u64> {
106 get_statm(pid).map(|(_rss, vsz)| vsz)
107}
108
109fn get_statm(pid: i32) -> Option<(u64, u64)> {
111 let statm = fs::read_to_string(format!("/proc/{pid}/statm")).ok()?;
112 let fields: Vec<&str> = statm.split_whitespace().collect();
113
114 if fields.len() < 2 {
116 return None;
117 }
118
119 let vsz_pages: u64 = fields[0].parse().ok()?;
120 let rss_pages: u64 = fields[1].parse().ok()?;
121
122 let page_size_raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
124 let page_size = if page_size_raw <= 0 {
126 4096
127 } else {
128 page_size_raw as u64
129 };
130
131 Some((rss_pages * page_size, vsz_pages * page_size))
132}
133
134pub fn get_disk_io(pid: i32) -> Option<DiskIo> {
139 let io = fs::read_to_string(format!("/proc/{pid}/io")).ok()?;
140
141 let mut read_bytes = 0u64;
142 let mut write_bytes = 0u64;
143
144 for line in io.lines() {
147 if let Some(value) = line.strip_prefix("read_bytes: ") {
148 read_bytes = value.parse().unwrap_or(0);
149 } else if let Some(value) = line.strip_prefix("write_bytes: ") {
150 write_bytes = value.parse().unwrap_or(0);
151 }
152 }
153
154 Some(DiskIo {
155 read_bytes,
156 write_bytes,
157 })
158}
159
160pub fn get_all_stats(pid: i32) -> Option<AllStats> {
165 let cpu_time = get_cpu_time(pid)?;
166 let memory_rss = get_memory(pid)?;
167 let disk_io = get_disk_io(pid).unwrap_or_default();
168
169 Some(AllStats {
170 cpu_time,
171 memory_rss,
172 disk_io,
173 })
174}
175
176pub fn process_exists(pid: i32) -> bool {
178 std::path::Path::new(&format!("/proc/{pid}")).exists()
179}
180
181pub fn get_children(pid: i32) -> Vec<i32> {
186 fs::read_to_string(format!("/proc/{pid}/task/{pid}/children"))
187 .unwrap_or_default()
188 .split_whitespace()
189 .filter_map(|s| s.parse().ok())
190 .collect()
191}
192
193pub fn get_ppid(pid: i32) -> Option<i32> {
198 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
199
200 let comm_end = stat.rfind(')')?;
202 let fields: Vec<&str> = stat[comm_end + 2..].split_whitespace().collect();
203
204 if fields.len() < 2 {
206 return None;
207 }
208
209 fields[1].parse().ok()
210}
211
212pub fn get_start_time(pid: i32) -> Option<i64> {
218 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
219
220 let comm_end = stat.rfind(')')?;
222 let fields: Vec<&str> = stat[comm_end + 2..].split_whitespace().collect();
223
224 if fields.len() < 20 {
227 return None;
228 }
229
230 let starttime_ticks: u64 = fields[19].parse().ok()?;
231 let ticks_per_sec = clock_ticks_per_sec();
232
233 let boot_time = get_boot_time()?;
235
236 let starttime_secs = starttime_ticks / ticks_per_sec;
238 Some(boot_time + starttime_secs as i64)
239}
240
241fn get_boot_time() -> Option<i64> {
243 let stat = fs::read_to_string("/proc/stat").ok()?;
244 for line in stat.lines() {
245 if let Some(value) = line.strip_prefix("btime ") {
246 return value.trim().parse().ok();
247 }
248 }
249 None
250}
251
252pub fn get_process_path(pid: i32) -> Option<String> {
257 fs::read_link(format!("/proc/{pid}/exe"))
258 .ok()
259 .and_then(|p| p.to_str().map(|s| s.to_string()))
260}
261
262pub fn get_process_comm(pid: i32) -> Option<String> {
275 fs::read_to_string(format!("/proc/{pid}/comm"))
276 .ok()
277 .map(|s| s.trim().to_string())
278 .filter(|s| !s.is_empty())
279}
280
281pub fn get_tty(pid: i32) -> Option<String> {
293 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
294
295 let comm_end = stat.rfind(')')?;
297 let fields: Vec<&str> = stat[comm_end + 2..].split_whitespace().collect();
298
299 if fields.len() < 5 {
301 return None;
302 }
303
304 let tty_nr: i32 = fields[4].parse().ok()?;
305
306 if tty_nr == 0 {
308 return None;
309 }
310
311 let major = ((tty_nr >> 8) & 0xff) as u32;
316 let minor = ((tty_nr & 0xff) | ((tty_nr >> 12) & 0xfff00)) as u32;
317
318 match major {
323 136..=143 => Some(format!("pts/{minor}")),
324 4 => Some(format!("tty{minor}")),
325 _ => {
326 fs::read_link(format!("/proc/{pid}/fd/0"))
329 .ok()
330 .and_then(|p| {
331 let path = p.to_string_lossy();
332 if path.starts_with("/dev/") {
333 Some(path.strip_prefix("/dev/")?.to_string())
334 } else {
335 None
336 }
337 })
338 }
339 }
340}
341
342pub fn list_all_pids() -> Vec<i32> {
347 let Ok(entries) = fs::read_dir("/proc") else {
348 return Vec::new();
349 };
350
351 entries
352 .filter_map(|e| e.ok())
353 .filter_map(|e| e.file_name().to_str()?.parse::<i32>().ok())
354 .filter(|&pid| pid > 0)
355 .collect()
356}
357
358#[derive(Debug, Clone)]
360pub struct ProcessInfo {
361 pub pid: i32,
362 pub ppid: i32,
363 pub cpu_time: super::CpuTime,
364 pub memory_bytes: u64,
365}
366
367pub fn scan_all_processes() -> Vec<ProcessInfo> {
375 let pids = list_all_pids();
376 let mut result = Vec::with_capacity(pids.len());
377
378 for pid in pids {
379 let ppid = match get_ppid(pid) {
381 Some(p) => p,
382 None => continue, };
384
385 let cpu_time = match get_cpu_time(pid) {
387 Some(c) => c,
388 None => continue, };
390
391 let memory_bytes = get_memory(pid).unwrap_or(0);
393
394 result.push(ProcessInfo {
395 pid,
396 ppid,
397 cpu_time,
398 memory_bytes,
399 });
400 }
401
402 result
403}
404
405pub fn build_parent_map() -> std::collections::HashMap<i32, i32> {
414 let pids = list_all_pids();
415 let mut map = std::collections::HashMap::with_capacity(pids.len());
416
417 for pid in pids {
418 if let Some(ppid) = get_ppid(pid) {
419 map.insert(pid, ppid);
420 }
421 }
422
423 map
424}
425
426pub fn get_process_environ(pid: i32) -> Option<std::collections::HashMap<String, String>> {
432 let environ = fs::read(format!("/proc/{pid}/environ")).ok()?;
433
434 let mut map = std::collections::HashMap::new();
435
436 for entry in environ.split(|&b| b == 0) {
438 if entry.is_empty() {
439 continue;
440 }
441 if let Ok(s) = std::str::from_utf8(entry)
442 && let Some((key, value)) = s.split_once('=')
443 {
444 map.insert(key.to_string(), value.to_string());
445 }
446 }
447
448 Some(map)
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_build_parent_map() {
457 let map = build_parent_map();
458 assert!(map.len() > 10);
460 let our_pid = std::process::id() as i32;
462 assert!(map.contains_key(&our_pid));
463 let ppid = map.get(&our_pid).unwrap();
465 assert!(map.contains_key(ppid) || *ppid == 1);
466 }
467
468 #[test]
469 fn test_get_cpu_time_self() {
470 let pid = std::process::id() as i32;
471 let cpu = get_cpu_time(pid);
472 assert!(cpu.is_some());
473 }
474
475 #[test]
476 fn test_get_memory_self() {
477 let pid = std::process::id() as i32;
478 let mem = get_memory(pid);
479 assert!(mem.is_some());
480 assert!(mem.unwrap() > 0);
481 }
482
483 #[test]
484 fn test_get_cpu_time_invalid() {
485 let cpu = get_cpu_time(999_999_999);
486 assert!(cpu.is_none());
487 }
488
489 #[test]
490 fn test_process_exists() {
491 let pid = std::process::id() as i32;
492 assert!(process_exists(pid));
493 assert!(!process_exists(999_999_999));
494 }
495
496 #[test]
497 fn test_get_disk_io_self() {
498 let pid = std::process::id() as i32;
499 let io = get_disk_io(pid);
500 assert!(io.is_some());
501 let _io = io.unwrap();
503 }
504
505 #[test]
506 fn test_get_all_stats_self() {
507 let pid = std::process::id() as i32;
508 let stats = get_all_stats(pid);
509 assert!(stats.is_some());
510 let stats = stats.unwrap();
511
512 assert!(stats.memory_rss > 0, "Memory should be non-zero");
514
515 }
517
518 #[test]
519 fn test_get_all_stats_invalid_pid() {
520 let stats = get_all_stats(999_999_999);
521 assert!(stats.is_none());
522 }
523}