diff --git a/Cargo.lock b/Cargo.lock index b6eafa58..b1886550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "windows 0.62.0", + "winprocinfo", ] [[package]] @@ -2840,6 +2841,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "winprocinfo" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d010942d0ed74baf25362e267b54d4f57b859d0cb9b2dc7040e26333a87e1d8" +dependencies = [ + "ntapi", + "regex", + "winapi", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 64a7c90e..a6d75af5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ windows = { version = "0.62.0", features = [ "Win32_System_ProcessStatus", "Win32_System_Threading", ] } +winprocinfo = "0.1.2" [target.'cfg(target_os = "freebsd")'.dependencies] serde_json = { version = "1.0.145" } diff --git a/src/collection/processes.rs b/src/collection/processes.rs index 8a15073a..86a80314 100644 --- a/src/collection/processes.rs +++ b/src/collection/processes.rs @@ -147,6 +147,13 @@ pub struct ProcessHarvest { /// The process entry "type". #[cfg(target_os = "linux")] pub process_type: ProcessType, + + /// The nice value (user-settable scheduling hint). + #[cfg(unix)] + pub nice: i32, + + /// The kernel scheduling priority. + pub priority: i32, // TODO: Additional fields // pub rss_kb: u64, // pub virt_kb: u64, diff --git a/src/collection/processes/linux/mod.rs b/src/collection/processes/linux/mod.rs index bce79662..41847837 100644 --- a/src/collection/processes/linux/mod.rs +++ b/src/collection/processes/linux/mod.rs @@ -285,6 +285,9 @@ fn read_proc( #[cfg(feature = "gpu")] gpu_util: 0, process_type, + #[cfg(unix)] + nice: stat.nice, + priority: stat.priority, }, new_process_times, )) diff --git a/src/collection/processes/linux/process.rs b/src/collection/processes/linux/process.rs index 39658dd7..ff3a8e33 100644 --- a/src/collection/processes/linux/process.rs +++ b/src/collection/processes/linux/process.rs @@ -62,6 +62,13 @@ pub(crate) struct Stat { /// Kernel thread pub is_kernel_thread: bool, + + /// The kernel scheduling priority. + pub priority: i32, + + /// The nice value (user-settable scheduling hint). + #[cfg(unix)] + pub nice: i32, } impl Stat { @@ -107,11 +114,20 @@ impl Stat { let utime: u64 = next_part(&mut rest)?.parse()?; let stime: u64 = next_part(&mut rest)?.parse()?; - // Skip 6 fields until starttime (cutime, cstime, priority, nice, num_threads, - // itrealvalue). - let mut rest = rest.skip(6); - let start_time: u64 = next_part(&mut rest)?.parse()?; + // cutime + let _ = next_part(&mut rest)?; + // cstime + let _ = next_part(&mut rest)?; + // priority + let priority: i32 = next_part(&mut rest)?.parse()?; + // nice + let nice: i32 = next_part(&mut rest)?.parse()?; + // num_threads + let _ = next_part(&mut rest)?; + // itrealvalue + let _ = next_part(&mut rest)?; + let start_time: u64 = next_part(&mut rest)?.parse()?; let vsize: u64 = next_part(&mut rest)?.parse()?; let rss: u64 = next_part(&mut rest)?.parse()?; @@ -125,6 +141,8 @@ impl Stat { vsize, start_time, is_kernel_thread, + priority, + nice, }) } diff --git a/src/collection/processes/macos.rs b/src/collection/processes/macos.rs index e70edf4f..5a52ad0f 100644 --- a/src/collection/processes/macos.rs +++ b/src/collection/processes/macos.rs @@ -1,6 +1,6 @@ //! Process data collection for macOS. Uses sysinfo and custom bindings. -mod sysctl_bindings; +pub mod sysctl_bindings; use std::{io, process::Command}; diff --git a/src/collection/processes/unix/process_ext.rs b/src/collection/processes/unix/process_ext.rs index 3f3e327a..a3510ad1 100644 --- a/src/collection/processes/unix/process_ext.rs +++ b/src/collection/processes/unix/process_ext.rs @@ -1,5 +1,9 @@ //! Shared process data harvesting code from macOS and FreeBSD via sysinfo. +#[cfg(target_os = "macos")] +use crate::collection::processes::macos::sysctl_bindings; + +use cfg_if::cfg_if; use std::{io, time::Duration}; use itertools::Itertools; @@ -9,6 +13,58 @@ use sysinfo::{ProcessStatus, System}; use super::{ProcessHarvest, process_status_str}; use crate::collection::{Pid, error::CollectionResult, processes::UserTable}; +fn get_nice(pid: Pid) -> i32 { + // SAFETY: getpriority takes no user pointers; pid is passed as a value + // and errors are reported via the return value. + cfg_if! { + if #[cfg(target_os = "freebsd")] { + unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) } + } else if #[cfg(target_os = "macos")] { + unsafe { libc::getpriority(libc::PRIO_PROCESS, pid as u32) } + } else { + 0 + } + } +} + +fn get_priority(pid: Pid) -> i32 { + cfg_if! { + if #[cfg(target_os = "macos")] { + if let Ok(kinfo) = sysctl_bindings::kinfo_process(pid) { + kinfo.kp_proc.p_priority as i32 + } else { + 0 + } + } else if #[cfg(target_os = "freebsd")] { + use libc::{c_int, c_void}; + use std::{mem, ptr}; + + let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid as c_int]; + let mut kp: libc::kinfo_proc = unsafe { mem::zeroed() }; + let mut size = mem::size_of::(); + + // SAFETY: sysctl takes the following pointer arguments + // - mib is valid for KERN_PROC_PID. + // - kp is a properly sized output buffer. + // - newp is null for a read-only sysctl. + let ret = unsafe { + libc::sysctl( + mib.as_ptr(), + mib.len() as u32, + &mut kp as *mut _ as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + }; + + if ret == 0 { kp.ki_pri.pri_level as i32 } else { 0 } + } else { + 0 + } + } +} + pub(crate) trait UnixProcessExt { fn sysinfo_process_data( sys: &System, use_current_cpu_total: bool, unnormalized_cpu: bool, total_memory: u64, @@ -69,6 +125,9 @@ pub(crate) trait UnixProcessExt { }; let uid = process_val.user_id().map(|u| **u); let pid = process_val.pid().as_u32() as Pid; + let nice = get_nice(pid); + let priority = get_priority(pid); + process_vector.push(ProcessHarvest { pid, parent_pid: Self::parent_pid(process_val), @@ -90,11 +149,6 @@ pub(crate) trait UnixProcessExt { uid, user: uid.and_then(|uid| user_table.uid_to_username(uid).ok()), time: if process_val.start_time() == 0 { - // Workaround for sysinfo occasionally returning a start time equal to UNIX - // epoch, giving a run time in the range of 50+ years. We just - // return a time of zero in this case for simplicity. - // - // TODO: Maybe return an option instead? Duration::ZERO } else { Duration::from_secs(process_val.run_time()) @@ -105,6 +159,9 @@ pub(crate) trait UnixProcessExt { gpu_mem_percent: 0.0, #[cfg(feature = "gpu")] gpu_util: 0, + #[cfg(unix)] + nice, + priority, }); } diff --git a/src/collection/processes/windows.rs b/src/collection/processes/windows.rs index 1aabede9..a3d71d92 100644 --- a/src/collection/processes/windows.rs +++ b/src/collection/processes/windows.rs @@ -1,9 +1,9 @@ -//! Process data collection for Windows. Uses sysinfo. - -use std::time::Duration; +//! Process data collection for Windows. Uses sysinfo and winprocinfo. use super::{ProcessHarvest, process_status_str}; use crate::collection::{DataCollector, error::CollectionResult}; +use std::time::Duration; +use winprocinfo; use itertools::Itertools; @@ -90,6 +90,13 @@ pub fn sysinfo_process_data( } (gpu_mem, gpu_util, gpu_mem_percent) }; + + let base_priority = winprocinfo::get_proc_info_by_pid(process_val.pid().as_u32()) + .ok() + .flatten() + .map(|proc| proc.base_priority) + .unwrap_or(0); + process_vector.push(ProcessHarvest { pid: process_val.pid().as_u32() as _, parent_pid: process_val.parent().map(|p| p.as_u32() as _), @@ -127,6 +134,7 @@ pub fn sysinfo_process_data( gpu_util, #[cfg(feature = "gpu")] gpu_mem_percent, + priority: base_priority, }); } diff --git a/src/utils/logging.rs b/src/utils/logging.rs index 1656afcb..d5088581 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -146,17 +146,14 @@ mod test { /// This doesn't do anything if you use something like nextest, which runs /// a test-per-process, but that's fine. fn init_test_logger() { - use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Once; - static LOG_INIT: AtomicBool = AtomicBool::new(false); + static INIT: Once = Once::new(); - if LOG_INIT.load(Ordering::SeqCst) { - return; - } - - LOG_INIT.store(true, Ordering::SeqCst); - super::init_logger(log::LevelFilter::Trace, None) - .expect("initializing the logger should succeed"); + INIT.call_once(|| { + super::init_logger(log::LevelFilter::Trace, None) + .expect("initializing the logger should succeed"); + }); } #[cfg(feature = "logging")] diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 8ca25a39..b1c5a896 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -165,6 +165,9 @@ fn make_column(column: ProcColumn) -> SortColumn { User => SortColumn::soft(User, Some(0.05)), State => SortColumn::hard(State, 9), Time => SortColumn::new(Time), + Priority => SortColumn::new(Priority).default_descending(), + #[cfg(unix)] + Nice => SortColumn::new(Nice), #[cfg(feature = "gpu")] GpuMemValue => SortColumn::new(GpuMemValue).default_descending(), #[cfg(feature = "gpu")] @@ -198,6 +201,9 @@ pub enum ProcWidgetColumn { User, State, Time, + Priority, + #[cfg(unix)] + Nice, #[cfg(feature = "gpu")] GpuMem, #[cfg(feature = "gpu")] @@ -340,6 +346,9 @@ impl ProcWidgetState { ProcWidgetColumn::User => User, ProcWidgetColumn::State => State, ProcWidgetColumn::Time => Time, + ProcWidgetColumn::Priority => Priority, + #[cfg(unix)] + ProcWidgetColumn::Nice => Nice, #[cfg(feature = "gpu")] ProcWidgetColumn::GpuMem => { if mem_as_values { @@ -368,6 +377,9 @@ impl ProcWidgetState { User, State, Time, + Priority, + #[cfg(unix)] + Nice, ]; default_columns.into_iter().map(make_column).collect() @@ -393,6 +405,9 @@ impl ProcWidgetState { State => ProcWidgetColumn::State, User => ProcWidgetColumn::User, Time => ProcWidgetColumn::Time, + Priority => ProcWidgetColumn::Priority, + #[cfg(unix)] + Nice => ProcWidgetColumn::Nice, #[cfg(feature = "gpu")] GpuMemValue | GpuMemPercent => ProcWidgetColumn::GpuMem, #[cfg(feature = "gpu")] @@ -1205,6 +1220,9 @@ mod test { gpu_usage: 0, #[cfg(target_os = "linux")] process_type: crate::collection::processes::ProcessType::Regular, + #[cfg(unix)] + nice: 0, + priority: -20, }; let b = ProcWidgetData { diff --git a/src/widgets/process_table/process_columns.rs b/src/widgets/process_table/process_columns.rs index 97691e41..2bd0c868 100644 --- a/src/widgets/process_table/process_columns.rs +++ b/src/widgets/process_table/process_columns.rs @@ -30,6 +30,9 @@ pub enum ProcColumn { State, User, Time, + #[cfg(unix)] + Nice, + Priority, #[cfg(feature = "gpu")] GpuMemValue, #[cfg(feature = "gpu")] @@ -63,6 +66,9 @@ impl ProcColumn { ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => &["GMem", "GMem%"], #[cfg(feature = "gpu")] ProcColumn::GpuUtilPercent => &["GPU%"], + #[cfg(unix)] + ProcColumn::Nice => &["Nice"], + ProcColumn::Priority => &["Priority"], } } } @@ -85,6 +91,9 @@ impl ColumnHeader for ProcColumn { ProcColumn::State => "State", ProcColumn::User => "User", ProcColumn::Time => "Time", + #[cfg(unix)] + ProcColumn::Nice => "Nice", + ProcColumn::Priority => "Priority", #[cfg(feature = "gpu")] ProcColumn::GpuMemValue => "GMem", #[cfg(feature = "gpu")] @@ -103,6 +112,9 @@ impl ColumnHeader for ProcColumn { ProcColumn::Pid => "PID(p)".into(), ProcColumn::Name => "Name(n)".into(), ProcColumn::Command => "Command(n)".into(), + #[cfg(unix)] + ProcColumn::Nice => "Nice".into(), + ProcColumn::Priority => "Priority".into(), _ => self.text(), } } @@ -167,6 +179,13 @@ impl SortsRow for ProcColumn { ProcColumn::Time => { data.sort_by(|a, b| sort_partial_fn(descending)(a.time, b.time)); } + ProcColumn::Priority => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.priority, b.priority)); + } + #[cfg(unix)] + ProcColumn::Nice => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.nice, b.nice)); + } #[cfg(feature = "gpu")] ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => { data.sort_by(|a, b| { @@ -230,6 +249,9 @@ impl From<&ProcColumn> for ProcWidgetColumn { ProcColumn::State => ProcWidgetColumn::State, ProcColumn::User => ProcWidgetColumn::User, ProcColumn::Time => ProcWidgetColumn::Time, + ProcColumn::Priority => ProcWidgetColumn::Priority, + #[cfg(unix)] + ProcColumn::Nice => ProcWidgetColumn::Nice, #[cfg(feature = "gpu")] ProcColumn::GpuMemPercent | ProcColumn::GpuMemValue => ProcWidgetColumn::GpuMem, #[cfg(feature = "gpu")] diff --git a/src/widgets/process_table/process_data.rs b/src/widgets/process_table/process_data.rs index f1e27585..0ed6ab92 100644 --- a/src/widgets/process_table/process_data.rs +++ b/src/widgets/process_table/process_data.rs @@ -218,6 +218,9 @@ pub struct ProcWidgetData { /// The process "type". Used to color things. #[cfg(target_os = "linux")] pub process_type: crate::collection::processes::ProcessType, + #[cfg(unix)] + pub nice: i32, + pub priority: i32, } impl ProcWidgetData { @@ -264,6 +267,9 @@ impl ProcWidgetData { gpu_usage: process.gpu_util, #[cfg(target_os = "linux")] process_type: process.process_type, + #[cfg(unix)] + nice: process.nice, + priority: process.priority, } } @@ -308,6 +314,9 @@ impl ProcWidgetData { fn to_string(&self, column: &ProcColumn) -> String { match column { + &ProcColumn::Priority => self.priority.to_string(), + #[cfg(unix)] + ProcColumn::Nice => self.nice.to_string(), ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent), ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string(), ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem), @@ -337,12 +346,13 @@ impl DataToCell for ProcWidgetData { fn to_cell_text( &self, column: &ProcColumn, calculated_width: NonZeroU16, ) -> Option> { - let calculated_width = calculated_width.get(); - // TODO: Optimize the string allocations here... // TODO: Also maybe just pull in the to_string call but add a variable for the // differences. Some(match column { + #[cfg(unix)] + ProcColumn::Nice => self.nice.to_string().into(), + &ProcColumn::Priority => self.priority.to_string().into(), ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(), ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string().into(), ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem).into(), @@ -354,7 +364,7 @@ impl DataToCell for ProcWidgetData { ProcColumn::TotalRead => dec_bytes_string(self.total_read).into(), ProcColumn::TotalWrite => dec_bytes_string(self.total_write).into(), ProcColumn::State => { - if calculated_width < 8 { + if calculated_width.get() < 8 { self.process_char.to_string().into() } else { self.process_state.into() diff --git a/src/widgets/process_table/query.rs b/src/widgets/process_table/query.rs index 3f408af0..a26f9d95 100644 --- a/src/widgets/process_table/query.rs +++ b/src/widgets/process_table/query.rs @@ -670,6 +670,9 @@ enum PrefixType { State, User, Time, + #[cfg(unix)] + Nice, + Priority, #[cfg(feature = "gpu")] PGpu, #[cfg(feature = "gpu")] @@ -711,6 +714,13 @@ impl std::str::FromStr for PrefixType { result = User; } else if multi_eq_ignore_ascii_case!(s, "time") { result = Time; + } else if multi_eq_ignore_ascii_case!(s, "nice") { + #[cfg(unix)] + { + result = Nice; + } + } else if multi_eq_ignore_ascii_case!(s, "priority") { + result = Priority; } #[cfg(feature = "gpu")] { @@ -880,6 +890,17 @@ impl Prefix { process.gpu_mem_percent, numerical_query.value, ), + #[cfg(unix)] + PrefixType::Nice => matches_condition( + &numerical_query.condition, + process.nice, + numerical_query.value, + ), + PrefixType::Priority => matches_condition( + &numerical_query.condition, + process.priority, + numerical_query.value, + ), _ => true, }, ComparableQuery::Time(time_query) => match prefix_type {