1#![allow(unsafe_code)]
7#![allow(rustdoc::invalid_html_tags)]
8
9use super::{CpuTime, LivenessInfo};
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 get_liveness_info(pid: i32) -> Option<LivenessInfo> {
360 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
361
362 let comm_end = stat.rfind(')')?;
364 let fields: Vec<&str> = stat[comm_end + 2..].split_whitespace().collect();
365
366 if fields.len() < 20 {
370 return None;
371 }
372
373 let ppid: i32 = fields[1].parse().ok()?;
374 let tty_nr: i32 = fields[4].parse().ok()?;
375 let starttime_ticks: u64 = fields[19].parse().ok()?;
376
377 let ticks_per_sec = clock_ticks_per_sec();
379 let boot_time = get_boot_time()?;
380 let start_time = boot_time + (starttime_ticks / ticks_per_sec) as i64;
381
382 let has_tty = tty_nr != 0;
384
385 Some(LivenessInfo {
386 start_time,
387 ppid,
388 has_tty,
389 })
390}
391
392pub fn list_all_pids() -> Vec<i32> {
397 let Ok(entries) = fs::read_dir("/proc") else {
398 return Vec::new();
399 };
400
401 entries
402 .filter_map(|e| e.ok())
403 .filter_map(|e| e.file_name().to_str()?.parse::<i32>().ok())
404 .filter(|&pid| pid > 0)
405 .collect()
406}
407
408#[derive(Debug, Clone)]
410pub struct ProcessInfo {
411 pub pid: i32,
412 pub ppid: i32,
413 pub cpu_time: super::CpuTime,
414 pub memory_bytes: u64,
415}
416
417pub fn scan_all_processes() -> Vec<ProcessInfo> {
425 let pids = list_all_pids();
426 let mut result = Vec::with_capacity(pids.len());
427
428 for pid in pids {
429 let ppid = match get_ppid(pid) {
431 Some(p) => p,
432 None => continue, };
434
435 let cpu_time = match get_cpu_time(pid) {
437 Some(c) => c,
438 None => continue, };
440
441 let memory_bytes = get_memory(pid).unwrap_or(0);
443
444 result.push(ProcessInfo {
445 pid,
446 ppid,
447 cpu_time,
448 memory_bytes,
449 });
450 }
451
452 result
453}
454
455pub fn build_parent_map() -> std::collections::HashMap<i32, i32> {
464 let pids = list_all_pids();
465 let mut map = std::collections::HashMap::with_capacity(pids.len());
466
467 for pid in pids {
468 if let Some(ppid) = get_ppid(pid) {
469 map.insert(pid, ppid);
470 }
471 }
472
473 map
474}
475
476pub fn get_process_environ(pid: i32) -> Option<std::collections::HashMap<String, String>> {
482 let environ = fs::read(format!("/proc/{pid}/environ")).ok()?;
483
484 let mut map = std::collections::HashMap::new();
485
486 for entry in environ.split(|&b| b == 0) {
488 if entry.is_empty() {
489 continue;
490 }
491 if let Ok(s) = std::str::from_utf8(entry)
492 && let Some((key, value)) = s.split_once('=')
493 {
494 map.insert(key.to_string(), value.to_string());
495 }
496 }
497
498 Some(map)
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_build_parent_map() {
507 let map = build_parent_map();
508 assert!(map.len() > 10);
510 let our_pid = std::process::id() as i32;
512 assert!(map.contains_key(&our_pid));
513 let ppid = map.get(&our_pid).unwrap();
515 assert!(map.contains_key(ppid) || *ppid == 1);
516 }
517
518 #[test]
519 fn test_get_cpu_time_self() {
520 let pid = std::process::id() as i32;
521 let cpu = get_cpu_time(pid);
522 assert!(cpu.is_some());
523 }
524
525 #[test]
526 fn test_get_memory_self() {
527 let pid = std::process::id() as i32;
528 let mem = get_memory(pid);
529 assert!(mem.is_some());
530 assert!(mem.unwrap() > 0);
531 }
532
533 #[test]
534 fn test_get_cpu_time_invalid() {
535 let cpu = get_cpu_time(999_999_999);
536 assert!(cpu.is_none());
537 }
538
539 #[test]
540 fn test_process_exists() {
541 let pid = std::process::id() as i32;
542 assert!(process_exists(pid));
543 assert!(!process_exists(999_999_999));
544 }
545
546 #[test]
547 fn test_get_disk_io_self() {
548 let pid = std::process::id() as i32;
549 let io = get_disk_io(pid);
550 assert!(io.is_some());
551 let _io = io.unwrap();
553 }
554
555 #[test]
556 fn test_get_all_stats_self() {
557 let pid = std::process::id() as i32;
558 let stats = get_all_stats(pid);
559 assert!(stats.is_some());
560 let stats = stats.unwrap();
561
562 assert!(stats.memory_rss > 0, "Memory should be non-zero");
564
565 }
567
568 #[test]
569 fn test_get_all_stats_invalid_pid() {
570 let stats = get_all_stats(999_999_999);
571 assert!(stats.is_none());
572 }
573}