//! Process collection and management (port of `glances.processes`). //! //! The process list is shared between the processlist, processcount, //! programlist, amps plugins and the event/action machinery, exactly like the //! `glances_processes` global in the Python implementation. use crate::filter::{Filter, FilterList}; use serde_json::{Map, Value, json}; use std::collections::HashMap; use std::sync::Mutex; use std::sync::OnceLock; use sysinfo::{ProcessesToUpdate, System, Users}; /// Mandatory process stats which can not be disabled by the user. pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"]; /// Available process sort keys. pub const SORT_PROCESSES_STATS: [&str; 7] = ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"]; /// Sort keys as displayed to humans. pub fn sort_for_human(key: &str) -> &str { match key { "io_counters" => "disk IO", "cpu_percent" => "CPU consumption", "memory_percent" => "memory consumption", "cpu_times" => "process time", "username" => "user name", "name" => "processs name", "cpu_num" => "CPU core number", "auto" => "auto", _ => "None", } } #[derive(Debug, Clone, Default)] pub struct ProcessCount { pub total: i64, pub running: i64, pub sleeping: i64, pub thread: i64, pub pid_max: Option, } #[derive(Debug)] pub struct Processes { system: System, users: Users, users_timer: f64, pub processlist: Vec, pub processlist_export: Vec, pub processcount: ProcessCount, pub sort_key: String, pub auto_sort: bool, pub sort_reverse: bool, pub disable_tag: bool, pub disable_extended_tag: bool, pub extended_process_pid: Option, pub extended_stats: Option, pub no_kernel_threads: bool, pub disable_io_counters: bool, pub disable_gids: bool, pub disable_cpu_num: bool, pub disable_stats: Vec, /// List of processes to focus on. pub filter_focus: FilterList, /// Process filter (regular expression). pub filter: Filter, /// List of processes stats to export. pub filter_export: FilterList, pub max_values: HashMap, io_old: HashMap, programs: Vec, programs_valid: bool, } impl Default for Processes { fn default() -> Self { Self::new() } } impl Processes { pub fn new() -> Self { let system = System::new(); let users = Users::new_with_refreshed_list(); let mut p = Self { system, users, users_timer: 0.0, processlist: Vec::new(), processlist_export: Vec::new(), processcount: ProcessCount::default(), sort_key: "cpu_percent".to_string(), auto_sort: true, sort_reverse: true, disable_tag: false, disable_extended_tag: false, extended_process_pid: None, extended_stats: None, no_kernel_threads: false, disable_io_counters: false, disable_gids: false, disable_cpu_num: false, disable_stats: Vec::new(), filter_focus: FilterList::default(), filter: Filter::default(), filter_export: FilterList::default(), max_values: HashMap::new(), io_old: HashMap::new(), programs: Vec::new(), programs_valid: false, }; p.reset_max_values(); p } pub fn reset_max_values(&mut self) { self.max_values.clear(); self.max_values.insert("cpu_percent".to_string(), 0.0); self.max_values.insert("memory_percent".to_string(), 0.0); } pub fn set_sort_key(&mut self, key: &str, auto: bool) { if key == "auto" { self.auto_sort = true; self.sort_key = "cpu_percent".to_string(); } else { self.auto_sort = auto; self.sort_key = key.to_string(); } self.sort_reverse = !matches!(self.sort_key.as_str(), "name" | "username" | "cpu_num"); } pub fn enable(&mut self) { self.disable_tag = false; } pub fn disable(&mut self) { self.disable_tag = true; } pub fn enable_extended(&mut self) { self.disable_extended_tag = false; } pub fn disable_extended(&mut self) { self.disable_extended_tag = true; } pub fn pid_max(&self) -> Option { std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::().ok()) } pub fn processes_count(&self, max_processes: Option) -> i64 { match max_processes { Some(max) => (max - 2).min(self.processcount.total - 1), None => 0, } } /// Update the processes stats. pub fn update(&mut self, cursor_position: i64, args: &crate::args::Args) -> &[Value] { if self.disable_tag { return &[]; } self.no_kernel_threads = args.no_kernel_threads; let time_since_update = crate::timer::get_time_since_last_update("process_disk"); // Refresh the user list at most every 60 seconds. if self.users_timer <= 0.0 { self.users = Users::new_with_refreshed_list(); self.users_timer = 60.0; } self.users_timer -= 1.0; self.system.refresh_memory(); self.system.refresh_processes(ProcessesToUpdate::All, true); let total_mem = self.system.total_memory().max(1) as f64; let mut processlist: Vec = Vec::with_capacity(self.system.processes().len()); for (pid, process) in self.system.processes() { // Skip threads: the process list shows processes only. if process.thread_kind().is_some() { continue; } let pid = pid.as_u32() as i64; if self.no_kernel_threads && process.parent().map(|p| p.as_u32()) == Some(2) { continue; } let stat = read_proc_stat(pid); let name = process.name().to_string_lossy().to_string(); let cmdline: Vec = process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect(); let username = process .user_id() .and_then(|uid| self.users.get_user_by_id(uid)) .map(|u| u.name().to_string()) .unwrap_or_default(); let status = stat.as_ref().map(|s| s.state.clone()).unwrap_or_else(|| "?".to_string()); let nice = stat.as_ref().map(|s| s.nice).unwrap_or(0); let num_threads = stat.as_ref().map(|s| s.num_threads).unwrap_or(0); let cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0); let (user_time, system_time) = stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0)); let rss = process.memory() as f64; let vms = process.virtual_memory() as f64; let memory_percent = rss * 100.0 / total_mem; let mut proc = Map::new(); proc.insert("pid".to_string(), json!(pid)); proc.insert("name".to_string(), json!(name)); proc.insert("cmdline".to_string(), json!(cmdline)); proc.insert("username".to_string(), json!(username)); proc.insert("num_threads".to_string(), json!(num_threads)); proc.insert("cpu_percent".to_string(), json!(process.cpu_usage() as f64)); proc.insert("memory_percent".to_string(), json!(memory_percent)); proc.insert("memory_info".to_string(), json!({"rss": rss, "vms": vms})); proc.insert("status".to_string(), json!(status)); proc.insert("nice".to_string(), json!(nice)); proc.insert( "cpu_times".to_string(), json!({"user": user_time, "system": system_time, "iowait": 0.0}), ); proc.insert("cpu_num".to_string(), json!(cpu_num)); // IO counters: [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] let (read_bytes, write_bytes) = match &stat { Some(stat) => (stat.read_bytes, stat.write_bytes), None => { let disk = process.disk_usage(); (disk.total_read_bytes, disk.total_written_bytes) } }; let (io_tag, io_old) = match self.io_old.get(&pid) { Some(old) => (1, *old), None => (0, [0, 0]), }; self.io_old.insert(pid, [read_bytes, write_bytes]); proc.insert( "io_counters".to_string(), json!([read_bytes, write_bytes, io_old[0], io_old[1], io_tag]), ); // GIDs if let Some(stat) = &stat { proc.insert( "gids".to_string(), json!({"real": stat.gid_real, "effective": stat.gid_effective, "saved": stat.gid_saved}), ); } // Metadata proc.insert("key".to_string(), json!("pid")); proc.insert("time_since_update".to_string(), json!(time_since_update)); processlist.push(Value::Object(proc)); } // Sort processes by the current sort key. sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse); self.update_processcount(&processlist); // Extended stats for the selected process. for (position, proc) in processlist.iter_mut().enumerate() { let pid = proc.get("pid").and_then(Value::as_i64).unwrap_or(0); if args.enable_process_extended && !args.programs && !self.disable_extended_tag && position as i64 == cursor_position && !args.disable_cursor { self.extended_process_pid = Some(pid); } if let Some(target) = self.extended_process_pid { if target == pid { let extended = self.set_extended_stats(pid, proc); if let Value::Object(map) = extended { if let Some(obj) = proc.as_object_mut() { for (k, v) in map { obj.insert(k, v); } } } } } } // Remove non running processes from the IO cache. let running: std::collections::HashSet = processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect(); self.io_old.retain(|pid, _| running.contains(pid)); // Export list. self.processlist_export = self.update_export_list(&processlist); // Filtered process list. let filtered = self.update_list(&processlist); self.programs_valid = false; // Compute the maximum values for CPU and MEM. for key in ["cpu_percent", "memory_percent"] { if self.disable_stats.iter().any(|s| s == key) { continue; } let max = filtered .iter() .filter_map(|p| p.get(key).and_then(Value::as_f64)) .fold(f64::NEG_INFINITY, f64::max); if max.is_finite() { self.max_values.insert(key.to_string(), max); } } self.processlist = filtered; &self.processlist } fn set_extended_stats(&mut self, pid: i64, proc: &Value) -> Value { let mut ret = Map::new(); // CPU and MEM min/max/mean let previous = self.extended_stats.clone(); let prefix = |ret: &mut Map, stat_prefix: &str, current: f64, previous: Option<&Value>| { let prev = previous.and_then(|p| p.as_object()); let min_key = format!("{stat_prefix}_min"); let max_key = format!("{stat_prefix}_max"); let mean_sum_key = format!("{stat_prefix}_mean_sum"); let mean_counter_key = format!("{stat_prefix}_mean_counter"); let prev_min = prev.and_then(|p| p.get(&min_key)).and_then(Value::as_f64); let prev_max = prev.and_then(|p| p.get(&max_key)).and_then(Value::as_f64); let prev_sum = prev.and_then(|p| p.get(&mean_sum_key)).and_then(Value::as_f64); let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64); let min = prev_min.map(|m| m.min(current)).unwrap_or(current); let max = prev_max.map(|m| m.max(current)).unwrap_or(current); let sum = prev_sum.map(|s| s + current).unwrap_or(current); let count = prev_count.map(|c| c + 1).unwrap_or(1); ret.insert(min_key, json!(min)); ret.insert(max_key, json!(max)); ret.insert(mean_sum_key, json!(sum)); ret.insert(mean_counter_key, json!(count)); ret.insert(format!("{stat_prefix}_mean"), json!(sum / count as f64)); }; let cpu_percent = proc.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0); let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); prefix(&mut ret, "cpu", cpu_percent, previous.as_ref()); prefix(&mut ret, "memory", memory_percent, previous.as_ref()); ret.insert("extended_stats".to_string(), json!(true)); let extended = Value::Object(ret); self.extended_stats = Some(extended.clone()); self.extended_process_pid = Some(pid); extended } pub fn update_processcount(&mut self, processlist: &[Value]) { self.processcount.pid_max = self.pid_max(); self.processcount.running = processlist .iter() .filter(|p| p.get("status").and_then(Value::as_str) == Some("R")) .count() as i64; self.processcount.sleeping = processlist .iter() .filter(|p| p.get("status").and_then(Value::as_str) == Some("S")) .count() as i64; self.processcount.thread = processlist .iter() .filter_map(|p| p.get("num_threads").and_then(Value::as_i64)) .sum(); self.processcount.total = processlist.len() as i64; } fn update_list(&self, processlist: &[Value]) -> Vec { if !self.filter_focus.is_empty() { return processlist.iter().filter(|p| self.filter_focus.is_filtered(p)).cloned().collect(); } if self.filter.filter().is_none() { return processlist.to_vec(); } processlist.iter().filter(|p| self.filter.is_filtered(p)).cloned().collect() } fn update_export_list(&self, processlist: &[Value]) -> Vec { if self.filter_export.is_empty() { return Vec::new(); } processlist.iter().filter(|p| self.filter_export.is_filtered(p)).cloned().collect() } /// Get the process list (sorted or not). pub fn get_list(&self, sorted: bool) -> &[Value] { let _ = sorted; // the list is always kept sorted after update() &self.processlist } /// Get the process list as programs (aggregated by name). pub fn get_list_as_programs(&mut self) -> &[Value] { if !self.programs_valid { self.programs = processes_to_programs(&self.processlist); self.programs_valid = true; } &self.programs } pub fn get_export(&self) -> &[Value] { &self.processlist_export } pub fn get_stats(&self, pid: i64) -> Option<&Value> { self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid)) } /// Decrease the nice level of a process (higher priority). pub fn nice_decrease(&self, pid: i64) { let current = read_nice(pid).unwrap_or(0) as i32; if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current - 1) } == 0 { tracing::info!("Set nice level of process {pid} to {} (higher the priority)", current - 1); } else { tracing::warn!("Can not decrease (higher the priority) the nice level of process {pid} (access denied)"); } } /// Increase the nice level of a process (lower priority). pub fn nice_increase(&self, pid: i64) { let current = read_nice(pid).unwrap_or(0) as i32; if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current + 1) } == 0 { tracing::info!("Set nice level of process {pid} to {} (lower the priority)", current + 1); } else { tracing::warn!("Can not increase (lower the priority) the nice level of process {pid} (access denied)"); } } /// Kill a process. pub fn kill(&self, pid: i64) { assert!(pid != std::process::id() as i64, "Glances can kill itself..."); tracing::debug!("Send kill signal to process: {pid}"); unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL); } } } /// The global process manager (equivalent of `glances_processes`). pub fn processes() -> &'static Mutex { static PROCESSES: OnceLock> = OnceLock::new(); PROCESSES.get_or_init(|| Mutex::new(Processes::new())) } fn weighted(value: Option) -> f64 { value.unwrap_or(f64::NEG_INFINITY) } /// Sort a list of process dicts by the given key. pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &str, reverse: bool) { let key_of = |process: &Value, key: &str| -> f64 { match key { "io_counters" => { // [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] let counters = process.get("io_counters").and_then(Value::as_array); let read = counters.and_then(|c| c.first()).and_then(Value::as_f64).unwrap_or(0.0); let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0); let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0); let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0); read - read_old + write - write_old } "cpu_times" => { let times = process.get("cpu_times"); let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0); let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0); user + system } _ => process.get(key).and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY), } }; let string_key = |process: &Value, key: &str| -> String { process.get(key).and_then(Value::as_str).unwrap_or("~").to_string() }; stats.sort_by(|a, b| { let (primary, secondary) = match sorted_by { "name" | "username" => { let cmp = string_key(a, sorted_by).cmp(&string_key(b, sorted_by)); let ord = if reverse { cmp.reverse() } else { cmp }; return ord; } "cpu_num" => { let av = a.get("cpu_num").and_then(Value::as_i64).unwrap_or(0); let bv = b.get("cpu_num").and_then(Value::as_i64).unwrap_or(0); let ord = av.cmp(&bv); return if reverse { ord.reverse() } else { ord }; } _ => (key_of(a, sorted_by), key_of(b, sorted_by)), }; let ord = weighted(Some(primary)) .partial_cmp(&weighted(Some(secondary))) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| { weighted(Some(key_of(a, sorted_by_secondary))) .partial_cmp(&weighted(Some(key_of(b, sorted_by_secondary)))) .unwrap_or(std::cmp::Ordering::Equal) }); if reverse { ord.reverse() } else { ord } }); } /// Aggregate processes by program name (port of `processes_to_programs`). pub fn processes_to_programs(processlist: &[Value]) -> Vec { let no_io = [0u64; 5]; let mut programs: indexmap::IndexMap = indexmap::IndexMap::new(); for process in processlist { let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string(); let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0); let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0); let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({})); let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({})); let io_counters: Vec = process .get("io_counters") .and_then(Value::as_array) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .unwrap_or_else(|| no_io.to_vec()); let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0); let username = process.get("username").and_then(Value::as_str).unwrap_or("_").to_string(); let nice = process.get("nice").and_then(Value::as_i64).unwrap_or(0); let status = process.get("status").and_then(Value::as_str).unwrap_or("?").to_string(); let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null); match programs.get_mut(&name) { None => { programs.insert( name.clone(), json!({ "time_since_update": time_since_update, "num_threads": num_threads, "cpu_percent": cpu_percent, "memory_percent": memory_percent, "cpu_times": cpu_times, "memory_info": memory_info, "io_counters": io_counters, "childrens": [pid], "name": name, "cmdline": [name], "pid": "_", "username": username, "nice": nice, "status": status, }), ); } Some(program) => { let Some(program) = program.as_object_mut() else { continue; }; let add = |program: &mut Map, key: &str, value: f64| { let current = program.get(key).and_then(Value::as_f64).unwrap_or(0.0); program.insert(key.to_string(), json!(current + value)); }; add(program, "num_threads", num_threads); add(program, "cpu_percent", cpu_percent); add(program, "memory_percent", memory_percent); for key in ["user", "system", "iowait"] { let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0); if value != 0.0 || cpu_times.get(key).is_some() { if let Some(times) = program.get_mut("cpu_times").and_then(Value::as_object_mut) { let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0); times.insert(key.to_string(), json!(current + value)); } } } if let Some(program_memory) = program.get_mut("memory_info").and_then(Value::as_object_mut) { if let Some(addition) = memory_info.as_object() { for (key, value) in addition { let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0); let value = value.as_f64().unwrap_or(0.0); program_memory.insert(key.clone(), json!(current + value)); } } } // io_counters: sum slots 0..4, OR the tag. let mut summed: Vec = vec![0; 5]; let existing: Vec = program .get("io_counters") .and_then(Value::as_array) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .unwrap_or_else(|| no_io.to_vec()); for index in 0..4 { summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0); } summed[4] = existing.get(4).copied().unwrap_or(0).max(io_counters.get(4).copied().unwrap_or(0)); program.insert("io_counters".to_string(), json!(summed)); if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) { children.push(json!(pid)); } let same = |program: &Map, key: &str, value: &str| { program.get(key).and_then(Value::as_str) == Some(value) }; if !same(program, "username", &username) { program.insert("username".to_string(), json!("_")); } if program.get("nice").and_then(Value::as_i64) != Some(nice) { program.insert("nice".to_string(), json!("_")); } if !same(program, "status", &status) { program.insert("status".to_string(), json!("_")); } } } } programs .into_values() .map(|mut program| { if let Some(program) = program.as_object_mut() { let nprocs = program.get("childrens").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0); program.insert("nprocs".to_string(), json!(nprocs)); } program }) .collect() } /// A few fields grabbed straight from `/proc//stat` and `/proc//io`. #[derive(Debug, Clone, Default)] struct ProcStat { state: String, nice: i64, num_threads: i64, processor: i64, utime: f64, stime: f64, read_bytes: u64, write_bytes: u64, gid_real: i64, gid_effective: i64, gid_saved: i64, } fn read_proc_stat(pid: i64) -> Option { let content = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?; // The process name may contain spaces and parenthesis: strip it. let close = content.rfind(')')?; let name = &content[..=close]; let fields: Vec<&str> = content[close + 1..].split_whitespace().collect(); // fields[0] = state (stat field 3), so stat field N (1-based) is fields[N-3]. let field = |n: usize| fields.get(n).copied().unwrap_or(""); let state = field(0).chars().next().unwrap_or('?').to_ascii_uppercase().to_string(); let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64; let ticks = if ticks <= 0.0 { 100.0 } else { ticks }; let mut stat = ProcStat { state, nice: field(16).parse().unwrap_or(0), num_threads: field(17).parse().unwrap_or(0), processor: field(36).parse().unwrap_or(0), utime: field(11).parse::().unwrap_or(0.0) / ticks, stime: field(12).parse::().unwrap_or(0.0) / ticks, ..Default::default() }; let _ = name; if let Ok(io) = std::fs::read_to_string(format!("/proc/{pid}/io")) { for line in io.lines() { if let Some(value) = line.strip_prefix("read_bytes:") { stat.read_bytes = value.trim().parse().unwrap_or(0); } else if let Some(value) = line.strip_prefix("write_bytes:") { stat.write_bytes = value.trim().parse().unwrap_or(0); } } } if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) { if let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) { let gids: Vec = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect(); stat.gid_real = gids.first().copied().unwrap_or(0); stat.gid_effective = gids.get(1).copied().unwrap_or(0); stat.gid_saved = gids.get(2).copied().unwrap_or(0); } } Some(stat) } fn read_nice(pid: i64) -> Option { read_proc_stat(pid).map(|s| s.nice) } #[cfg(test)] mod tests { use super::*; #[test] fn test_processes_update() { use clap::Parser; let args = crate::args::Args::parse_from(["glances"]); let mut processes = Processes::new(); let list = processes.update(0, &args).to_vec(); assert!(!list.is_empty()); assert!(list[0].get("pid").is_some()); assert!(list[0].get("name").is_some()); } #[test] fn test_sort_stats() { let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})]; sort_stats(&mut stats, "cpu_percent", "memory_percent", true); assert_eq!(stats[0]["name"], "b"); } }