use std::collections::HashMap;
use std::time::Instant;
use crate::sampler::ProcInfo;
const PROC_PIDTASKINFO: libc::c_int = 4;
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct ProcTaskInfo {
pti_virtual_size: u64,
pti_resident_size: u64,
pti_total_user: u64,
pti_total_system: u64,
pti_threads_user: u64,
pti_threads_system: u64,
pti_policy: i32,
pti_faults: i32,
pti_pageins: i32,
pti_cow_faults: i32,
pti_messages_sent: i32,
pti_messages_received: i32,
pti_syscalls_mach: i32,
pti_syscalls_unix: i32,
pti_csw: i32,
pti_threadnum: i32,
pti_numrunning: i32,
pti_priority: i32,
}
#[repr(C)]
struct MachTimebaseInfo {
numer: u32,
denom: u32,
}
extern "C" {
fn mach_timebase_info(info: *mut MachTimebaseInfo) -> libc::c_int;
fn proc_listallpids(buffer: *mut libc::c_void, buffersize: libc::c_int) -> libc::c_int;
fn proc_pidinfo(
pid: libc::c_int,
flavor: libc::c_int,
arg: u64,
buffer: *mut libc::c_void,
buffersize: libc::c_int,
) -> libc::c_int;
fn proc_name(pid: libc::c_int, buffer: *mut libc::c_void, buffersize: u32) -> libc::c_int;
}
struct Seen {
cum_ns: u64,
name: String,
}
pub struct ProcScanner {
prev: HashMap<i32, Seen>,
last_scan: Option<Instant>,
ns_per_unit: f64,
}
impl Default for ProcScanner {
fn default() -> Self {
Self::new()
}
}
impl ProcScanner {
pub fn new() -> Self {
let mut tb = MachTimebaseInfo { numer: 0, denom: 0 };
unsafe { mach_timebase_info(&mut tb) };
let ns_per_unit = if tb.denom == 0 {
1.0
} else {
f64::from(tb.numer) / f64::from(tb.denom)
};
Self { prev: HashMap::new(), last_scan: None, ns_per_unit }
}
pub fn scan(&mut self) -> Vec<ProcInfo> {
let now = Instant::now();
let dt_ns = self
.last_scan
.map(|t| now.duration_since(t).as_nanos() as f64)
.unwrap_or(f64::INFINITY)
.max(1.0);
self.last_scan = Some(now);
let pids = list_all_pids();
let mut out = Vec::with_capacity(pids.len());
let mut next = HashMap::with_capacity(pids.len());
let sz = std::mem::size_of::<ProcTaskInfo>() as libc::c_int;
for pid in pids {
if pid <= 0 {
continue;
}
let mut ti = ProcTaskInfo::default();
let r = unsafe {
proc_pidinfo(pid, PROC_PIDTASKINFO, 0, (&mut ti as *mut ProcTaskInfo).cast(), sz)
};
if r != sz {
continue; }
let raw = ti.pti_total_user.saturating_add(ti.pti_total_system);
let cum_ns = (raw as f64 * self.ns_per_unit) as u64;
let (cpu, name) = match self.prev.remove(&pid) {
Some(seen) if cum_ns >= seen.cum_ns => {
let cpu = (cum_ns - seen.cum_ns) as f64 / dt_ns * 100.0;
(cpu as f32, seen.name)
}
_ => (0.0, read_name(pid)),
};
out.push(ProcInfo { pid, name: name.clone(), cpu, mem: ti.pti_resident_size });
next.insert(pid, Seen { cum_ns, name });
}
self.prev = next; out
}
}
const PROC_PIDVNODEPATHINFO: libc::c_int = 9;
const MAXPATHLEN: usize = 1024;
const VNODE_INFO_SIZE: usize = 152;
#[repr(C)]
struct VnodeInfoPath {
_vi: [u8; VNODE_INFO_SIZE],
vip_path: [u8; MAXPATHLEN],
}
#[repr(C)]
struct ProcVnodePathInfo {
pvi_cdir: VnodeInfoPath,
pvi_rdir: VnodeInfoPath,
}
pub fn cwd(pid: i32) -> Option<std::path::PathBuf> {
let mut info: ProcVnodePathInfo = unsafe { std::mem::zeroed() };
let sz = std::mem::size_of::<ProcVnodePathInfo>() as libc::c_int;
let r = unsafe {
proc_pidinfo(
pid,
PROC_PIDVNODEPATHINFO,
0,
&mut info as *mut _ as *mut libc::c_void,
sz,
)
};
if r != sz {
return None;
}
let path = &info.pvi_cdir.vip_path;
let end = path.iter().position(|&b| b == 0).unwrap_or(path.len());
let s = std::str::from_utf8(&path[..end]).ok()?;
if s.is_empty() || s == "/" {
return None;
}
Some(std::path::PathBuf::from(s))
}
fn read_name(pid: i32) -> String {
let mut buf = [0u8; 128];
let r = unsafe { proc_name(pid, buf.as_mut_ptr().cast(), buf.len() as u32) };
if r <= 0 {
return format!("pid {pid}");
}
String::from_utf8_lossy(&buf[..r as usize]).into_owned()
}
const PROC_PIDTBSDINFO: libc::c_int = 3;
unsafe extern "C" {
fn proc_pidpath(pid: libc::c_int, buf: *mut libc::c_void, len: u32) -> libc::c_int;
fn devname(dev: libc::dev_t, mode: libc::mode_t) -> *const libc::c_char;
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct ProcBsdInfo {
pbi_flags: u32,
pbi_status: u32,
pbi_xstatus: u32,
pbi_pid: u32,
pbi_ppid: u32,
pbi_uid: u32,
pbi_gid: u32,
pbi_ruid: u32,
pbi_rgid: u32,
pbi_svuid: u32,
pbi_svgid: u32,
rfu_1: u32,
pbi_comm: [u8; 16],
pbi_name: [u8; 32],
pbi_nfiles: u32,
pbi_pgid: u32,
pbi_pjobc: u32,
e_tdev: u32,
e_tpgid: u32,
pbi_nice: i32,
pbi_start_tvsec: u64,
pbi_start_tvusec: u64,
}
pub fn exec_path(pid: i32) -> Option<std::path::PathBuf> {
let mut buf = [0u8; 4096];
let n = unsafe { proc_pidpath(pid, buf.as_mut_ptr().cast(), buf.len() as u32) };
if n <= 0 {
return None;
}
let s = std::str::from_utf8(&buf[..n as usize]).ok()?;
Some(std::path::PathBuf::from(s))
}
pub fn tty(pid: i32) -> Option<Option<String>> {
let mut i = ProcBsdInfo::default();
let sz = std::mem::size_of::<ProcBsdInfo>() as libc::c_int;
let r = unsafe {
proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &mut i as *mut _ as *mut libc::c_void, sz)
};
if r != sz {
return None;
}
let tty = if i.e_tdev == 0 || i.e_tdev == u32::MAX {
None
} else {
let p = unsafe { devname(i.e_tdev as libc::dev_t, libc::S_IFCHR) };
if p.is_null() {
None
} else {
let s = unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy().into_owned();
if s.is_empty() || s == "??" { None } else { Some(s) }
}
};
Some(tty)
}
pub fn list_all_pids() -> Vec<i32> {
let mut buf = vec![0i32; 2048];
loop {
let cap = (buf.len() * std::mem::size_of::<i32>()) as libc::c_int;
let n = unsafe { proc_listallpids(buf.as_mut_ptr().cast(), cap) };
if n < 0 {
return Vec::new();
}
let n = n as usize;
if n < buf.len() {
buf.truncate(n);
return buf;
}
buf.resize(buf.len() * 2, 0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scanner_sees_own_busy_loop() {
let mut scanner = ProcScanner::new();
let me = std::process::id() as i32;
let first = scanner.scan();
let self_first = first.iter().find(|p| p.pid == me).expect("own pid visible");
assert_eq!(self_first.cpu, 0.0, "first scan reports 0 cpu");
assert!(self_first.mem > 0, "resident size populated");
assert!(!self_first.name.is_empty());
let t0 = Instant::now();
while t0.elapsed().as_millis() < 150 {
std::hint::black_box((0..1000).sum::<u64>());
}
let second = scanner.scan();
let self_second = second.iter().find(|p| p.pid == me).expect("own pid visible");
assert!(
self_second.cpu > 20.0 && self_second.cpu < 1000.0,
"busy loop should register as cpu load, got {}",
self_second.cpu
);
}
#[test]
fn scan_is_plausible() {
let mut scanner = ProcScanner::new();
let procs = scanner.scan();
assert!(procs.len() > 50, "expected many processes, got {}", procs.len());
assert!(procs.iter().all(|p| p.pid > 0));
}
#[test]
fn cwd_of_self_is_the_full_current_dir() {
let want = std::env::current_dir().expect("cwd readable");
let got = super::cwd(std::process::id() as i32).expect("own cwd readable");
assert_eq!(
got.canonicalize().unwrap(),
want.canonicalize().unwrap(),
"expected the full path, not a basename"
);
assert!(got.is_absolute(), "must be absolute so .git can be tested");
}
#[test]
fn cwd_of_dead_pid_is_none() {
assert_eq!(super::cwd(-1), None);
}
#[test]
fn exec_path_of_self_is_the_test_binary() {
let p = super::exec_path(std::process::id() as i32).expect("own exec path readable");
assert!(p.is_absolute(), "must be absolute: {p:?}");
assert!(p.exists(), "path must exist on disk: {p:?}");
}
#[test]
fn exec_path_of_dead_pid_is_none() {
assert_eq!(super::exec_path(-1), None);
}
#[test]
fn tty_of_self_is_readable_and_well_shaped() {
let t = super::tty(std::process::id() as i32).expect("own pid must be inspectable");
if let Some(name) = &t {
assert!(name.starts_with("tty") || name.starts_with("cons"), "odd tty name: {name}");
}
}
#[test]
fn tty_of_dead_pid_is_none() {
assert!(super::tty(-1).is_none(), "an uninspectable pid must be the outer None");
}
#[test]
fn list_all_pids_includes_self() {
let pids = super::list_all_pids();
assert!(pids.len() > 10, "a live macOS box has many pids, got {}", pids.len());
assert!(pids.contains(&(std::process::id() as i32)), "own pid missing");
}
}