nushell/crates/nu-system/src/freebsd.rs
Devyn Cairns 9845d13347
fix nu-system build on arm64 FreeBSD (#13196)
# Description

Fixes #13194

`ki_stat` is supposed to be a `c_char`, but was defined was `i8`.
Unfortunately, `c_char` is `u8` on Aarch64 (on all platforms), so this
doesn't compile. I fixed it to use `c_char` instead.

Double checked whether NetBSD is affected, but the `libc` code defines
it as `i8` for some reason (erroneously, really) but that doesn't matter
too much. Anyway should be ok there.

Confirmed to be working.
2024-06-21 03:03:10 -07:00

306 lines
9.8 KiB
Rust

use itertools::{EitherOrBoth, Itertools};
use libc::{
c_char, kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS,
TDF_IDLETD,
};
use std::{
ffi::CStr,
io,
mem::{self, MaybeUninit},
ptr,
time::{Duration, Instant},
};
#[derive(Debug)]
pub struct ProcessInfo {
pub pid: i32,
pub ppid: i32,
pub name: String,
pub argv: Vec<u8>,
pub stat: c_char,
pub percent_cpu: f64,
pub mem_resident: u64, // in bytes
pub mem_virtual: u64, // in bytes
}
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
compare_procs(interval).unwrap_or_else(|err| {
log::warn!("Failed to get processes: {}", err);
vec![]
})
}
fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
let pagesize = get_pagesize()? as u64;
// Compare two full snapshots of all of the processes over the interval
let now = Instant::now();
let procs_a = get_procs()?;
std::thread::sleep(interval);
let procs_b = get_procs()?;
let true_interval = Instant::now().saturating_duration_since(now);
let true_interval_sec = true_interval.as_secs_f64();
// Group all of the threads in each process together
let a_grouped = procs_a.into_iter().group_by(|proc| proc.ki_pid);
let b_grouped = procs_b.into_iter().group_by(|proc| proc.ki_pid);
// Join the processes between the two snapshots
Ok(a_grouped
.into_iter()
.merge_join_by(b_grouped.into_iter(), |(pid_a, _), (pid_b, _)| {
pid_a.cmp(pid_b)
})
.map(|threads| {
// Join the threads between the two snapshots for the process
let mut threads = {
let (left, right) = threads.left_and_right();
left.into_iter()
.flat_map(|(_, threads)| threads)
.merge_join_by(
right.into_iter().flat_map(|(_, threads)| threads),
|thread_a, thread_b| thread_a.ki_tid.cmp(&thread_b.ki_tid),
)
.peekable()
};
// Pick the later process entry of the first thread to use for basic process information
let proc = match threads.peek().ok_or(io::ErrorKind::NotFound)? {
EitherOrBoth::Both(_, b) => b,
EitherOrBoth::Left(a) => a,
EitherOrBoth::Right(b) => b,
}
.clone();
// Skip over the idle process. It always appears with high CPU usage when the
// system is idle
if proc.ki_tdflags as u64 & TDF_IDLETD as u64 != 0 {
return Err(io::ErrorKind::NotFound.into());
}
// Aggregate all of the threads that exist in both snapshots and sum their runtime.
let (runtime_a, runtime_b) =
threads
.flat_map(|t| t.both())
.fold((0., 0.), |(runtime_a, runtime_b), (a, b)| {
let runtime_in_seconds =
|proc: &kinfo_proc| proc.ki_runtime as f64 /* µsec */ / 1_000_000.0;
(
runtime_a + runtime_in_seconds(&a),
runtime_b + runtime_in_seconds(&b),
)
});
// The percentage CPU is the ratio of how much runtime occurred for the process out of
// the true measured interval that occurred.
let percent_cpu = 100. * (runtime_b - runtime_a).max(0.) / true_interval_sec;
let info = ProcessInfo {
pid: proc.ki_pid,
ppid: proc.ki_ppid,
name: read_cstr(&proc.ki_comm).to_string_lossy().into_owned(),
argv: get_proc_args(proc.ki_pid)?,
stat: proc.ki_stat,
percent_cpu,
mem_resident: proc.ki_rssize.max(0) as u64 * pagesize,
mem_virtual: proc.ki_size.max(0) as u64,
};
Ok(info)
})
// Remove errors from the list - probably just processes that are gone now
.flat_map(|result: io::Result<_>| result.ok())
.collect())
}
fn check(err: libc::c_int) -> std::io::Result<()> {
if err < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
/// This is a bounds-checked way to read a `CStr` from a slice of `c_char`
fn read_cstr(slice: &[libc::c_char]) -> &CStr {
unsafe {
// SAFETY: ensure that c_char and u8 are the same size
mem::transmute::<libc::c_char, u8>(0);
let slice: &[u8] = mem::transmute(slice);
CStr::from_bytes_until_nul(slice).unwrap_or_default()
}
}
fn get_procs() -> io::Result<Vec<libc::kinfo_proc>> {
// To understand what's going on here, see the sysctl(3) manpage for FreeBSD.
unsafe {
const STRUCT_SIZE: usize = mem::size_of::<libc::kinfo_proc>();
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ALL];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
ptr::null(),
0,
))?;
// data_len will be set in bytes, so divide by the size of the structure
let expected_len = data_len.div_ceil(STRUCT_SIZE);
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<libc::kinfo_proc> = Vec::with_capacity(expected_len);
data_len = vec.capacity() * STRUCT_SIZE;
// Call sysctl() again to put the result in the vec
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
ptr::null(),
0,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
let true_len = data_len.div_ceil(STRUCT_SIZE);
vec.set_len(true_len);
// Sort the procs by pid and then tid before using them
vec.sort_by_key(|p| (p.ki_pid, p.ki_tid));
Ok(vec)
}
}
fn get_proc_args(pid: i32) -> io::Result<Vec<u8>> {
unsafe {
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ARGS, pid];
// First, try to figure out how large a buffer we need to allocate
// (calling with NULL just tells us that)
let mut data_len = 0;
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
ptr::null(),
0,
))?;
// Now allocate the Vec and set data_len to the real number of bytes allocated
let mut vec: Vec<u8> = Vec::with_capacity(data_len);
data_len = vec.capacity();
// Call sysctl() again to put the result in the vec
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
ptr::null(),
0,
))?;
// If that was ok, we can set the actual length of the vec to whatever
// data_len was changed to, since that should now all be properly initialized data.
vec.set_len(data_len);
Ok(vec)
}
}
// For getting simple values from the sysctl interface
unsafe fn get_ctl<T>(ctl_name: &[i32]) -> io::Result<T> {
let mut value: MaybeUninit<T> = MaybeUninit::uninit();
let mut value_len = mem::size_of_val(&value);
check(sysctl(
ctl_name.as_ptr(),
ctl_name.len() as u32,
value.as_mut_ptr() as *mut libc::c_void,
&mut value_len,
ptr::null(),
0,
))?;
Ok(value.assume_init())
}
fn get_pagesize() -> io::Result<libc::c_int> {
// not in libc for some reason
const HW_PAGESIZE: i32 = 7;
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
}
impl ProcessInfo {
/// PID of process
pub fn pid(&self) -> i32 {
self.pid
}
/// Parent PID of process
pub fn ppid(&self) -> i32 {
self.ppid
}
/// Name of command
pub fn name(&self) -> String {
let argv_name = self
.argv
.split(|b| *b == 0)
.next()
.map(String::from_utf8_lossy)
.unwrap_or_default()
.into_owned();
if !argv_name.is_empty() {
argv_name
} else {
// Just use the command name alone.
self.name.clone()
}
}
/// Full name of command, with arguments
pub fn command(&self) -> String {
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
// The command string is NUL separated
// Take the string up to the last NUL, then replace the NULs with spaces
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
} else {
// The argv is empty, so use the name instead
self.name()
}
}
/// Get the status of the process
pub fn status(&self) -> String {
match self.stat {
libc::SIDL | libc::SRUN => "Running",
libc::SSLEEP => "Sleeping",
libc::SSTOP => "Stopped",
libc::SWAIT => "Waiting",
libc::SLOCK => "Locked",
libc::SZOMB => "Zombie",
_ => "Unknown",
}
.into()
}
/// CPU usage as a percent of total
pub fn cpu_usage(&self) -> f64 {
self.percent_cpu
}
/// Memory size in number of bytes
pub fn mem_size(&self) -> u64 {
self.mem_resident
}
/// Virtual memory size in bytes
pub fn virtual_size(&self) -> u64 {
self.mem_virtual
}
}