mirror of
https://github.com/nushell/nushell.git
synced 2025-04-09 21:28:55 +02:00
# Description I feel like it's a little sad that BSDs get to enjoy almost everything other than the `ps` command, and there are some tests that rely on this command, so I figured it would be fun to patch that and make it work. The different BSDs have diverged from each other somewhat, but generally have a similar enough API for reading process information via `sysctl()`, with some slightly different args. This supports FreeBSD with the `freebsd` module, and NetBSD and OpenBSD with the `netbsd` module. OpenBSD is a fork of NetBSD and the interface has some minor differences but many things are the same. I had wanted to try to support DragonFlyBSD too, but their Rust version in the latest release is only 1.72.0, which is too old for me to want to try to compile rustc up to 1.77.2... but I will revisit this whenever they do update it. Dragonfly is a fork of FreeBSD, so it's likely to be more or less the same - I just don't want to enable it without testing it. Fixes #6862 (partially, we probably won't be adding `zfs list`) # User-Facing Changes `ps` added for FreeBSD, NetBSD, and OpenBSD. # Tests + Formatting The CI doesn't run tests for BSDs, so I'm not entirely sure if everything was already passing before. (Frankly, it's unlikely.) But nothing appears to be broken. # After Submitting - [ ] release notes? - [ ] DragonflyBSD, whenever they do update Rust to something close enough for me to try it
337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
//! This is used for both NetBSD and OpenBSD, because they are fairly similar.
|
|
|
|
use itertools::{EitherOrBoth, Itertools};
|
|
use libc::{sysctl, CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV};
|
|
use std::{
|
|
io,
|
|
mem::{self, MaybeUninit},
|
|
ptr,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
#[cfg(target_os = "netbsd")]
|
|
type KInfoProc = libc::kinfo_proc2;
|
|
#[cfg(target_os = "openbsd")]
|
|
type KInfoProc = libc::kinfo_proc;
|
|
|
|
#[derive(Debug)]
|
|
pub struct ProcessInfo {
|
|
pub pid: i32,
|
|
pub ppid: i32,
|
|
pub argv: Vec<u8>,
|
|
pub stat: i8,
|
|
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();
|
|
|
|
// Join the processes between the two snapshots
|
|
Ok(procs_a
|
|
.into_iter()
|
|
.merge_join_by(procs_b.into_iter(), |a, b| a.p_pid.cmp(&b.p_pid))
|
|
.map(|proc| {
|
|
// Take both snapshotted processes if we can, but if not then just keep the one that
|
|
// exists and set prev_proc to None
|
|
let (prev_proc, proc) = match proc {
|
|
EitherOrBoth::Both(a, b) => (Some(a), b),
|
|
EitherOrBoth::Left(a) => (None, a),
|
|
EitherOrBoth::Right(b) => (None, 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 = if let Some(prev_proc) = prev_proc {
|
|
let prev_rtime =
|
|
prev_proc.p_rtime_sec as f64 + prev_proc.p_rtime_usec as f64 / 1_000_000.0;
|
|
let rtime = proc.p_rtime_sec as f64 + proc.p_rtime_usec as f64 / 1_000_000.0;
|
|
100. * (rtime - prev_rtime).max(0.) / true_interval_sec
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
Ok(ProcessInfo {
|
|
pid: proc.p_pid,
|
|
ppid: proc.p_ppid,
|
|
argv: get_proc_args(proc.p_pid, KERN_PROC_ARGV)?,
|
|
stat: proc.p_stat,
|
|
percent_cpu,
|
|
mem_resident: proc.p_vm_rssize.max(0) as u64 * pagesize,
|
|
#[cfg(target_os = "netbsd")]
|
|
mem_virtual: proc.p_vm_msize.max(0) as u64 * pagesize,
|
|
#[cfg(target_os = "openbsd")]
|
|
mem_virtual: proc.p_vm_map_size.max(0) as u64 * pagesize,
|
|
})
|
|
})
|
|
// 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(())
|
|
}
|
|
}
|
|
|
|
/// Call `sysctl()` in read mode (i.e. the last two arguments are NULL and zero)
|
|
unsafe fn sysctl_get(
|
|
name: *const i32,
|
|
name_len: u32,
|
|
data: *mut libc::c_void,
|
|
data_len: *mut usize,
|
|
) -> i32 {
|
|
sysctl(
|
|
name,
|
|
name_len,
|
|
data,
|
|
data_len,
|
|
// NetBSD and OpenBSD differ in mutability for this pointer, but it's null anyway
|
|
#[cfg(target_os = "netbsd")]
|
|
ptr::null(),
|
|
#[cfg(target_os = "openbsd")]
|
|
ptr::null_mut(),
|
|
0,
|
|
)
|
|
}
|
|
|
|
fn get_procs() -> io::Result<Vec<KInfoProc>> {
|
|
// To understand what's going on here, see the sysctl(3) and sysctl(7) manpages for NetBSD.
|
|
unsafe {
|
|
const STRUCT_SIZE: usize = mem::size_of::<KInfoProc>();
|
|
|
|
#[cfg(target_os = "netbsd")]
|
|
const TGT_KERN_PROC: i32 = libc::KERN_PROC2;
|
|
#[cfg(target_os = "openbsd")]
|
|
const TGT_KERN_PROC: i32 = libc::KERN_PROC;
|
|
|
|
let mut ctl_name = [
|
|
CTL_KERN,
|
|
TGT_KERN_PROC,
|
|
KERN_PROC_ALL,
|
|
0,
|
|
STRUCT_SIZE as i32,
|
|
0,
|
|
];
|
|
|
|
// 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_get(
|
|
ctl_name.as_ptr(),
|
|
ctl_name.len() as u32,
|
|
ptr::null_mut(),
|
|
&mut data_len,
|
|
))?;
|
|
|
|
// 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<KInfoProc> = Vec::with_capacity(expected_len);
|
|
data_len = vec.capacity() * STRUCT_SIZE;
|
|
|
|
// We are also supposed to set ctl_name[5] to the number of structures we want
|
|
ctl_name[5] = expected_len.try_into().expect("expected_len too big");
|
|
|
|
// Call sysctl() again to put the result in the vec
|
|
check(sysctl_get(
|
|
ctl_name.as_ptr(),
|
|
ctl_name.len() as u32,
|
|
vec.as_mut_ptr() as *mut libc::c_void,
|
|
&mut data_len,
|
|
))?;
|
|
|
|
// 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 before using them
|
|
vec.sort_by_key(|p| p.p_pid);
|
|
Ok(vec)
|
|
}
|
|
}
|
|
|
|
fn get_proc_args(pid: i32, what: i32) -> io::Result<Vec<u8>> {
|
|
unsafe {
|
|
let ctl_name = [CTL_KERN, KERN_PROC_ARGS, pid, what];
|
|
|
|
// 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_get(
|
|
ctl_name.as_ptr(),
|
|
ctl_name.len() as u32,
|
|
ptr::null_mut(),
|
|
&mut data_len,
|
|
))?;
|
|
|
|
// 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_get(
|
|
ctl_name.as_ptr(),
|
|
ctl_name.len() as u32,
|
|
vec.as_mut_ptr() as *mut libc::c_void,
|
|
&mut data_len,
|
|
))?;
|
|
|
|
// 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);
|
|
|
|
// On OpenBSD we have to do an extra step, because it fills the buffer with pointers to the
|
|
// strings first, even though the strings are within the buffer as well.
|
|
#[cfg(target_os = "openbsd")]
|
|
let vec = {
|
|
use std::ffi::CStr;
|
|
|
|
// Set up some bounds checking. We assume there will be some pointers at the base until
|
|
// we reach NULL, but we want to make sure we only ever read data within the range of
|
|
// min_ptr..max_ptr.
|
|
let ptrs = vec.as_ptr() as *const *const u8;
|
|
let min_ptr = vec.as_ptr() as *const u8;
|
|
let max_ptr = vec.as_ptr().add(vec.len()) as *const u8;
|
|
let max_index: isize = (vec.len() / mem::size_of::<*const u8>())
|
|
.try_into()
|
|
.expect("too big for isize");
|
|
|
|
let mut new_vec = Vec::with_capacity(vec.len());
|
|
for index in 0..max_index {
|
|
let ptr = ptrs.offset(index);
|
|
if *ptr == ptr::null() {
|
|
break;
|
|
} else {
|
|
// Make sure it's within the bounds of the buffer
|
|
assert!(
|
|
*ptr >= min_ptr && *ptr < max_ptr,
|
|
"pointer out of bounds of the buffer returned by sysctl()"
|
|
);
|
|
// Also bounds-check the C strings, to make sure we don't overrun the buffer
|
|
new_vec.extend(
|
|
CStr::from_bytes_until_nul(std::slice::from_raw_parts(
|
|
*ptr,
|
|
max_ptr.offset_from(*ptr) as usize,
|
|
))
|
|
.expect("invalid C string")
|
|
.to_bytes_with_nul(),
|
|
);
|
|
}
|
|
}
|
|
new_vec
|
|
};
|
|
|
|
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_get(
|
|
ctl_name.as_ptr(),
|
|
ctl_name.len() as u32,
|
|
value.as_mut_ptr() as *mut libc::c_void,
|
|
&mut value_len,
|
|
))?;
|
|
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 {
|
|
self.argv
|
|
.split(|b| *b == 0)
|
|
.next()
|
|
.map(String::from_utf8_lossy)
|
|
.unwrap_or_default()
|
|
.into_owned()
|
|
}
|
|
|
|
/// 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 {
|
|
"".into()
|
|
}
|
|
}
|
|
|
|
/// Get the status of the process
|
|
pub fn status(&self) -> String {
|
|
// see sys/proc.h (OpenBSD), sys/lwp.h (NetBSD)
|
|
// the names given here are the NetBSD ones, starting with LS*, but the OpenBSD ones are
|
|
// the same, just starting with S* instead
|
|
match self.stat {
|
|
1 /* LSIDL */ => "",
|
|
2 /* LSRUN */ => "Waiting",
|
|
3 /* LSSLEEP */ => "Sleeping",
|
|
4 /* LSSTOP */ => "Stopped",
|
|
5 /* LSZOMB */ => "Zombie",
|
|
#[cfg(target_os = "openbsd")] // removed in NetBSD
|
|
6 /* LSDEAD */ => "Dead",
|
|
7 /* LSONPROC */ => "Running",
|
|
#[cfg(target_os = "netbsd")] // doesn't exist in OpenBSD
|
|
8 /* LSSUSPENDED */ => "Suspended",
|
|
_ => "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
|
|
}
|
|
}
|