use std::ffi::{CStr, CString};
use libc::{c_char, c_int, c_void};
#[derive(Copy, Clone, Debug, Default)]
pub struct RawProc {
pub pid: u32,
pub tid: i32,
pub ppid: u32,
pub pgid: u32,
pub sid: u32,
pub tpgid: i32,
pub stat: i8,
pub tdf_intr: bool,
pub slptime: u32,
pub nice: i8,
pub pri_class: u8,
pub pri_level: u8,
pub uid: u32,
pub runtime_us: u64,
pub start_sec: i64,
pub start_usec: i64,
pub rss_pages: i64,
pub vsize: u64,
pub num_threads: i32,
pub oncpu: i32,
pub lastcpu: i32,
pub min_flt: u64,
pub maj_flt: u64,
pub tty_dev: u64,
pub comm: [c_char; COMM_LEN + 1],
pub tdname: [c_char; TDNAMLEN + 1],
pub jail: i32,
}
const COMM_LEN: usize = 19;
const TDF_SINTR: u64 = 0x0000_0008;
const TDNAMLEN: usize = 15;
impl RawProc {
pub fn comm_str(&self) -> String {
c_array_to_string(&self.comm)
}
pub fn tdname_str(&self) -> String {
c_array_to_string(&self.tdname)
}
pub fn is_kernel_thread(&self) -> bool {
self.pri_class == PRI_ITHD
}
}
pub const PRI_ITHD: u8 = 1; pub const PRI_REALTIME: u8 = 2;
pub const PRI_TIMESHARE: u8 = 3;
pub const PRI_IDLE: u8 = 4;
fn c_array_to_string(arr: &[c_char]) -> String {
let bytes: Vec<u8> = arr
.iter()
.take_while(|c| **c != 0)
.map(|c| char_byte(*c))
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
#[inline]
fn char_byte(c: c_char) -> u8 {
#[allow(clippy::unnecessary_cast)]
{
c as u8
}
}
fn sysctl_buf(mib: &mut [c_int]) -> std::io::Result<Vec<u8>> {
let mut size: usize = 0;
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
std::ptr::null_mut(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
for attempt in 0..3usize {
let capacity = size + 4096 + attempt * attempt * 65_536;
let mut buf = vec![0u8; capacity];
let mut got = capacity;
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 {
buf.truncate(got);
return Ok(buf);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::ENOMEM) {
return Err(err);
}
size = got.max(size);
}
Err(std::io::Error::other("sysctl table kept growing"))
}
type KInfoProc = libc::kinfo_proc;
pub fn list_processes() -> std::io::Result<Vec<RawProc>> {
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PROC, 0];
let buf = sysctl_buf(&mut mib)?;
Ok(decode(&buf))
}
pub fn list_threads(pid: u32) -> std::io::Result<Vec<RawProc>> {
if pid == 0 {
return Ok(Vec::new());
}
let mut mib = [
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_PID | libc::KERN_PROC_INC_THREAD,
pid as c_int,
];
let buf = sysctl_buf(&mut mib)?;
Ok(decode(&buf))
}
fn decode(buf: &[u8]) -> Vec<RawProc> {
let stride = std::mem::size_of::<KInfoProc>();
if stride == 0 {
return Vec::new();
}
let count = buf.len() / stride;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let start = i * stride;
let kp: KInfoProc = unsafe {
std::ptr::read_unaligned(buf.as_ptr().add(start).cast::<KInfoProc>())
};
if kp.ki_structsize as usize != stride {
break;
}
let mut comm = [0 as c_char; COMM_LEN + 1];
for (dst, src) in comm.iter_mut().zip(kp.ki_comm.iter()) {
*dst = *src;
}
let mut tdname = [0 as c_char; TDNAMLEN + 1];
for (dst, src) in tdname.iter_mut().zip(kp.ki_tdname.iter()) {
*dst = *src;
}
out.push(RawProc {
pid: kp.ki_pid.max(0) as u32,
tid: kp.ki_tid,
ppid: kp.ki_ppid.max(0) as u32,
pgid: kp.ki_pgid.max(0) as u32,
sid: kp.ki_sid.max(0) as u32,
tpgid: kp.ki_tpgid,
stat: kp.ki_stat as i8,
tdf_intr: (kp.ki_tdflags as u64 & TDF_SINTR) != 0,
slptime: kp.ki_slptime,
nice: kp.ki_nice as i8,
pri_class: kp.ki_pri.pri_class as u8,
pri_level: kp.ki_pri.pri_level as u8,
uid: kp.ki_uid,
runtime_us: kp.ki_runtime,
start_sec: kp.ki_start.tv_sec as i64,
start_usec: kp.ki_start.tv_usec as i64,
rss_pages: kp.ki_rssize as i64,
vsize: kp.ki_size as u64,
num_threads: kp.ki_numthreads,
oncpu: kp.ki_oncpu,
lastcpu: kp.ki_lastcpu,
min_flt: kp.ki_rusage.ru_minflt as u64,
maj_flt: kp.ki_rusage.ru_majflt as u64,
tty_dev: kp.ki_tdev as u64,
comm,
tdname,
jail: kp.ki_jid,
});
}
out
}
pub fn validate() -> Result<(), String> {
let stride = std::mem::size_of::<KInfoProc>();
let procs = list_processes().map_err(|e| format!("kern.proc: {e}"))?;
if procs.is_empty() {
return Err("kern.proc returned no processes".to_string());
}
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PROC, 0];
let raw = sysctl_buf(&mut mib).map_err(|e| format!("kern.proc: {e}"))?;
if raw.len() < stride {
return Err(format!(
"kern.proc returned {} bytes, less than one {stride}-byte record",
raw.len()
));
}
let first: KInfoProc =
unsafe { std::ptr::read_unaligned(raw.as_ptr().cast::<KInfoProc>()) };
if first.ki_structsize as usize != stride {
return Err(format!(
"kinfo_proc layout mismatch: kernel says {}, this build has {stride}; \
build gander for this FreeBSD version",
first.ki_structsize
));
}
let init = procs.iter().find(|p| p.pid == 1);
match init {
Some(p) if p.comm_str().starts_with("init") && p.uid == 0 => Ok(()),
Some(p) => Err(format!(
"pid 1 looks wrong: comm={:?} uid={}, expected init/0 — kinfo_proc offsets are off",
p.comm_str(),
p.uid
)),
None => Err("pid 1 missing from kern.proc".to_string()),
}
}
pub fn process(pid: u32) -> Option<RawProc> {
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid as c_int];
let buf = sysctl_buf(&mut mib).ok()?;
decode(&buf).into_iter().next()
}
pub fn proc_string(selector: c_int, pid: u32, max: usize) -> Option<Vec<u8>> {
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, selector, pid as c_int];
let mut buf = vec![0u8; max];
let mut size = buf.len();
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 == 0 {
return None;
}
buf.truncate(size);
Some(buf)
}
pub fn exe_path(pid: u32) -> Option<String> {
let raw = proc_string(libc::KERN_PROC_PATHNAME, pid, libc::PATH_MAX as usize)?;
let text = cstr_prefix(&raw);
(!text.is_empty()).then_some(text)
}
pub fn cwd_path(pid: u32) -> Option<String> {
let want = std::mem::size_of::<libc::kinfo_file>();
let raw = proc_string(libc::KERN_PROC_CWD, pid, want + 256)?;
if raw.len() < want {
return None;
}
let kf: libc::kinfo_file = unsafe { std::ptr::read_unaligned(raw.as_ptr().cast()) };
let bytes: Vec<u8> = kf
.kf_path
.iter()
.take_while(|c| **c != 0)
.map(|c| char_byte(*c))
.collect();
let text = String::from_utf8_lossy(&bytes).into_owned();
(!text.is_empty()).then_some(text)
}
pub fn cmdline(pid: u32) -> Option<String> {
let raw = proc_string(libc::KERN_PROC_ARGS, pid, 256 * 1024)?;
let mut out = String::with_capacity(raw.len());
let mut rest = &raw[..];
while !rest.is_empty() {
let end = rest.iter().position(|b| *b == 0).unwrap_or(rest.len());
if end > 0 {
if !out.is_empty() {
out.push(' ');
}
out.push_str(&String::from_utf8_lossy(&rest[..end]));
}
if end == rest.len() {
break;
}
rest = &rest[end + 1..];
}
(!out.is_empty()).then_some(out)
}
fn cstr_prefix(raw: &[u8]) -> String {
let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len());
String::from_utf8_lossy(&raw[..end]).into_owned()
}
#[allow(clippy::unnecessary_cast)] pub fn boot_time() -> Option<i64> {
let mut tv = std::mem::MaybeUninit::<libc::timeval>::zeroed();
let mut size = std::mem::size_of::<libc::timeval>();
let rc = unsafe {
libc::sysctlbyname(
c"kern.boottime".as_ptr(),
tv.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return None;
}
let tv = unsafe { tv.assume_init() };
(tv.tv_sec > 0).then_some(tv.tv_sec as i64)
}
pub fn cp_time() -> Option<[i64; 5]> {
let mut buf = [0 as libc::c_long; 5];
let mut size = std::mem::size_of::<[libc::c_long; 5]>();
let rc = unsafe {
libc::sysctlbyname(
c"kern.cp_time".as_ptr(),
buf.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || size != std::mem::size_of::<[libc::c_long; 5]>() {
return None;
}
Some([
wide(buf[0]),
wide(buf[1]),
wide(buf[2]),
wide(buf[3]),
wide(buf[4]),
])
}
pub fn cp_times() -> Option<Vec<[i64; 5]>> {
let mut size: usize = 0;
let rc = unsafe {
libc::sysctlbyname(
c"kern.cp_times".as_ptr(),
std::ptr::null_mut(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || size == 0 || !size.is_multiple_of(5 * std::mem::size_of::<libc::c_long>()) {
return None;
}
let mut buf = vec![0 as libc::c_long; size / std::mem::size_of::<libc::c_long>()];
let rc = unsafe {
libc::sysctlbyname(
c"kern.cp_times".as_ptr(),
buf.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return None;
}
let groups = buf.as_chunks::<5>().0;
Some(
groups
.iter()
.map(|c| [wide(c[0]), wide(c[1]), wide(c[2]), wide(c[3]), wide(c[4])])
.collect(),
)
}
pub fn sysctl_u64(name: &CStr) -> Option<u64> {
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;
}
match size {
4 => {
let mut v: u32 = 0;
let mut len = 4usize;
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
(&mut v as *mut u32).cast::<c_void>(),
&mut len,
std::ptr::null_mut(),
0,
)
};
(rc == 0).then_some(v as u64)
}
8 => {
let mut v: u64 = 0;
let mut len = 8usize;
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
(&mut v as *mut u64).cast::<c_void>(),
&mut len,
std::ptr::null_mut(),
0,
)
};
(rc == 0).then_some(v)
}
_ => None,
}
}
#[inline]
fn wide(v: libc::c_long) -> i64 {
#[allow(clippy::unnecessary_cast)]
{
v as i64
}
}
pub fn swap_info() -> Option<crate::system::SwapInfo> {
let kd = Kvm::open()?;
let mut entries = [std::mem::MaybeUninit::<libc::kvm_swap>::zeroed(); 1];
let n = unsafe {
libc::kvm_getswapinfo(kd.0, entries.as_mut_ptr().cast(), 1, 0)
};
if n < 0 {
return None;
}
let entry = unsafe { entries[0].assume_init() };
let page = crate::freebsd::FreeBsdPlatform::page_size();
let total = (entry.ksw_total as u64).saturating_mul(page);
let used = (entry.ksw_used as u64).saturating_mul(page);
Some(crate::system::SwapInfo {
total,
used,
free: total.saturating_sub(used),
})
}
struct Kvm(*mut libc::kvm_t);
impl Kvm {
fn open() -> Option<Self> {
let mut err = [0 as c_char; 256];
let kd = unsafe {
libc::kvm_openfiles(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
libc::O_RDONLY,
err.as_mut_ptr(),
)
};
if kd.is_null() {
return None;
}
Some(Kvm(kd))
}
}
impl Drop for Kvm {
fn drop(&mut self) {
unsafe {
libc::kvm_close(self.0);
}
}
}
pub fn disk_counters() -> crate::system::DiskCounters {
let mut out = crate::system::DiskCounters::default();
let Some(kd) = Kvm::open() else {
return out;
};
let version = unsafe { libc::devstat_checkversion(kd.0) };
if version == 0 {
return out;
}
let mut statinfo = StatInfo {
dinfo: std::ptr::null_mut(),
..StatInfo::default()
};
let rc = unsafe { devstat_getdevs(kd.0, &mut statinfo) };
if rc != 0 || statinfo.dinfo.is_null() {
return out;
}
let dinfo = unsafe { &*statinfo.dinfo };
if dinfo.devices.is_null() || dinfo.numdevs <= 0 {
return out;
}
let devices = unsafe { std::slice::from_raw_parts(dinfo.devices, dinfo.numdevs as usize) };
for d in devices {
if (d.device_type as u32) & (libc::devstat_type_flags::DEVSTAT_TYPE_PASS as u32) != 0 {
continue;
}
out.read_bytes = out
.read_bytes
.saturating_add(d.bytes[libc::devstat_trans_flags::DEVSTAT_READ as usize]);
out.write_bytes = out
.write_bytes
.saturating_add(d.bytes[libc::devstat_trans_flags::DEVSTAT_WRITE as usize]);
}
out
}
#[repr(C)]
struct StatInfo {
busy_time: f64,
busy_time2: f64,
busy_other: std::ffi::c_long,
dcpyps: i32,
_pad: i32,
dinfo: *mut libc::devinfo,
busy_time_total: f64,
busy_other_total: std::ffi::c_long,
}
impl Default for StatInfo {
fn default() -> Self {
Self {
busy_time: 0.0,
busy_time2: 0.0,
busy_other: 0,
dcpyps: 0,
_pad: 0,
dinfo: std::ptr::null_mut(),
busy_time_total: 0.0,
busy_other_total: 0,
}
}
}
#[link(name = "devstat")]
extern "C" {
fn devstat_getdevs(kd: *mut libc::kvm_t, stats: *mut StatInfo) -> c_int;
}
pub fn cpu_count() -> usize {
sysctl_u64(c"hw.ncpu").unwrap_or(1).max(1) as usize
}
pub fn net_counters() -> crate::system::NetCounters {
let mut out = crate::system::NetCounters::default();
let mut mib = [libc::CTL_NET, libc::PF_ROUTE, 0, 0, libc::NET_RT_IFLIST, 0];
let Ok(buf) = sysctl_buf(&mut mib) else {
return out;
};
let mut offset = 0usize;
let hdr_len = std::mem::size_of::<libc::if_msghdr>();
while offset + hdr_len <= buf.len() {
let hdr: libc::if_msghdr = unsafe {
std::ptr::read_unaligned(buf.as_ptr().add(offset).cast::<libc::if_msghdr>())
};
let msglen = hdr.ifm_msglen as usize;
if msglen == 0 {
break; }
if hdr.ifm_type == libc::RTM_IFINFO as u8 {
if hdr.ifm_flags & libc::IFF_LOOPBACK == 0 {
out.rx_bytes = out.rx_bytes.saturating_add(hdr.ifm_data.ifi_ibytes);
out.tx_bytes = out.tx_bytes.saturating_add(hdr.ifm_data.ifi_obytes);
}
}
offset = offset.saturating_add(msglen);
}
out
}
pub fn user_name(uid: u32) -> Option<String> {
let mut buf = vec![0u8; 1024];
let mut pwd = std::mem::MaybeUninit::<libc::passwd>::zeroed();
let mut result: *mut libc::passwd = std::ptr::null_mut();
let rc = unsafe {
libc::getpwuid_r(
uid,
pwd.as_mut_ptr(),
buf.as_mut_ptr().cast::<c_char>(),
buf.len(),
&mut result,
)
};
if rc != 0 || result.is_null() {
return None;
}
let pwd = unsafe { pwd.assume_init() };
if pwd.pw_name.is_null() {
return None;
}
let name = unsafe { CStr::from_ptr(pwd.pw_name) };
Some(name.to_string_lossy().into_owned())
}
pub fn devname(dev: u64) -> Option<String> {
if dev == u64::MAX {
return None; }
let mut buf = [0 as c_char; 256];
let ptr = unsafe { libc::devname_r(dev, libc::S_IFCHR, buf.as_mut_ptr(), buf.len() as c_int) };
if ptr.is_null() {
return None;
}
let name = unsafe { CStr::from_ptr(buf.as_ptr()) };
let text = name.to_string_lossy().into_owned();
(!text.is_empty()).then_some(text)
}
pub fn hostname() -> Option<String> {
let mut buf = [0u8; 256];
let mut size = buf.len();
let rc = unsafe {
libc::sysctlbyname(
c"kern.hostname".as_ptr(),
buf.as_mut_ptr().cast::<c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return None;
}
let text = cstr_prefix(&buf[..size.min(buf.len())]);
(!text.is_empty()).then_some(text)
}
pub fn affinity(pid: u32, cores: usize) -> Option<Vec<u64>> {
let mut set = std::mem::MaybeUninit::<libc::cpuset_t>::zeroed();
let rc = unsafe {
libc::cpuset_getaffinity(
libc::CPU_LEVEL_WHICH,
libc::CPU_WHICH_PID,
pid as libc::id_t,
std::mem::size_of::<libc::cpuset_t>(),
set.as_mut_ptr(),
)
};
if rc != 0 {
return None;
}
let set = unsafe { set.assume_init() };
let bytes = unsafe {
std::slice::from_raw_parts(
(&set as *const libc::cpuset_t).cast::<u8>(),
std::mem::size_of::<libc::cpuset_t>(),
)
};
let mut words = vec![0u64; cores.div_ceil(64).max(1)];
for i in 0..cores {
let byte = bytes.get(i / 8).copied().unwrap_or(0);
if byte & (1 << (i % 8)) != 0 {
words[i / 64] |= 1 << (i % 64);
}
}
Some(words)
}
pub fn set_affinity(pid: u32, words: &[u64], cores: usize) -> Result<(), String> {
let mut set = std::mem::MaybeUninit::<libc::cpuset_t>::zeroed();
let set_ptr = set.as_mut_ptr();
unsafe {
std::ptr::write_bytes(set_ptr.cast::<u8>(), 0, std::mem::size_of::<libc::cpuset_t>());
let bytes = std::slice::from_raw_parts_mut(
set_ptr.cast::<u8>(),
std::mem::size_of::<libc::cpuset_t>(),
);
for i in 0..cores {
let word = words.get(i / 64).copied().unwrap_or(0);
if word & (1 << (i % 64)) != 0 {
if let Some(byte) = bytes.get_mut(i / 8) {
*byte |= 1 << (i % 8);
}
}
}
}
let rc = unsafe {
libc::cpuset_setaffinity(
libc::CPU_LEVEL_WHICH,
libc::CPU_WHICH_PID,
pid as libc::id_t,
std::mem::size_of::<libc::cpuset_t>(),
set.as_ptr(),
)
};
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().to_string())
}
}
#[allow(dead_code)]
pub fn cstring(name: &str) -> Option<CString> {
CString::new(name).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_runtime_layout_check_passes() {
validate().expect("kinfo_proc layout must match the running kernel");
}
#[test]
fn our_own_process_is_listed_with_a_command_line() {
let me = std::process::id();
let procs = list_processes().expect("kern.proc");
let mine = procs
.iter()
.find(|p| p.pid == me)
.expect("own pid must appear");
assert!(mine.runtime_us > 0, "cpu time must be non-zero");
assert!(mine.vsize > 0, "virtual size must be non-zero");
assert!(mine.rss_pages > 0, "resident size must be non-zero");
assert_eq!(mine.uid, unsafe { libc::getuid() });
assert!(cmdline(me).is_some(), "own cmdline must be readable");
assert!(exe_path(me).is_some(), "own exe must be readable");
assert!(cwd_path(me).is_some(), "own cwd must be readable");
}
#[test]
fn threads_of_our_own_process_include_the_leader() {
let me = std::process::id();
let threads = list_threads(me).expect("thread list");
assert!(!threads.is_empty());
assert!(
threads.iter().any(|t| t.pid == me),
"the leader is one of its own threads"
);
assert!(threads.iter().all(|t| t.tid != 0), "a tid is never zero");
}
#[test]
fn boot_time_is_in_the_past() {
let boot = boot_time().expect("kern.boottime");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert!(boot > 0 && boot <= now, "boot {boot} now {now}");
}
#[test]
fn cpu_counters_are_monotonic() {
let a = cp_time().expect("kern.cp_time");
std::thread::sleep(std::time::Duration::from_millis(30));
let b = cp_time().expect("kern.cp_time");
for i in 0..5 {
assert!(b[i] >= a[i], "CPUSTATES[{i}] went backwards: {} -> {}", a[i], b[i]);
}
let per = cp_times().expect("kern.cp_times");
assert_eq!(per.len(), crate::freebsd::FreeBsdPlatform::cpu_count());
}
#[test]
fn swap_is_readable_when_configured() {
if let Some(s) = swap_info() {
assert!(s.used <= s.total, "{s:?}");
}
}
#[test]
fn devname_resolves_our_tty_or_says_nothing() {
let me = std::process::id();
let procs = list_processes().unwrap();
if let Some(mine) = procs.iter().find(|p| p.pid == me) {
if mine.tty_dev != u64::MAX {
assert!(devname(mine.tty_dev).is_some());
}
}
assert_eq!(devname(u64::MAX), None, "no terminal is not a device name");
}
}