M4: fetch mode, cleanup and documentation

- --fetch with a Jinja-subset renderer and the shipped templates
- clippy clean, cargo fmt, all tests green
- README status and differential testing instructions
This commit is contained in:
glances-rs
2026-10-09 23:59:26 +00:00
parent cb22e53aa6
commit 6819ce4e18
47 changed files with 4145 additions and 935 deletions
+218 -75
View File
@@ -15,8 +15,15 @@ use sysinfo::{ProcessesToUpdate, System, Users};
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"];
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 {
@@ -155,7 +162,9 @@ impl Processes {
}
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())
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 {
@@ -199,20 +208,28 @@ impl Processes {
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 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 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 (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;
@@ -269,7 +286,12 @@ impl Processes {
}
// Sort processes by the current sort key.
sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse);
sort_stats(
&mut processlist,
&self.sort_key,
"memory_percent",
self.sort_reverse,
);
self.update_processcount(&processlist);
@@ -284,23 +306,25 @@ impl Processes {
{
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);
}
}
if let Some(target) = self.extended_process_pid
&& target == pid
{
let extended = self.set_extended_stats(pid, proc);
if let Value::Object(map) = extended
&& 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();
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.
@@ -343,8 +367,12 @@ impl Processes {
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 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);
@@ -356,8 +384,14 @@ impl Processes {
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);
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));
@@ -387,19 +421,31 @@ impl Processes {
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();
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()
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()
processlist
.iter()
.filter(|p| self.filter_export.is_filtered(p))
.cloned()
.collect()
}
/// Get the process list (sorted or not).
@@ -422,16 +468,23 @@ impl Processes {
}
pub fn get_stats(&self, pid: i64) -> Option<&Value> {
self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid))
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);
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)");
tracing::warn!(
"Can not decrease (higher the priority) the nice level of process {pid} (access denied)"
);
}
}
@@ -439,15 +492,23 @@ impl Processes {
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);
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)");
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...");
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);
@@ -472,23 +533,48 @@ pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &st
"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);
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);
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),
_ => 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()
process
.get(key)
.and_then(Value::as_str)
.unwrap_or("~")
.to_string()
};
stats.sort_by(|a, b| {
@@ -523,22 +609,53 @@ 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 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 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);
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 => {
@@ -575,20 +692,26 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
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 (value != 0.0 || cpu_times.get(key).is_some())
&& 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));
}
if let Some(program_memory) = program
.get_mut("memory_info")
.and_then(Value::as_object_mut)
&& 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.
@@ -598,10 +721,15 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.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);
for (index, value) in summed.iter_mut().take(4).enumerate() {
*value = 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));
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));
@@ -625,7 +753,11 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.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);
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
@@ -657,7 +789,12 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
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 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 {
@@ -680,13 +817,16 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
}
}
}
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);
}
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status"))
&& 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)
}
@@ -712,7 +852,10 @@ mod tests {
#[test]
fn test_sort_stats() {
let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})];
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");
}