- --fetch with a Jinja-subset renderer and the shipped templates - clippy clean, cargo fmt, all tests green - README status and differential testing instructions
863 lines
31 KiB
Rust
863 lines
31 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
|
|
&& 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();
|
|
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())
|
|
&& 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)
|
|
&& 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, 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));
|
|
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"))
|
|
&& 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");
|
|
}
|
|
}
|