use std::collections::HashMap;
use std::sync::OnceLock;
use arrayvec::ArrayString;
const UNKNOWN: &str = "unknown";
fn clock_ticks_per_sec() -> i64 {
static TICKS: OnceLock<i64> = OnceLock::new();
*TICKS.get_or_init(|| {
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if ticks <= 0 { 100 } else { ticks }
})
}
pub fn read_proc_comm(pid: u32) -> Option<String> {
let path = proc_path(pid, "comm");
let mut buf = [0u8; 64];
let mut file = std::fs::File::open(path.as_str()).ok()?;
use std::io::Read;
let n = file.read(&mut buf).ok()?;
let s = std::str::from_utf8(&buf[..n]).ok()?;
Some(s.trim().to_string())
}
fn proc_path(pid: u32, suffix: &str) -> ArrayString<32> {
use std::fmt::Write;
let mut buf = ArrayString::new();
write!(buf, "/proc/{pid}/{suffix}").unwrap();
buf
}
pub fn read_proc_start_time_ns(pid: u32) -> u64 {
let path = proc_path(pid, "stat");
let stat = match std::fs::read_to_string(path.as_str()) {
Ok(s) => s,
Err(_) => return 0,
};
let after_comm = match stat.rfind(") ") {
Some(pos) => pos + 2,
None => return 0,
};
let mut rest = &stat[after_comm..];
for _ in 0..19 {
if let Some(pos) = rest.find(' ') {
rest = &rest[pos + 1..];
} else {
return 0;
}
}
let starttime_jiffies: u64 = match rest.split_whitespace().next() {
Some(s) => s.parse().unwrap_or(0),
None => return 0,
};
if starttime_jiffies == 0 {
return 0;
}
(starttime_jiffies as u128 * 1_000_000_000 / clock_ticks_per_sec() as u128) as u64
}
fn uid_passwd_map() -> &'static HashMap<u32, String> {
static MAP: OnceLock<HashMap<u32, String>> = OnceLock::new();
MAP.get_or_init(|| {
let mut map = HashMap::new();
if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
for entry in passwd.lines() {
let mut parts = entry.splitn(4, ':');
let name = parts.next();
let _password = parts.next();
let uid_str = parts.next();
if let (Some(name), Some(uid_str)) = (name, uid_str)
&& let Ok(uid) = uid_str.parse::<u32>()
{
map.insert(uid, name.to_string());
}
}
}
map
})
}
pub fn read_proc_cmdline(pid: u32) -> Option<String> {
let path = proc_path(pid, "cmdline");
let bytes = std::fs::read(path.as_str()).ok()?;
if bytes.is_empty() {
return None;
}
let s = String::from_utf8_lossy(&bytes);
let trimmed = s.trim_end_matches('\0');
Some(trimmed.replace('\0', " "))
}
pub fn parse_proc_entry(pid: u32) -> Option<crate::types::ProcessInfo> {
let path = proc_path(pid, "status");
let status = std::fs::read_to_string(path.as_str()).ok()?;
let mut ppid = 0u32;
let mut user = String::new();
let mut tgid = 0u32;
for line in status.lines() {
if let Some(val) = line.strip_prefix("PPid:") {
ppid = val.trim().parse().unwrap_or(0);
} else if let Some(val) = line.strip_prefix("Uid:") {
if let Some(uid_str) = val.split_whitespace().next()
&& let Ok(uid) = uid_str.parse::<u32>()
{
user = uid_to_username(uid).unwrap_or(UNKNOWN).to_owned();
} else {
user = UNKNOWN.to_string();
}
} else if let Some(val) = line.strip_prefix("Tgid:") {
tgid = val.trim().parse().unwrap_or(0);
}
}
let comm = read_proc_comm(pid).unwrap_or_else(|| UNKNOWN.to_string());
let cmd = read_proc_cmdline(pid).unwrap_or_else(|| comm.clone());
let start_time_ns = read_proc_start_time_ns(pid);
Some(crate::types::ProcessInfo::new(
comm,
cmd,
user,
ppid,
tgid,
start_time_ns,
))
}
pub fn uid_to_username(uid: u32) -> Option<&'static str> {
uid_passwd_map().get(&uid).map(|s| s.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_proc_comm_pid1() {
let comm = read_proc_comm(1);
assert!(comm.is_some(), "PID 1 should exist");
assert!(!comm.unwrap().is_empty());
}
#[test]
fn test_read_proc_comm_nonexistent() {
assert!(read_proc_comm(0x7FFFFFFF).is_none());
}
#[test]
fn test_read_proc_start_time_ns_pid1() {
let ns = read_proc_start_time_ns(1);
assert!(ns > 0, "PID 1 start_time_ns should be > 0, got {ns}");
}
#[test]
fn test_read_proc_start_time_ns_nonexistent() {
assert_eq!(read_proc_start_time_ns(0x7FFFFFFF), 0);
}
#[test]
fn test_uid_to_username_root() {
let name = uid_to_username(0);
assert_eq!(name, Some("root"));
}
#[test]
fn test_uid_to_username_nonexistent() {
assert!(uid_to_username(0xFFFFFFFF).is_none());
}
#[test]
fn test_read_proc_cmdline_pid1() {
let cmdline = read_proc_cmdline(1);
assert!(cmdline.is_some(), "PID 1 should have a cmdline");
let s = cmdline.unwrap();
assert!(!s.is_empty());
assert!(
!s.contains('\0'),
"cmdline should not contain NUL bytes: {:?}",
s
);
}
#[test]
fn test_read_proc_cmdline_nonexistent() {
assert!(read_proc_cmdline(0x7FFFFFFF).is_none());
}
#[test]
fn test_read_proc_cmdline_kernel_thread() {
let cmdline = read_proc_cmdline(2);
assert!(
cmdline.is_none(),
"kernel thread PID 2 should have empty cmdline"
);
}
#[test]
fn test_parse_proc_entry_uses_full_cmdline() {
let info = parse_proc_entry(1).expect("PID 1 should exist");
assert!(
info.cmd().len() > 15 || !info.cmd().contains(' '),
"PID 1 cmd should be full cmdline, got: {:?}",
info.cmd()
);
}
}