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, 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 { compare_procs(interval).unwrap_or_else(|err| { log::warn!("Failed to get processes: {}", err); vec![] }) } fn compare_procs(interval: Duration) -> io::Result> { 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::(0); let slice: &[u8] = mem::transmute(slice); CStr::from_bytes_until_nul(slice).unwrap_or_default() } } fn get_procs() -> io::Result> { // To understand what's going on here, see the sysctl(3) manpage for FreeBSD. unsafe { const STRUCT_SIZE: usize = mem::size_of::(); 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 = 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> { 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 = 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(ctl_name: &[i32]) -> io::Result { let mut value: MaybeUninit = 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 { // 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 } }