Files
glances-rs/crates/glances-core/src/processes.rs
T
glances-rs 975385da5d M1: core engine, config, plugin model and core plugins
- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI
- Python-compatible config file handling, limits, views, rates, MMM, history
- process collection via sysinfo + procfs, filters, thresholds, events, actions
- 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...)
- stdout, stdout-csv, stdout-json, --issue, --modules-list output modes
- differential test harness against the Python implementation
2026-10-09 23:25:35 +00:00

720 lines
29 KiB
Rust

//! 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<i64>,
}
#[derive(Debug)]
pub struct Processes {
system: System,
users: Users,
users_timer: f64,
pub processlist: Vec<Value>,
pub processlist_export: Vec<Value>,
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<i64>,
pub extended_stats: Option<Value>,
pub no_kernel_threads: bool,
pub disable_io_counters: bool,
pub disable_gids: bool,
pub disable_cpu_num: bool,
pub disable_stats: Vec<String>,
/// 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<String, f64>,
io_old: HashMap<i64, [u64; 2]>,
programs: Vec<Value>,
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<i64> {
std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::<i64>().ok())
}
pub fn processes_count(&self, max_processes: Option<i64>) -> 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<Value> = 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<String> =
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<i64> =
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<String, Value>,
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<Value> {
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<Value> {
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<Processes> {
static PROCESSES: OnceLock<Mutex<Processes>> = OnceLock::new();
PROCESSES.get_or_init(|| Mutex::new(Processes::new()))
}
fn weighted(value: Option<f64>) -> 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<Value> {
let no_io = [0u64; 5];
let mut programs: indexmap::IndexMap<String, Value> = 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<u64> = 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<String, Value>, 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<u64> = vec![0; 5];
let existing: Vec<u64> = 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<String, Value>, 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/<pid>/stat` and `/proc/<pid>/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<ProcStat> {
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::<f64>().unwrap_or(0.0) / ticks,
stime: field(12).parse::<f64>().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<i64> = 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<i64> {
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");
}
}