#![allow(deprecated)]
use std::ffi::CStr;
use libc::{c_int, c_void};
#[derive(Copy, Clone, Debug, Default)]
pub struct KInfoProc {
pub pid: u32,
pub ppid: u32,
pub uid: u32,
pub nice: i32,
pub p_stat: i8,
pub start_sec: u64,
pub start_usec: u64,
pub pgrp: u32,
pub tpgid: i64,
pub tty_dev: i64,
pub comm: [u8; 17],
}
pub fn list_pids() -> std::io::Result<Vec<u32>> {
let elem = std::mem::size_of::<i32>();
let estimate = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
if estimate <= 0 {
return Err(std::io::Error::last_os_error());
}
for attempt in 0..4i32 {
let extra = 16 + attempt as usize * attempt as usize * 64;
let capacity = estimate.max(16) as usize + extra;
let mut pids: Vec<i32> = vec![0; capacity];
let count = unsafe {
libc::proc_listallpids(pids.as_mut_ptr().cast::<c_void>(), (capacity * elem) as i32)
};
if count <= 0 {
return Err(std::io::Error::last_os_error());
}
let count = (count as usize).min(capacity);
if count == capacity {
continue; }
return Ok(pids[..count]
.iter()
.filter(|p| **p > 0)
.map(|p| *p as u32)
.collect());
}
Err(std::io::Error::other(
"proc_listallpids kept returning a full buffer",
))
}
#[derive(Copy, Clone, Debug, Default)]
pub struct TaskAllInfo {
pub bsd: KInfoProc,
pub task: TaskInfo,
}
pub fn task_all_info(pid: u32) -> Option<TaskAllInfo> {
let mut info = std::mem::MaybeUninit::<libc::proc_taskallinfo>::zeroed();
let want = std::mem::size_of::<libc::proc_taskallinfo>() as c_int;
let got = unsafe {
libc::proc_pidinfo(
pid as c_int,
libc::PROC_PIDTASKALLINFO,
0,
info.as_mut_ptr().cast::<c_void>(),
want,
)
};
if got != want {
return None;
}
let info = unsafe { info.assume_init() };
let b = &info.pbsd;
let mut comm = [0u8; 17];
for (dst, src) in comm.iter_mut().zip(b.pbi_comm.iter()) {
*dst = *src as u8;
if *src == 0 {
break;
}
}
Some(TaskAllInfo {
bsd: KInfoProc {
pid: b.pbi_pid,
ppid: b.pbi_ppid,
uid: b.pbi_uid,
nice: b.pbi_nice,
p_stat: b.pbi_status as i8,
start_sec: b.pbi_start_tvsec,
start_usec: b.pbi_start_tvusec,
pgrp: b.pbi_pgid,
tpgid: b.e_tpgid as i32 as i64,
tty_dev: if b.e_tdev == u32::MAX {
0
} else {
b.e_tdev as i64
},
comm,
},
task: TaskInfo {
user_ns: info.ptinfo.pti_total_user,
system_ns: info.ptinfo.pti_total_system,
virtual_size: info.ptinfo.pti_virtual_size,
resident_size: info.ptinfo.pti_resident_size,
faults: info.ptinfo.pti_faults as u32,
threads: info.ptinfo.pti_threadnum as u32,
running_threads: info.ptinfo.pti_numrunning as u32,
priority: info.ptinfo.pti_priority,
},
})
}
#[derive(Copy, Clone, Debug)]
pub struct ThreadInfo {
pub handle: u64,
pub name: [u8; 64],
pub user_ns: u64,
pub system_ns: u64,
pub run_state: i32,
}
impl Default for ThreadInfo {
fn default() -> Self {
Self {
handle: 0,
name: [0; 64],
user_ns: 0,
system_ns: 0,
run_state: 0,
}
}
}
impl ThreadInfo {
pub fn name_str(&self) -> &str {
let len = self.name.iter().position(|b| *b == 0).unwrap_or(self.name.len());
std::str::from_utf8(&self.name[..len]).unwrap_or("")
}
}
pub fn list_threads(pid: u32) -> Option<Vec<u64>> {
for capacity in [64usize, 512, 4096, 16384] {
let mut buf = vec![0u64; capacity];
let want = (capacity * std::mem::size_of::<u64>()) as c_int;
let got = unsafe {
libc::proc_pidinfo(
pid as c_int,
PROC_PIDLISTTHREADS,
0,
buf.as_mut_ptr().cast::<c_void>(),
want,
)
};
if got < 0 {
return None;
}
if got == 0 {
return scan_task_thread_count(pid).map(|_| Vec::new());
}
let count = got as usize / std::mem::size_of::<u64>();
if count < capacity {
buf.truncate(count);
while buf.last() == Some(&0) {
buf.pop();
}
return Some(buf);
}
}
None
}
const PROC_PIDLISTTHREADS: c_int = 6;
fn scan_task_thread_count(pid: u32) -> Option<u32> {
task_all_info(pid).map(|info| info.task.threads)
}
pub fn thread_info(pid: u32, handle: u64) -> Option<ThreadInfo> {
let mut info = std::mem::MaybeUninit::<libc::proc_threadinfo>::zeroed();
let want = std::mem::size_of::<libc::proc_threadinfo>() as c_int;
let got = unsafe {
libc::proc_pidinfo(
pid as c_int,
libc::PROC_PIDTHREADINFO,
handle,
info.as_mut_ptr().cast::<c_void>(),
want,
)
};
if got != want {
return None;
}
let info = unsafe { info.assume_init() };
let mut name = [0u8; 64];
for (dst, src) in name.iter_mut().zip(info.pth_name.iter()) {
*dst = *src as u8;
}
Some(ThreadInfo {
handle,
name,
user_ns: info.pth_user_time,
system_ns: info.pth_system_time,
run_state: info.pth_run_state,
})
}
#[derive(Copy, Clone, Debug, Default)]
pub struct TaskInfo {
pub user_ns: u64,
pub system_ns: u64,
pub virtual_size: u64,
pub resident_size: u64,
pub faults: u32,
pub threads: u32,
pub running_threads: u32,
pub priority: i32,
}
pub fn exe_path(pid: u32) -> Option<String> {
let mut buf = [0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
let len = unsafe {
libc::proc_pidpath(
pid as c_int,
buf.as_mut_ptr().cast::<c_void>(),
buf.len() as u32,
)
};
if len <= 0 {
return None;
}
let cstr = CStr::from_bytes_until_nul(&buf).ok()?;
Some(cstr.to_string_lossy().into_owned())
}
pub fn cwd_path(pid: u32) -> Option<String> {
let mut info = std::mem::MaybeUninit::<libc::proc_vnodepathinfo>::zeroed();
let want = std::mem::size_of::<libc::proc_vnodepathinfo>() as c_int;
let got = unsafe {
libc::proc_pidinfo(
pid as c_int,
libc::PROC_PIDVNODEPATHINFO,
0,
info.as_mut_ptr().cast::<c_void>(),
want,
)
};
if got != want {
return None;
}
let info = unsafe { info.assume_init() };
let mut path: Vec<u8> = Vec::with_capacity(32 * 32);
for chunk in info.pvi_cdir.vip_path.iter() {
for byte in chunk.iter() {
let b = *byte as u8;
if b == 0 {
let text = String::from_utf8_lossy(&path).into_owned();
return (!text.is_empty()).then_some(text);
}
path.push(b);
}
}
let text = String::from_utf8_lossy(&path).into_owned();
(!text.is_empty()).then_some(text)
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Rusage {
pub disk_read: u64,
pub disk_written: u64,
}
pub fn rusage(pid: u32) -> Option<Rusage> {
let mut ri = std::mem::MaybeUninit::<libc::rusage_info_v2>::zeroed();
let rc = unsafe {
libc::proc_pid_rusage(
pid as c_int,
libc::RUSAGE_INFO_V2,
ri.as_mut_ptr().cast::<libc::rusage_info_t>(),
)
};
if rc != 0 {
return None;
}
let ri = unsafe { ri.assume_init() };
Some(Rusage {
disk_read: ri.ri_diskio_bytesread,
disk_written: ri.ri_diskio_byteswritten,
})
}
pub fn procargs2(pid: u32) -> Option<Vec<u8>> {
let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int];
let mut size: usize = crate::darwin::parse::ARG_MAX_CAP;
let mut buf = vec![0u8; size];
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
buf.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || size < 4 {
return None;
}
buf.truncate(size);
Some(buf)
}
pub fn sysctl_string(name: &CStr) -> Option<String> {
let mut size: usize = 0;
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
std::ptr::null_mut(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || size == 0 {
return None;
}
let mut buf = vec![0u8; size];
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
buf.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return None;
}
buf.truncate(size);
while buf.last() == Some(&0) {
buf.pop();
}
Some(String::from_utf8_lossy(&buf).into_owned())
}
pub fn sysctl_u64(name: &CStr) -> Option<u64> {
let mut value: u64 = 0;
let mut size = std::mem::size_of::<u64>();
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
(&mut value as *mut u64).cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc == 0 {
Some(value)
} else {
None
}
}
pub fn cpu_count() -> usize {
static NAME: &CStr = c"hw.logicalcpu";
sysctl_u64(NAME).unwrap_or(1).max(1) as usize
}
pub fn total_memory() -> u64 {
static NAME: &CStr = c"hw.memsize";
sysctl_u64(NAME).unwrap_or(0)
}
pub fn swap_usage() -> crate::system::SwapInfo {
let mut usage = std::mem::MaybeUninit::<libc::xsw_usage>::zeroed();
let mut size = std::mem::size_of::<libc::xsw_usage>();
static NAME: &CStr = c"vm.swapusage";
let rc = unsafe {
libc::sysctlbyname(
NAME.as_ptr(),
usage.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return crate::system::SwapInfo::default();
}
let usage = unsafe { usage.assume_init() };
crate::system::SwapInfo {
total: usage.xsu_total,
free: usage.xsu_avail,
used: usage.xsu_used,
}
}
pub fn net_counters() -> crate::system::NetCounters {
let mut mib = [libc::CTL_NET, libc::PF_ROUTE, 0, 0, libc::NET_RT_IFLIST2, 0];
let mut len: usize = 0;
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
std::ptr::null_mut(),
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || len == 0 {
return crate::system::NetCounters::default();
}
for _ in 0..4 {
let mut buf = vec![0u8; len + 1024];
let mut got = buf.len();
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
buf.as_mut_ptr().cast::<c_void>(),
&mut got,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return crate::system::NetCounters::default();
}
if got >= buf.len() {
len = got;
continue;
}
buf.truncate(got);
return parse_iflist2(&buf);
}
crate::system::NetCounters::default()
}
fn parse_iflist2(buf: &[u8]) -> crate::system::NetCounters {
let mut out = crate::system::NetCounters::default();
let mut offset = 0usize;
while offset + std::mem::size_of::<libc::if_msghdr2>() <= buf.len() {
let mut hdr = std::mem::MaybeUninit::<libc::if_msghdr2>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(
buf.as_ptr().add(offset),
hdr.as_mut_ptr().cast::<u8>(),
std::mem::size_of::<libc::if_msghdr2>(),
);
}
let hdr = unsafe { hdr.assume_init() };
let msglen = hdr.ifm_msglen as usize;
if msglen == 0 {
break; }
if hdr.ifm_type == libc::RTM_IFINFO2 as u8 {
let data = hdr.ifm_data;
let loopback = hdr.ifm_flags & libc::IFF_LOOPBACK != 0;
if !loopback {
out.rx_bytes = out.rx_bytes.saturating_add(data.ifi_ibytes);
out.tx_bytes = out.tx_bytes.saturating_add(data.ifi_obytes);
}
}
offset = offset.saturating_add(msglen);
}
out
}
pub fn page_size() -> u64 { let v = unsafe { libc::vm_page_size };
if v > 0 {
v as u64
} else {
16_384
}
}
pub fn cpu_ticks() -> Vec<crate::system::CpuTimes> {
let mut info: libc::processor_info_array_t = std::ptr::null_mut();
let mut count: libc::mach_msg_type_number_t = 0;
let mut ncpu: libc::natural_t = 0;
let host = unsafe { libc::mach_host_self() };
let rc = unsafe {
libc::host_processor_info(
host,
libc::PROCESSOR_CPU_LOAD_INFO,
&mut ncpu,
&mut info,
&mut count,
)
};
if rc != 0 || info.is_null() || ncpu == 0 {
return Vec::new();
}
let per_cpu = libc::CPU_STATE_MAX as usize;
let mut out = Vec::with_capacity(ncpu as usize + 1);
let ticks = unsafe { std::slice::from_raw_parts(info, count as usize) };
let mut aggregate = crate::system::CpuTimes::default();
for core in 0..ncpu as usize {
let base = core * per_cpu;
if base + per_cpu > ticks.len() {
break;
}
let user = ticks[base + libc::CPU_STATE_USER as usize] as u64;
let system = ticks[base + libc::CPU_STATE_SYSTEM as usize] as u64;
let idle = ticks[base + libc::CPU_STATE_IDLE as usize] as u64;
let nice = ticks[base + libc::CPU_STATE_NICE as usize] as u64;
out.push(crate::system::CpuTimes {
user,
system,
idle,
nice,
..Default::default()
});
aggregate.user += user;
aggregate.system += system;
aggregate.idle += idle;
aggregate.nice += nice;
}
unsafe {
libc::vm_deallocate(
libc::mach_task_self(),
info as libc::vm_address_t,
(count as usize * std::mem::size_of::<libc::integer_t>()) as libc::vm_size_t,
);
}
out.insert(0, aggregate);
out
}
#[derive(Copy, Clone, Debug, Default)]
pub struct VmStats {
pub free: u64,
pub active: u64,
pub inactive: u64,
pub wired: u64,
pub speculative: u64,
pub compressed: u64,
pub purgeable: u64,
pub internal: u64,
pub external: u64,
}
pub fn vm_stats() -> VmStats {
let mut stats = std::mem::MaybeUninit::<libc::vm_statistics64>::zeroed();
let mut count = libc::HOST_VM_INFO64_COUNT;
let host = unsafe { libc::mach_host_self() };
let rc = unsafe {
libc::host_statistics64(
host,
libc::HOST_VM_INFO64,
stats.as_mut_ptr().cast::<libc::integer_t>(),
&mut count,
)
};
if rc != 0 {
return VmStats::default();
}
let stats = unsafe { stats.assume_init() };
let page = page_size();
let b = |pages: u64| pages.saturating_mul(page);
VmStats {
free: b(stats.free_count as u64),
active: b(stats.active_count as u64),
inactive: b(stats.inactive_count as u64),
wired: b(stats.wire_count as u64),
speculative: b(stats.speculative_count as u64),
compressed: b(stats.compressor_page_count as u64),
purgeable: b(stats.purgeable_count as u64),
internal: b(stats.internal_page_count as u64),
external: b(stats.external_page_count as u64),
}
}
pub fn loadavg() -> [f64; 3] {
let mut loads = [0.0f64; 3];
let n = unsafe { libc::getloadavg(loads.as_mut_ptr(), 3) };
if n == 3 {
loads
} else {
[0.0; 3]
}
}
pub fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn uptime_secs() -> u64 {
let mut tv = libc::timeval {
tv_sec: 0,
tv_usec: 0,
};
let mut size = std::mem::size_of::<libc::timeval>();
let mut mib = [libc::CTL_KERN, libc::KERN_BOOTTIME];
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
(&mut tv as *mut libc::timeval).cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || tv.tv_sec <= 0 {
return 0;
}
now_secs().saturating_sub(tv.tv_sec as u64)
}
pub fn kill(pid: u32, signal: i32) -> std::io::Result<()> {
let rc = unsafe { libc::kill(pid as libc::pid_t, signal as c_int) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
pub fn setpriority(pid: u32, nice: i32) -> std::io::Result<()> {
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
pub fn getpriority(pid: u32) -> i32 {
unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_process_list_is_non_empty() {
let pids = list_pids().expect("proc_listallpids must work");
assert!(!pids.is_empty());
assert!(pids.contains(&1));
let mut sorted = pids.clone();
sorted.dedup();
assert_eq!(sorted.len(), pids.len(), "no duplicate pids");
}
#[test]
fn self_task_info_is_readable() {
let me = std::process::id();
let info = task_all_info(me).expect("own task info is always readable");
assert_eq!(info.bsd.pid, me);
assert!(info.bsd.ppid > 0);
assert!(info.task.resident_size > 0);
assert!(info.task.threads >= 1);
assert!(info.task.virtual_size >= info.task.resident_size);
assert!(info.bsd.start_sec > 0, "start time must be known");
}
#[test]
fn self_uid_matches_the_real_uid() {
let me = std::process::id();
let info = task_all_info(me).unwrap();
let uid = unsafe { libc::getuid() };
assert_eq!(info.bsd.uid, uid);
}
#[test]
fn self_exe_path_is_absolute() {
let me = std::process::id();
let path = exe_path(me).expect("own exe is readable");
assert!(path.starts_with('/'), "got {path:?}");
}
#[test]
fn rusage_is_readable_for_self() {
let me = std::process::id();
let io = rusage(me).expect("own rusage is readable");
println!("rusage: read={} written={}", io.disk_read, io.disk_written);
}
#[test]
fn self_procargs2_mentions_the_test_binary() {
let me = std::process::id();
let raw = procargs2(me).expect("own KERN_PROCARGS2 is readable");
let cmd = crate::darwin::parse::parse_procargs2(&raw).expect("must parse");
assert!(!cmd.is_empty());
}
#[test]
fn total_memory_is_plausible() {
let m = total_memory();
assert!(m > 256 * 1024 * 1024, "got {m}");
}
#[test]
fn cpu_ticks_include_aggregate() {
let ticks = cpu_ticks();
assert!(ticks.len() >= 2, "aggregate plus at least one core");
assert!(ticks[0].total() > 0);
let cores = cpu_count();
assert_eq!(ticks.len(), cores + 1, "one aggregate plus every core");
}
#[test]
fn vm_stats_are_non_zero() {
let vm = vm_stats();
assert!(vm.free > 0 || vm.active > 0);
}
#[test]
fn loadavg_is_sane() {
let l = loadavg();
assert!(l[0] >= 0.0);
}
#[test]
fn uptime_is_positive() {
assert!(uptime_secs() > 0);
}
#[test]
fn swap_usage_is_plausible() {
let swap = swap_usage();
if swap.total == 0 {
assert_eq!(swap.used, 0, "no swap total means nothing can be in use");
assert_eq!(swap.free, 0);
} else {
assert!(swap.used <= swap.total, "used exceeds total");
assert_eq!(swap.total, swap.used + swap.free);
}
}
#[test]
fn nonexistent_pid_yields_none() {
assert!(
task_all_info(4_194_303).is_none(),
"pid 4_194_303 cannot exist"
);
}
}