use std::{
ffi::{c_int, c_void, OsStr},
os::unix::ffi::OsStrExt,
path::Path,
sync::OnceLock,
time::Duration,
};
use rustix::{
io::Errno,
process::{kill_process, Pid},
};
use crate::{FileIdentity, MemoryKind, ResourceUsage, Signal};
const KINFO_PROC_SIZE: usize = 648;
const STARTTIME_SEC_OFFSET: usize = 0;
const STARTTIME_USEC_OFFSET: usize = 8;
const P_STAT_OFFSET: usize = 36;
const P_PID_OFFSET: usize = 40;
const SZOMB: u8 = 5;
#[repr(C, align(8))]
struct KinfoProc([u8; KINFO_PROC_SIZE]);
fn kinfo_proc(pid: u32) -> Option<KinfoProc> {
let pid = c_int::try_from(pid).ok()?;
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
let mut info = KinfoProc([0; KINFO_PROC_SIZE]);
let mut len: libc::size_t = KINFO_PROC_SIZE;
#[allow(unsafe_code)]
let result = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
info.0.as_mut_ptr().cast::<c_void>(),
&mut len,
std::ptr::null_mut(),
0,
)
};
if result != 0 || len != KINFO_PROC_SIZE {
return None;
}
let reported_pid = i32::from_ne_bytes(info.0[P_PID_OFFSET..P_PID_OFFSET + 4].try_into().ok()?);
(reported_pid == pid).then_some(info)
}
pub(crate) fn exists(pid: u32) -> bool {
start_time(pid).is_some()
}
pub(crate) fn start_time(pid: u32) -> Option<u64> {
let info = kinfo_proc(pid)?;
if info.0[P_STAT_OFFSET] == SZOMB {
return None;
}
let seconds = i64::from_ne_bytes(
info.0[STARTTIME_SEC_OFFSET..STARTTIME_SEC_OFFSET + 8]
.try_into()
.ok()?,
);
let micros = i32::from_ne_bytes(
info.0[STARTTIME_USEC_OFFSET..STARTTIME_USEC_OFFSET + 4]
.try_into()
.ok()?,
);
let seconds = u64::try_from(seconds).ok()?;
let micros = u64::try_from(micros).ok()?;
seconds.checked_mul(1_000_000)?.checked_add(micros)
}
pub(crate) fn executable_identity(pid: u32) -> Option<FileIdentity> {
let pid = c_int::try_from(pid).ok()?;
let mut buffer = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
#[allow(unsafe_code)]
let written = unsafe {
libc::proc_pidpath(
pid,
buffer.as_mut_ptr().cast::<c_void>(),
buffer.len() as u32,
)
};
let written = usize::try_from(written).ok().filter(|&n| n > 0)?;
buffer.truncate(written.min(buffer.len()));
crate::file_identity(Path::new(OsStr::from_bytes(&buffer)))
}
pub(crate) fn resource_usage(pid: u32) -> Option<ResourceUsage> {
let raw_pid = c_int::try_from(pid).ok()?;
let mut info = libc::rusage_info_v2 {
ri_uuid: [0; 16],
ri_user_time: 0,
ri_system_time: 0,
ri_pkg_idle_wkups: 0,
ri_interrupt_wkups: 0,
ri_pageins: 0,
ri_wired_size: 0,
ri_resident_size: 0,
ri_phys_footprint: 0,
ri_proc_start_abstime: 0,
ri_proc_exit_abstime: 0,
ri_child_user_time: 0,
ri_child_system_time: 0,
ri_child_pkg_idle_wkups: 0,
ri_child_interrupt_wkups: 0,
ri_child_pageins: 0,
ri_child_elapsed_abstime: 0,
ri_diskio_bytesread: 0,
ri_diskio_byteswritten: 0,
};
#[allow(unsafe_code)]
let result = unsafe {
libc::proc_pid_rusage(
raw_pid,
libc::RUSAGE_INFO_V2,
(&mut info as *mut libc::rusage_info_v2).cast::<libc::rusage_info_t>(),
)
};
if result != 0 {
return None;
}
start_time(pid)?;
Some(ResourceUsage {
memory_bytes: info.ri_phys_footprint,
memory_kind: MemoryKind::PhysFootprint,
swap_bytes: None,
cpu_user: mach_ticks_to_duration(info.ri_user_time),
cpu_system: mach_ticks_to_duration(info.ri_system_time),
})
}
fn mach_ticks_to_duration(ticks: u64) -> Duration {
let (numer, denom) = mach_timebase();
let nanos = u128::from(ticks) * u128::from(numer) / u128::from(denom.max(1));
Duration::from_nanos(u64::try_from(nanos).unwrap_or(u64::MAX))
}
#[allow(deprecated)]
fn mach_timebase() -> (u32, u32) {
static TIMEBASE: OnceLock<(u32, u32)> = OnceLock::new();
*TIMEBASE.get_or_init(|| {
let mut info = libc::mach_timebase_info { numer: 0, denom: 0 };
#[allow(unsafe_code)]
let result = unsafe { libc::mach_timebase_info(&mut info) };
if result == 0 && info.numer != 0 && info.denom != 0 {
(info.numer, info.denom)
} else {
(1, 1)
}
})
}
pub(crate) fn signal(pid: u32, signal: Signal) -> Result<(), Errno> {
let raw = i32::try_from(pid)
.ok()
.and_then(Pid::from_raw)
.ok_or(Errno::SRCH)?;
let signal = match signal {
Signal::Terminate => rustix::process::Signal::TERM,
Signal::Kill => rustix::process::Signal::KILL,
};
kill_process(raw, signal)
}