// Take a look at the license at the top of the repository in the LICENSE file. use crate::{ sys::{component::Component, Cpu, Disk, Networks, Process}, CpuRefreshKind, LoadAvg, Pid, ProcessRefreshKind, RefreshKind, SystemExt, User, }; use std::cell::UnsafeCell; use std::collections::HashMap; use std::ffi::CStr; use std::mem::MaybeUninit; use std::path::{Path, PathBuf}; use std::ptr::NonNull; use std::time::Duration; use super::utils::{ self, boot_time, c_buf_to_string, from_cstr_array, get_frequency_for_cpu, get_sys_value, get_sys_value_array, get_sys_value_by_name, get_sys_value_str_by_name, get_system_info, init_mib, }; use libc::c_int; declare_signals! { c_int, Signal::Hangup => libc::SIGHUP, Signal::Interrupt => libc::SIGINT, Signal::Quit => libc::SIGQUIT, Signal::Illegal => libc::SIGILL, Signal::Trap => libc::SIGTRAP, Signal::Abort => libc::SIGABRT, Signal::IOT => libc::SIGIOT, Signal::Bus => libc::SIGBUS, Signal::FloatingPointException => libc::SIGFPE, Signal::Kill => libc::SIGKILL, Signal::User1 => libc::SIGUSR1, Signal::Segv => libc::SIGSEGV, Signal::User2 => libc::SIGUSR2, Signal::Pipe => libc::SIGPIPE, Signal::Alarm => libc::SIGALRM, Signal::Term => libc::SIGTERM, Signal::Child => libc::SIGCHLD, Signal::Continue => libc::SIGCONT, Signal::Stop => libc::SIGSTOP, Signal::TSTP => libc::SIGTSTP, Signal::TTIN => libc::SIGTTIN, Signal::TTOU => libc::SIGTTOU, Signal::Urgent => libc::SIGURG, Signal::XCPU => libc::SIGXCPU, Signal::XFSZ => libc::SIGXFSZ, Signal::VirtualAlarm => libc::SIGVTALRM, Signal::Profiling => libc::SIGPROF, Signal::Winch => libc::SIGWINCH, Signal::IO => libc::SIGIO, Signal::Sys => libc::SIGSYS, _ => None, } #[doc = include_str!("../../md_doc/system.md")] pub struct System { process_list: HashMap, mem_total: u64, mem_free: u64, mem_used: u64, swap_total: u64, swap_used: u64, global_cpu: Cpu, cpus: Vec, components: Vec, disks: Vec, networks: Networks, users: Vec, boot_time: u64, system_info: SystemInfo, got_cpu_frequency: bool, } impl SystemExt for System { const IS_SUPPORTED: bool = true; const SUPPORTED_SIGNALS: &'static [Signal] = supported_signals(); const MINIMUM_CPU_UPDATE_INTERVAL: Duration = Duration::from_millis(100); fn new_with_specifics(refreshes: RefreshKind) -> System { let system_info = SystemInfo::new(); let mut s = System { process_list: HashMap::with_capacity(200), mem_total: 0, mem_free: 0, mem_used: 0, swap_total: 0, swap_used: 0, global_cpu: Cpu::new(String::new(), String::new(), 0), cpus: Vec::with_capacity(system_info.nb_cpus as _), components: Vec::with_capacity(2), disks: Vec::with_capacity(1), networks: Networks::new(), users: Vec::new(), boot_time: boot_time(), system_info, got_cpu_frequency: false, }; s.refresh_specifics(refreshes); s } fn refresh_memory(&mut self) { if self.mem_total == 0 { self.mem_total = self.system_info.get_total_memory(); } self.mem_used = self.system_info.get_used_memory(); self.mem_free = self.system_info.get_free_memory(); let (swap_used, swap_total) = self.system_info.get_swap_info(); self.swap_total = swap_total; self.swap_used = swap_used; } fn refresh_cpu_specifics(&mut self, refresh_kind: CpuRefreshKind) { if self.cpus.is_empty() { let mut frequency = 0; // We get the CPU vendor ID in here. let vendor_id = get_sys_value_str_by_name(b"hw.model\0").unwrap_or_else(|| "".to_owned()); for pos in 0..self.system_info.nb_cpus { if refresh_kind.frequency() { unsafe { frequency = get_frequency_for_cpu(pos); } } self.cpus .push(Cpu::new(format!("cpu {pos}"), vendor_id.clone(), frequency)); } self.global_cpu.vendor_id = vendor_id; self.got_cpu_frequency = refresh_kind.frequency(); } else if refresh_kind.frequency() && !self.got_cpu_frequency { for (pos, proc_) in self.cpus.iter_mut().enumerate() { unsafe { proc_.frequency = get_frequency_for_cpu(pos as _); } } self.got_cpu_frequency = true; } if refresh_kind.cpu_usage() { self.system_info .get_cpu_usage(&mut self.global_cpu, &mut self.cpus); } if refresh_kind.frequency() { self.global_cpu.frequency = self.cpus.get(0).map(|cpu| cpu.frequency).unwrap_or(0); } } fn refresh_components_list(&mut self) { if self.cpus.is_empty() { self.refresh_cpu(); } self.components = unsafe { super::component::get_components(self.cpus.len()) }; } fn refresh_processes_specifics(&mut self, refresh_kind: ProcessRefreshKind) { unsafe { self.refresh_procs(refresh_kind) } } fn refresh_process_specifics(&mut self, pid: Pid, refresh_kind: ProcessRefreshKind) -> bool { unsafe { let kd = self.system_info.kd.as_ptr(); let mut count = 0; let procs = libc::kvm_getprocs(kd, libc::KERN_PROC_PROC, 0, &mut count); if count < 1 { sysinfo_debug!("kvm_getprocs returned nothing..."); return false; } let now = super::utils::get_now(); let fscale = self.system_info.fscale; let page_size = self.system_info.page_size as isize; let proc_list = utils::WrapMap(UnsafeCell::new(&mut self.process_list)); let procs: &mut [utils::KInfoProc] = std::slice::from_raw_parts_mut(procs as _, count as _); #[cfg(feature = "multithread")] use rayon::iter::ParallelIterator; macro_rules! multi_iter { ($name:ident, $($iter:tt)+) => { $name = crate::utils::into_iter(procs).$($iter)+; } } let ret; #[cfg(not(feature = "multithread"))] multi_iter!(ret, find(|kproc| kproc.ki_pid == pid.0)); #[cfg(feature = "multithread")] multi_iter!(ret, find_any(|kproc| kproc.ki_pid == pid.0)); let kproc = if let Some(kproc) = ret { kproc } else { return false; }; match super::process::get_process_data( kproc, &proc_list, page_size, fscale, now, refresh_kind, ) { Ok(Some(proc_)) => { self.add_missing_proc_info(self.system_info.kd.as_ptr(), kproc, proc_); true } Ok(None) => true, Err(_) => false, } } } fn refresh_disks_list(&mut self) { self.disks = unsafe { super::disk::get_all_disks() }; } fn refresh_users_list(&mut self) { self.users = crate::users::get_users_list(); } // COMMON PART // // Need to be moved into a "common" file to avoid duplication. fn processes(&self) -> &HashMap { &self.process_list } fn process(&self, pid: Pid) -> Option<&Process> { self.process_list.get(&pid) } fn networks(&self) -> &Networks { &self.networks } fn networks_mut(&mut self) -> &mut Networks { &mut self.networks } fn global_cpu_info(&self) -> &Cpu { &self.global_cpu } fn cpus(&self) -> &[Cpu] { &self.cpus } fn physical_core_count(&self) -> Option { let mut physical_core_count: u32 = 0; unsafe { if get_sys_value_by_name(b"hw.ncpu\0", &mut physical_core_count) { Some(physical_core_count as _) } else { None } } } fn total_memory(&self) -> u64 { self.mem_total } fn free_memory(&self) -> u64 { self.mem_free } fn available_memory(&self) -> u64 { self.mem_free } fn used_memory(&self) -> u64 { self.mem_used } fn total_swap(&self) -> u64 { self.swap_total } fn free_swap(&self) -> u64 { self.swap_total - self.swap_used } // TODO: need to be checked fn used_swap(&self) -> u64 { self.swap_used } fn components(&self) -> &[Component] { &self.components } fn components_mut(&mut self) -> &mut [Component] { &mut self.components } fn disks(&self) -> &[Disk] { &self.disks } fn disks_mut(&mut self) -> &mut [Disk] { &mut self.disks } fn sort_disks_by(&mut self, compare: F) where F: FnMut(&Disk, &Disk) -> std::cmp::Ordering, { self.disks.sort_unstable_by(compare); } fn uptime(&self) -> u64 { unsafe { let csec = libc::time(::std::ptr::null_mut()); libc::difftime(csec, self.boot_time as _) as u64 } } fn boot_time(&self) -> u64 { self.boot_time } fn load_average(&self) -> LoadAvg { let mut loads = vec![0f64; 3]; unsafe { libc::getloadavg(loads.as_mut_ptr(), 3); LoadAvg { one: loads[0], five: loads[1], fifteen: loads[2], } } } fn users(&self) -> &[User] { &self.users } fn name(&self) -> Option { self.system_info.get_os_name() } fn long_os_version(&self) -> Option { self.system_info.get_os_release_long() } fn host_name(&self) -> Option { self.system_info.get_hostname() } fn kernel_version(&self) -> Option { self.system_info.get_kernel_version() } fn os_version(&self) -> Option { self.system_info.get_os_release() } fn distribution_id(&self) -> String { std::env::consts::OS.to_owned() } } impl Default for System { fn default() -> Self { Self::new() } } impl System { unsafe fn refresh_procs(&mut self, refresh_kind: ProcessRefreshKind) { let kd = self.system_info.kd.as_ptr(); let procs = { let mut count = 0; let procs = libc::kvm_getprocs(kd, libc::KERN_PROC_PROC, 0, &mut count); if count < 1 { sysinfo_debug!("kvm_getprocs returned nothing..."); return; } #[cfg(feature = "multithread")] use rayon::iter::{ParallelIterator, ParallelIterator as IterTrait}; #[cfg(not(feature = "multithread"))] use std::iter::Iterator as IterTrait; let fscale = self.system_info.fscale; let page_size = self.system_info.page_size as isize; let now = super::utils::get_now(); let proc_list = utils::WrapMap(UnsafeCell::new(&mut self.process_list)); let procs: &mut [utils::KInfoProc] = std::slice::from_raw_parts_mut(procs as _, count as _); IterTrait::filter_map(crate::utils::into_iter(procs), |kproc| { super::process::get_process_data( kproc, &proc_list, page_size, fscale, now, refresh_kind, ) .ok() .and_then(|p| p.map(|p| (kproc, p))) }) .collect::>() }; // We remove all processes that don't exist anymore. self.process_list .retain(|_, v| std::mem::replace(&mut v.updated, false)); for (kproc, proc_) in procs { self.add_missing_proc_info(kd, kproc, proc_); } } unsafe fn add_missing_proc_info( &mut self, kd: *mut libc::kvm_t, kproc: &libc::kinfo_proc, mut proc_: Process, ) { proc_.cmd = from_cstr_array(libc::kvm_getargv(kd, kproc, 0) as _); self.system_info.get_proc_missing_info(kproc, &mut proc_); if !proc_.cmd.is_empty() { // First, we try to retrieve the name from the command line. let p = Path::new(&proc_.cmd[0]); if let Some(name) = p.file_name().and_then(|s| s.to_str()) { proc_.name = name.to_owned(); } if proc_.root.as_os_str().is_empty() { if let Some(parent) = p.parent() { proc_.root = parent.to_path_buf(); } } } if proc_.name.is_empty() { // The name can be cut short because the `ki_comm` field size is limited, // which is why we prefer to get the name from the command line as much as // possible. proc_.name = c_buf_to_string(&kproc.ki_comm).unwrap_or_default(); } proc_.environ = from_cstr_array(libc::kvm_getenvv(kd, kproc, 0) as _); self.process_list.insert(proc_.pid, proc_); } } #[derive(Debug)] struct Zfs { enabled: bool, mib_arcstats_size: [c_int; 5], } impl Zfs { fn new() -> Self { let mut zfs = Self { enabled: false, mib_arcstats_size: Default::default(), }; unsafe { init_mib( b"kstat.zfs.misc.arcstats.size\0", &mut zfs.mib_arcstats_size, ); let mut arc_size: u64 = 0; if get_sys_value(&zfs.mib_arcstats_size, &mut arc_size) { zfs.enabled = arc_size != 0; } } zfs } fn arc_size(&self) -> Option { if self.enabled { let mut arc_size: u64 = 0; unsafe { get_sys_value(&self.mib_arcstats_size, &mut arc_size); Some(arc_size) } } else { None } } } /// This struct is used to get system information more easily. #[derive(Debug)] struct SystemInfo { hw_physical_memory: [c_int; 2], page_size: c_int, virtual_page_count: [c_int; 4], virtual_wire_count: [c_int; 4], virtual_active_count: [c_int; 4], virtual_cache_count: [c_int; 4], virtual_inactive_count: [c_int; 4], virtual_free_count: [c_int; 4], os_type: [c_int; 2], os_release: [c_int; 2], kern_version: [c_int; 2], hostname: [c_int; 2], buf_space: [c_int; 2], nb_cpus: c_int, kd: NonNull, // For these two fields, we could use `kvm_getcptime` but the function isn't very efficient... mib_cp_time: [c_int; 2], mib_cp_times: [c_int; 2], // For the global CPU usage. cp_time: utils::VecSwitcher, // For each CPU usage. cp_times: utils::VecSwitcher, /// From FreeBSD manual: "The kernel fixed-point scale factor". It's used when computing /// processes' CPU usage. fscale: f32, procstat: *mut libc::procstat, zfs: Zfs, } // This is needed because `kd: *mut libc::kvm_t` isn't thread-safe. unsafe impl Send for SystemInfo {} unsafe impl Sync for SystemInfo {} impl SystemInfo { fn new() -> Self { unsafe { let mut errbuf = MaybeUninit::<[libc::c_char; libc::_POSIX2_LINE_MAX as usize]>::uninit(); let kd = NonNull::new(libc::kvm_openfiles( std::ptr::null(), b"/dev/null\0".as_ptr() as *const _, std::ptr::null(), 0, errbuf.as_mut_ptr() as *mut _, )) .expect("kvm_openfiles failed"); let mut smp: c_int = 0; let mut nb_cpus: c_int = 1; if !get_sys_value_by_name(b"kern.smp.active\0", &mut smp) { smp = 0; } #[allow(clippy::collapsible_if)] // I keep as is for readability reasons. if smp != 0 { if !get_sys_value_by_name(b"kern.smp.cpus\0", &mut nb_cpus) || nb_cpus < 1 { nb_cpus = 1; } } let mut si = SystemInfo { hw_physical_memory: Default::default(), page_size: 0, virtual_page_count: Default::default(), virtual_wire_count: Default::default(), virtual_active_count: Default::default(), virtual_cache_count: Default::default(), virtual_inactive_count: Default::default(), virtual_free_count: Default::default(), buf_space: Default::default(), os_type: Default::default(), os_release: Default::default(), kern_version: Default::default(), hostname: Default::default(), nb_cpus, kd, mib_cp_time: Default::default(), mib_cp_times: Default::default(), cp_time: utils::VecSwitcher::new(vec![0; libc::CPUSTATES as usize]), cp_times: utils::VecSwitcher::new(vec![ 0; nb_cpus as usize * libc::CPUSTATES as usize ]), fscale: 0., procstat: std::ptr::null_mut(), zfs: Zfs::new(), }; let mut fscale: c_int = 0; if !get_sys_value_by_name(b"kern.fscale\0", &mut fscale) { // Default value used in htop. fscale = 2048; } si.fscale = fscale as f32; if !get_sys_value_by_name(b"vm.stats.vm.v_page_size\0", &mut si.page_size) { panic!("cannot get page size..."); } init_mib(b"hw.physmem\0", &mut si.hw_physical_memory); init_mib(b"vm.stats.vm.v_page_count\0", &mut si.virtual_page_count); init_mib(b"vm.stats.vm.v_wire_count\0", &mut si.virtual_wire_count); init_mib( b"vm.stats.vm.v_active_count\0", &mut si.virtual_active_count, ); init_mib(b"vm.stats.vm.v_cache_count\0", &mut si.virtual_cache_count); init_mib( b"vm.stats.vm.v_inactive_count\0", &mut si.virtual_inactive_count, ); init_mib(b"vm.stats.vm.v_free_count\0", &mut si.virtual_free_count); init_mib(b"vfs.bufspace\0", &mut si.buf_space); init_mib(b"kern.ostype\0", &mut si.os_type); init_mib(b"kern.osrelease\0", &mut si.os_release); init_mib(b"kern.version\0", &mut si.kern_version); init_mib(b"kern.hostname\0", &mut si.hostname); init_mib(b"kern.cp_time\0", &mut si.mib_cp_time); init_mib(b"kern.cp_times\0", &mut si.mib_cp_times); si } } fn get_os_name(&self) -> Option { get_system_info(&[self.os_type[0], self.os_type[1]], Some("FreeBSD")) } fn get_kernel_version(&self) -> Option { get_system_info(&[self.kern_version[0], self.kern_version[1]], None) } fn get_os_release_long(&self) -> Option { get_system_info(&[self.os_release[0], self.os_release[1]], None) } fn get_os_release(&self) -> Option { // It returns something like "13.0-RELEASE". We want to keep everything until the "-". get_system_info(&[self.os_release[0], self.os_release[1]], None) .and_then(|s| s.split('-').next().map(|s| s.to_owned())) } fn get_hostname(&self) -> Option { get_system_info(&[self.hostname[0], self.hostname[1]], Some("")) } /// Returns (used, total). fn get_swap_info(&self) -> (u64, u64) { // Magic number used in htop. Cannot find how they got it when reading `kvm_getswapinfo` // source code so here we go... const LEN: usize = 16; let mut swap = MaybeUninit::<[libc::kvm_swap; LEN]>::uninit(); unsafe { let nswap = libc::kvm_getswapinfo(self.kd.as_ptr(), swap.as_mut_ptr() as *mut _, LEN as _, 0) as usize; if nswap < 1 { return (0, 0); } let swap = std::slice::from_raw_parts(swap.as_ptr() as *mut libc::kvm_swap, nswap.min(LEN)); let (used, total) = swap.iter().fold((0, 0), |(used, total): (u64, u64), swap| { ( used.saturating_add(swap.ksw_used as _), total.saturating_add(swap.ksw_total as _), ) }); ( used.saturating_mul(self.page_size as _), total.saturating_mul(self.page_size as _), ) } } fn get_total_memory(&self) -> u64 { let mut nb_pages: u64 = 0; unsafe { if get_sys_value(&self.virtual_page_count, &mut nb_pages) { return nb_pages.saturating_mul(self.page_size as _); } // This is a fallback. It includes all the available memory, not just the one available for // the users. let mut total_memory: u64 = 0; get_sys_value(&self.hw_physical_memory, &mut total_memory); total_memory } } fn get_used_memory(&self) -> u64 { let mut mem_active: u64 = 0; let mut mem_wire: u64 = 0; unsafe { get_sys_value(&self.virtual_active_count, &mut mem_active); get_sys_value(&self.virtual_wire_count, &mut mem_wire); let mut mem_wire = mem_wire.saturating_mul(self.page_size as _); // We need to subtract "ZFS ARC" from the "wired memory" because it should belongs to cache // but the kernel reports it as "wired memory" instead... if let Some(arc_size) = self.zfs.arc_size() { mem_wire -= arc_size; } mem_active .saturating_mul(self.page_size as _) .saturating_add(mem_wire) } } fn get_free_memory(&self) -> u64 { let mut buffers_mem: u64 = 0; let mut inactive_mem: u64 = 0; let mut cached_mem: u64 = 0; let mut free_mem: u64 = 0; unsafe { get_sys_value(&self.buf_space, &mut buffers_mem); get_sys_value(&self.virtual_inactive_count, &mut inactive_mem); get_sys_value(&self.virtual_cache_count, &mut cached_mem); get_sys_value(&self.virtual_free_count, &mut free_mem); // For whatever reason, buffers_mem is already the right value... buffers_mem .saturating_add(inactive_mem.saturating_mul(self.page_size as _)) .saturating_add(cached_mem.saturating_mul(self.page_size as _)) .saturating_add(free_mem.saturating_mul(self.page_size as _)) } } fn get_cpu_usage(&mut self, global: &mut Cpu, cpus: &mut [Cpu]) { unsafe { get_sys_value_array(&self.mib_cp_time, self.cp_time.get_mut()); get_sys_value_array(&self.mib_cp_times, self.cp_times.get_mut()); } fn fill_cpu(proc_: &mut Cpu, new_cp_time: &[libc::c_ulong], old_cp_time: &[libc::c_ulong]) { let mut total_new: u64 = 0; let mut total_old: u64 = 0; let mut cp_diff: libc::c_ulong = 0; for i in 0..(libc::CPUSTATES as usize) { // We obviously don't want to get the idle part of the CPU usage, otherwise // we would always be at 100%... if i != libc::CP_IDLE as usize { cp_diff += new_cp_time[i] - old_cp_time[i]; } let mut tmp: u64 = new_cp_time[i] as _; total_new += tmp; tmp = old_cp_time[i] as _; total_old += tmp; } let total_diff = total_new - total_old; if total_diff < 1 { proc_.cpu_usage = 0.; } else { proc_.cpu_usage = cp_diff as f32 / total_diff as f32 * 100.; } } fill_cpu(global, self.cp_time.get_new(), self.cp_time.get_old()); let old_cp_times = self.cp_times.get_old(); let new_cp_times = self.cp_times.get_new(); for (pos, proc_) in cpus.iter_mut().enumerate() { let index = pos * libc::CPUSTATES as usize; fill_cpu(proc_, &new_cp_times[index..], &old_cp_times[index..]); } } #[allow(clippy::collapsible_if)] // I keep as is for readability reasons. unsafe fn get_proc_missing_info(&mut self, kproc: &libc::kinfo_proc, proc_: &mut Process) { if self.procstat.is_null() { self.procstat = libc::procstat_open_sysctl(); } if self.procstat.is_null() { return; } let head = libc::procstat_getfiles(self.procstat, kproc as *const _ as usize as *mut _, 0); if head.is_null() { return; } let mut entry = (*head).stqh_first; let mut done = 0; while !entry.is_null() && done < 2 { { let tmp = &*entry; if tmp.fs_uflags & libc::PS_FST_UFLAG_CDIR != 0 { if !tmp.fs_path.is_null() { if let Ok(p) = CStr::from_ptr(tmp.fs_path).to_str() { proc_.cwd = PathBuf::from(p); done += 1; } } } else if tmp.fs_uflags & libc::PS_FST_UFLAG_RDIR != 0 { if !tmp.fs_path.is_null() { if let Ok(p) = CStr::from_ptr(tmp.fs_path).to_str() { proc_.root = PathBuf::from(p); done += 1; } } } } entry = (*entry).next.stqe_next; } libc::procstat_freefiles(self.procstat, head); } } impl Drop for SystemInfo { fn drop(&mut self) { unsafe { libc::kvm_close(self.kd.as_ptr()); if !self.procstat.is_null() { libc::procstat_close(self.procstat); } } } }