use itertools::{EitherOrBoth, Itertools};
use libc::{CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV, sysctl};
use std::{
io,
mem::{self, MaybeUninit},
ptr,
time::Duration,
};
use nu_utils::time::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, pub mem_virtual: u64, }
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;
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();
Ok(procs_a
.into_iter()
.merge_join_by(procs_b.into_iter(), |a, b| a.p_pid.cmp(&b.p_pid))
.map(|proc| {
let (prev_proc, proc) = match proc {
EitherOrBoth::Both(a, b) => (Some(a), b),
EitherOrBoth::Left(a) => (None, a),
EitherOrBoth::Right(b) => (None, b),
};
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,
})
})
.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(())
}
}
unsafe fn sysctl_get(
name: *const i32,
name_len: u32,
data: *mut libc::c_void,
data_len: *mut usize,
) -> i32 {
unsafe {
sysctl(
name,
name_len,
data,
data_len,
#[cfg(target_os = "netbsd")]
ptr::null(),
#[cfg(target_os = "openbsd")]
ptr::null_mut(),
0,
)
}
}
fn get_procs() -> io::Result<Vec<KInfoProc>> {
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,
];
let mut data_len = 0;
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
))?;
let expected_len = data_len.div_ceil(STRUCT_SIZE);
let mut vec: Vec<KInfoProc> = Vec::with_capacity(expected_len);
data_len = vec.capacity() * STRUCT_SIZE;
ctl_name[5] = expected_len.try_into().expect("expected_len too big");
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
))?;
let true_len = data_len.div_ceil(STRUCT_SIZE);
vec.set_len(true_len);
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];
let mut data_len = 0;
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
ptr::null_mut(),
&mut data_len,
))?;
let mut vec: Vec<u8> = Vec::with_capacity(data_len);
data_len = vec.capacity();
check(sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
vec.as_mut_ptr() as *mut libc::c_void,
&mut data_len,
))?;
vec.set_len(data_len);
#[cfg(target_os = "openbsd")]
let vec = {
use std::ffi::CStr;
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 {
assert!(
*ptr >= min_ptr && *ptr < max_ptr,
"pointer out of bounds of the buffer returned by sysctl()"
);
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)
}
}
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(unsafe {
sysctl_get(
ctl_name.as_ptr(),
ctl_name.len() as u32,
value.as_mut_ptr() as *mut libc::c_void,
&mut value_len,
)
})?;
assert_eq!(
value_len,
mem::size_of_val(&value),
"Data requested from from `sysctl` diverged in size from the expected return type. For variable length data you need to manually truncate the data to the valid returned size!"
);
Ok(unsafe { value.assume_init() })
}
fn get_pagesize() -> io::Result<libc::c_int> {
const HW_PAGESIZE: i32 = 7;
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
}
impl ProcessInfo {
pub fn pid(&self) -> i32 {
self.pid
}
pub fn ppid(&self) -> i32 {
self.ppid
}
pub fn name(&self) -> String {
self.argv
.split(|b| *b == 0)
.next()
.map(String::from_utf8_lossy)
.unwrap_or_default()
.into_owned()
}
pub fn command(&self) -> String {
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
} else {
"".into()
}
}
pub fn status(&self) -> String {
match self.stat {
1 => "",
2 => "Waiting",
3 => "Sleeping",
4 => "Stopped",
5 => "Zombie",
#[cfg(target_os = "openbsd")] 6 => "Dead",
7 => "Running",
#[cfg(target_os = "netbsd")] 8 => "Suspended",
_ => "Unknown",
}
.into()
}
pub fn cpu_usage(&self) -> f64 {
self.percent_cpu
}
pub fn mem_size(&self) -> u64 {
self.mem_resident
}
pub fn virtual_size(&self) -> u64 {
self.mem_virtual
}
}