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
+35 -8
View File
@@ -35,12 +35,20 @@ fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result<String,
let Some(close) = stripped.find("}}}") else {
return Err("unclosed mustache tag".to_string());
};
(stripped[..close].trim().to_string(), true, tag_start + 1 + close + 3)
(
stripped[..close].trim().to_string(),
true,
tag_start + 1 + close + 3,
)
} else {
let Some(close) = after.find("}}") else {
return Err("unclosed mustache tag".to_string());
};
(after[..close].trim().to_string(), false, tag_start + close + 2)
(
after[..close].trim().to_string(),
false,
tag_start + close + 2,
)
};
if let Some(name) = tag.strip_prefix('#') {
@@ -199,7 +207,8 @@ impl Actions {
}
pub fn set(&mut self, stat_name: &str, criticality: &str) {
self.status.insert(stat_name.to_string(), criticality.to_string());
self.status
.insert(stat_name.to_string(), criticality.to_string());
}
pub fn get(&self, stat_name: &str) -> Option<&str> {
@@ -254,8 +263,14 @@ mod tests {
#[test]
fn test_render_simple() {
let data = json!({"name": "nginx", "pid": 42});
assert_eq!(render_template("kill {{pid}} {{name}}", &data).unwrap(), "kill 42 nginx");
assert_eq!(render_template("raw {{{name}}}", &data).unwrap(), "raw nginx");
assert_eq!(
render_template("kill {{pid}} {{name}}", &data).unwrap(),
"kill 42 nginx"
);
assert_eq!(
render_template("raw {{{name}}}", &data).unwrap(),
"raw nginx"
);
}
#[test]
@@ -268,13 +283,25 @@ mod tests {
#[test]
fn test_render_section() {
let data = json!({"items": [1, 2]});
assert_eq!(render_template("{{#items}}x{{/items}}", &data).unwrap(), "xx");
assert_eq!(render_template("{{^missing}}y{{/missing}}", &data).unwrap(), "y");
assert_eq!(
render_template("{{#items}}x{{/items}}", &data).unwrap(),
"xx"
);
assert_eq!(
render_template("{{^missing}}y{{/missing}}", &data).unwrap(),
"y"
);
}
#[test]
fn test_actions_no_trigger_before_start_timer() {
let mut actions = Actions::new(false, 100.0);
assert!(!actions.run("cpu_user", "critical", &["/bin/true".to_string()], false, None));
assert!(!actions.run(
"cpu_user",
"critical",
&["/bin/true".to_string()],
false,
None
));
}
}
+14 -7
View File
@@ -379,8 +379,14 @@ impl Args {
(self.server, "server mode (-s/--server)"),
(self.browser, "the server browser (--browser)"),
(self.export.is_some(), "export modules (--export)"),
(self.stdout_api_doc, "the Python API documentation (--api-doc)"),
(self.stdout_api_restful_doc, "the RESTful API documentation (--api-restful-doc)"),
(
self.stdout_api_doc,
"the Python API documentation (--api-doc)",
),
(
self.stdout_api_restful_doc,
"the RESTful API documentation (--api-restful-doc)",
),
(self.trace_malloc, "Python memory tracing (--trace-malloc)"),
];
checks.iter().find(|(used, _)| *used).map(|(_, name)| *name)
@@ -398,10 +404,7 @@ impl Args {
pub fn version_message(&self, log_file: &str) -> String {
format!(
"Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}",
"4.5.7",
GLANCES_API_VERSION,
GLANCES_VERSION,
log_file
"4.5.7", GLANCES_API_VERSION, GLANCES_VERSION, log_file
)
}
}
@@ -492,7 +495,11 @@ mod tests {
assert!(parse(&["glances", "-w"]).unsupported().is_some());
assert!(parse(&["glances", "-s"]).unsupported().is_some());
assert!(parse(&["glances", "-c", "host"]).unsupported().is_some());
assert!(parse(&["glances", "--export", "csv"]).unsupported().is_some());
assert!(
parse(&["glances", "--export", "csv"])
.unsupported()
.is_some()
);
assert!(parse(&["glances"]).unsupported().is_none());
}
+57 -24
View File
@@ -34,8 +34,11 @@ impl Config {
/// Build an empty configuration (used by tests and by callers that
/// already read a file).
pub fn empty(disable_config_exec: bool) -> Self {
let mut config = Config::default();
config.disable_config_exec = disable_config_exec;
let mut config = Config {
sections: IndexMap::new(),
loaded_file: None,
disable_config_exec,
};
config.set_defaults();
config
}
@@ -64,7 +67,11 @@ impl Config {
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config")));
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".config"))
});
if let Some(base) = user_base {
paths.push(base.join("glances").join(CONFIG_FILENAME));
}
@@ -74,12 +81,12 @@ impl Config {
// Default location: <prefix>/share/doc/glances (and the checkout
// layout next to the executable, useful while developing).
if let Ok(exe) = std::env::current_exe() {
if let Some(bin_dir) = exe.parent() {
for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME));
paths.push(prefix.join("conf").join(CONFIG_FILENAME));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(bin_dir) = exe.parent()
{
for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME));
paths.push(prefix.join("conf").join(CONFIG_FILENAME));
}
}
@@ -96,7 +103,10 @@ impl Config {
tracing::info!("Read configuration file '{}'", config_file.display());
}
Err(err) => {
tracing::error!("Can not read configuration file '{}': {err}", config_file.display());
tracing::error!(
"Can not read configuration file '{}': {err}",
config_file.display()
);
std::process::exit(1);
}
}
@@ -123,10 +133,19 @@ impl Config {
self.set_default_cwc("cpu", Some("steal"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]);
// Context switches bottleneck identification #1212
let ctx_switches_bottleneck =
500000.0 * 0.10 * std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64;
let careful = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20));
let warning = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10));
let ctx_switches_bottleneck = 500000.0
* 0.10
* std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1) as f64;
let careful = format!(
"{}",
ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)
);
let warning = format!(
"{}",
ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)
);
let critical = format!("{ctx_switches_bottleneck}");
self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]);
@@ -159,7 +178,12 @@ impl Config {
self.set_default_cwc("processlist", Some("mem"), ["50", "70", "90"]);
}
fn set_default_cwc<const N: usize>(&mut self, section: &str, option_header: Option<&str>, cwc: [&str; N]) {
fn set_default_cwc<const N: usize>(
&mut self,
section: &str,
option_header: Option<&str>,
cwc: [&str; N],
) {
let header = match option_header {
Some(header) => format!("{header}_"),
None => String::new(),
@@ -171,7 +195,9 @@ impl Config {
fn set_default(&mut self, section: &str, option: &str, default: &str) {
let entry = self.sections.entry(section.to_string()).or_default();
entry.entry(option.to_string()).or_insert_with(|| default.to_string());
entry
.entry(option.to_string())
.or_insert_with(|| default.to_string());
}
pub fn loaded_config_file(&self) -> Option<&Path> {
@@ -215,7 +241,9 @@ impl Config {
if let Some(value) = ret.as_mut() {
for command in find_backticks(value) {
if self.disable_config_exec {
tracing::warn!("Config exec disabled: skipping command `{command}` in [{section}] {option}");
tracing::warn!(
"Config exec disabled: skipping command `{command}` in [{section}] {option}"
);
} else {
tracing::warn!("Executing config command `{command}` for [{section}] {option}");
let output = system_exec(&command);
@@ -294,11 +322,11 @@ fn parse_ini(content: &str) -> IndexMap<String, IndexMap<String, String>> {
}
// Continuation line (indented).
if line.starts_with(' ') || line.starts_with('\t') {
if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref()) {
if let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key)) {
value.push('\n');
value.push_str(trimmed);
}
if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref())
&& let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key))
{
value.push('\n');
value.push_str(trimmed);
}
continue;
}
@@ -341,7 +369,9 @@ fn system_exec(command: &str) -> String {
return String::new();
};
match Command::new(program).args(args).output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).trim_end().to_string(),
Ok(output) => String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_string(),
Err(e) => format!("ERROR: {e}"),
}
}
@@ -387,6 +417,9 @@ mod tests {
let config = Config::from_str("[x]\nprefix=`echo hello`\n", false);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello");
let config = Config::from_str("[x]\nprefix=`echo hello`\n", true);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "`echo hello`");
assert_eq!(
config.get_value("x", "prefix", None).unwrap(),
"`echo hello`"
);
}
}
+52 -10
View File
@@ -13,7 +13,11 @@ const TREE: [(&str, &[&str], u32); 9] = [
("High CPU user mode", &["cpu_user"], 2),
("High CPU kernel usage", &["cpu_system"], 2),
("High CPU I/O waiting", &["cpu_iowait"], 2),
("Large CPU stolen time. System running the hypervisor is too busy.", &["cpu_steal"], 2),
(
"Large CPU stolen time. System running the hypervisor is too busy.",
&["cpu_steal"],
2,
),
("High CPU niced value", &["cpu_niced"], 2),
("System overloaded in the last 5 minutes", &["load"], 2),
("High swap (paging) usage", &["memswap"], 2),
@@ -28,14 +32,22 @@ pub fn build_global_message() -> String {
let mut best_min = 0u32;
let mut best_msg = TREE[0].0;
for (msg, names, min) in TREE {
let weight: u32 = names.iter().filter_map(|n| current.get(*n)).map(|t| t.value() as u32).sum();
let weight: u32 = names
.iter()
.filter_map(|n| current.get(*n))
.map(|t| t.value() as u32)
.sum();
if weight > best_weight {
best_weight = weight;
best_min = min;
best_msg = msg;
}
}
if best_weight >= best_min { best_msg.to_string() } else { TREE[0].0.to_string() }
if best_weight >= best_min {
best_msg.to_string()
} else {
TREE[0].0.to_string()
}
}
#[derive(Debug, Clone)]
@@ -106,11 +118,16 @@ impl Event {
let mut sorted: Vec<Value> = proc_list.to_vec();
sort_stats(&mut sorted, sort_key, "memory_percent", true);
for process in sorted.iter().take(6) {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
let name = process
.get("name")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string();
*self.top_dict.entry(name).or_insert(0) += 1;
}
let mut counts: Vec<(String, i64)> = self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect();
counts.sort_by(|a, b| b.1.cmp(&a.1));
let mut counts: Vec<(String, i64)> =
self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect();
counts.sort_by_key(|entry| std::cmp::Reverse(entry.1));
self.top = counts.into_iter().take(3).map(|(name, _)| name).collect();
} else {
self.top_dict.clear();
@@ -136,7 +153,12 @@ impl Default for EventsList {
impl EventsList {
pub fn new(max_events: usize, min_duration: f64, min_interval: f64) -> Self {
Self { events: Vec::new(), max_events, min_duration, min_interval }
Self {
events: Vec::new(),
max_events,
min_duration,
min_interval,
}
}
pub fn get(&self) -> Vec<Value> {
@@ -184,14 +206,27 @@ impl EventsList {
}
/// Add a new item to the events list.
pub fn add(&mut self, event_state: &str, event_type: &str, event_value: f64, proc_desc: &str) -> usize {
pub fn add(
&mut self,
event_state: &str,
event_type: &str,
event_value: f64,
proc_desc: &str,
) -> usize {
let event_time = chrono::Utc::now().timestamp() as f64;
let global_message = build_global_message();
let proc_list: Vec<Value> = processes().lock().unwrap().get_list(false).to_vec();
match self.event_exist(event_time, event_type) {
None => {
self.create_event(event_time, event_state, event_type, event_value, proc_desc, &global_message);
self.create_event(
event_time,
event_state,
event_type,
event_value,
proc_desc,
&global_message,
);
}
Some(index) => {
self.update_event(
@@ -266,7 +301,14 @@ impl EventsList {
Self::set_process_sort(event_type);
let sort_key = Self::get_event_sort_key(event_type).to_string();
let event = &mut self.events[event_index];
event.update(event_state, event_value, &sort_key, proc_list, proc_desc, global_message);
event.update(
event_state,
event_value,
&sort_key,
proc_list,
proc_desc,
global_message,
);
}
}
+17 -4
View File
@@ -74,9 +74,14 @@ impl Filter {
return false;
}
if self.filter_key.is_none() {
return self.filter_matches(process, "name", filter_re) || self.filter_matches(process, "cmdline", filter_re);
return self.filter_matches(process, "name", filter_re)
|| self.filter_matches(process, "cmdline", filter_re);
}
self.filter_matches(process, self.filter_key.as_deref().unwrap_or("name"), filter_re)
self.filter_matches(
process,
self.filter_key.as_deref().unwrap_or("name"),
filter_re,
)
}
fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool {
@@ -89,13 +94,21 @@ impl Filter {
if key == "cmdline" && !items.is_empty() {
items[0].as_str().unwrap_or_default().to_string()
} else {
items.iter().map(|i| i.as_str().unwrap_or_default()).collect::<Vec<_>>().join(" ")
items
.iter()
.map(|i| i.as_str().unwrap_or_default())
.collect::<Vec<_>>()
.join(" ")
}
}
Value::Null => return false,
other => crate::units::py_str(other),
};
filter_re.is_match(&text) && filter_re.find(&text).map(|m| m.len() == text.len()).unwrap_or(false)
filter_re.is_match(&text)
&& filter_re
.find(&text)
.map(|m| m.len() == text.len())
.unwrap_or(false)
}
}
+32 -6
View File
@@ -15,7 +15,11 @@ pub struct Attribute {
}
impl Attribute {
pub fn new(name: impl Into<String>, description: impl Into<String>, history_max_size: Option<usize>) -> Self {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
history_max_size: Option<usize>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
@@ -44,7 +48,15 @@ impl Attribute {
if nb == 0 {
self.history.iter().cloned().collect()
} else {
self.history.iter().rev().take(nb).collect::<Vec<_>>().into_iter().rev().cloned().collect()
self.history
.iter()
.rev()
.take(nb)
.collect::<Vec<_>>()
.into_iter()
.rev()
.cloned()
.collect()
}
}
@@ -63,7 +75,13 @@ pub struct History {
}
impl History {
pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option<usize>) {
pub fn add(
&mut self,
key: &str,
value: f64,
description: &str,
history_max_size: Option<usize>,
) {
let attribute = self
.attributes
.entry(key.to_string())
@@ -78,16 +96,24 @@ impl History {
}
pub fn get(&self, nb: usize) -> IndexMap<String, Vec<(DateTime<Utc>, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_raw(nb))).collect()
self.attributes
.iter()
.map(|(key, attr)| (key.clone(), attr.history_raw(nb)))
.collect()
}
pub fn get_json(&self, nb: usize) -> IndexMap<String, Vec<(String, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_json(nb))).collect()
self.attributes
.iter()
.map(|(key, attr)| (key.clone(), attr.history_json(nb)))
.collect()
}
/// Return the values of one item (used for TUI trends).
pub fn item_values(&self, item: &str) -> Option<Vec<f64>> {
self.attributes.get(item).map(|a| a.history.iter().map(|(_, v)| *v).collect())
self.attributes
.get(item)
.map(|a| a.history.iter().map(|(_, v)| *v).collect())
}
}
+2
View File
@@ -1,5 +1,7 @@
//! glances-core: shared foundations of the glances-rs rewrite.
//!
#![allow(clippy::type_complexity)]
//! This crate contains the pieces that are common to every plugin and output:
//! argument parsing, configuration file handling, the plugin model (limits,
//! views, history, rates...), formatting helpers, the event/threshold/action
+246 -88
View File
@@ -161,7 +161,10 @@ pub struct Field {
impl Field {
pub fn new(description: &str) -> Self {
Self { description: description.to_string(), ..Default::default() }
Self {
description: description.to_string(),
..Default::default()
}
}
pub fn unit(mut self, unit: &str) -> Self {
@@ -208,7 +211,9 @@ impl Field {
pub fn unit_type(&self) -> &'static str {
match self.unit.as_deref() {
Some("percent" | "percents") => "float",
Some("number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes") => "int",
Some(
"number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes",
) => "int",
_ => "float",
}
}
@@ -233,7 +238,10 @@ pub struct HistoryItem {
impl HistoryItem {
pub fn new(name: &str, description: &str) -> Self {
Self { name: name.to_string(), description: description.to_string() }
Self {
name: name.to_string(),
description: description.to_string(),
}
}
}
@@ -357,11 +365,16 @@ impl PluginModel {
/// Load limits from the configuration file.
pub fn load_limits(&mut self, config: &Config) {
self.limits.clear();
self.limits.insert("history_size".to_string(), Limit::Num(HISTORY_MAX_SIZE as f64));
self.limits.insert(
"history_size".to_string(),
Limit::Num(HISTORY_MAX_SIZE as f64),
);
if config.has_section("global") {
let history_size = config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64);
self.limits.insert("history_size".to_string(), Limit::Num(history_size));
let history_size =
config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64);
self.limits
.insert("history_size".to_string(), Limit::Num(history_size));
}
if config.has_section(&self.name) {
@@ -372,7 +385,8 @@ impl PluginModel {
self.limits.insert(limit.clone(), Limit::Num(number));
}
Err(_) => {
let items: Vec<String> = value.split(',').map(|s| s.trim().to_string()).collect();
let items: Vec<String> =
value.split(',').map(|s| s.trim().to_string()).collect();
self.limits.insert(limit.clone(), Limit::List(items));
}
}
@@ -389,7 +403,8 @@ impl PluginModel {
}
pub fn set_refresh(&mut self, value: f64) {
self.limits.insert(format!("{}_refresh", self.name), Limit::Num(value));
self.limits
.insert(format!("{}_refresh", self.name), Limit::Num(value));
}
pub fn get_refresh(&self) -> f64 {
@@ -424,12 +439,19 @@ impl PluginModel {
}
pub fn is_limit(&self, criticality: &str, stat_name: &str) -> bool {
let key = format!("{}_{criticality}", self.get_stat_name(Some(stat_name), None).to_lowercase());
let key = format!(
"{}_{criticality}",
self.get_stat_name(Some(stat_name), None).to_lowercase()
);
self.limits.contains_key(&key)
}
/// Return the (commands, repeat) action pair for the alert.
pub fn get_limit_action(&self, criticality: &str, stat_name: &str) -> (Option<Vec<String>>, bool) {
pub fn get_limit_action(
&self,
criticality: &str,
stat_name: &str,
) -> (Option<Vec<String>>, bool) {
let candidates = [
(format!("{stat_name}_{criticality}_action"), false),
(format!("{stat_name}_{criticality}_action_repeat"), true),
@@ -446,10 +468,18 @@ impl PluginModel {
pub fn get_limit_log(&self, stat_name: &str, default_action: bool) -> bool {
if let Some(limit) = self.limits.get(&format!("{stat_name}_log")) {
return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false);
return limit
.as_list()
.and_then(|l| l.first())
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
}
if let Some(limit) = self.limits.get(&format!("{}_log", self.name)) {
return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false);
return limit
.as_list()
.and_then(|l| l.first())
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
}
default_action
}
@@ -477,7 +507,10 @@ impl PluginModel {
key = format!("{key}_{header}");
}
match self.limits.get(&format!("{key}_{value}")) {
Some(Limit::List(items)) => items.first().map(|v| v.to_lowercase() == "true").unwrap_or(false),
Some(Limit::List(items)) => items
.first()
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false),
Some(Limit::Num(n)) => *n != 0.0,
None => false,
}
@@ -487,7 +520,11 @@ impl PluginModel {
pub fn is_show(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value);
self.get_conf_value("show", header).iter().any(|pattern| {
fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false)
fullmatch(pattern, value)
|| alias
.as_deref()
.map(|a| fullmatch(pattern, a))
.unwrap_or(false)
})
}
@@ -495,7 +532,11 @@ impl PluginModel {
pub fn is_hide(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value);
self.get_conf_value("hide", header).iter().any(|pattern| {
fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false)
fullmatch(pattern, value)
|| alias
.as_deref()
.map(|a| fullmatch(pattern, a))
.unwrap_or(false)
})
}
@@ -537,17 +578,17 @@ impl PluginModel {
pub fn get_stat_name(&self, header: Option<&str>, action_key: Option<&str>) -> String {
let mut ret = self.name.clone();
if let Some(key) = action_key {
if !key.is_empty() {
ret.push('_');
ret.push_str(key);
}
if let Some(key) = action_key
&& !key.is_empty()
{
ret.push('_');
ret.push_str(key);
}
if let Some(header) = header {
if !header.is_empty() {
ret.push('_');
ret.push_str(header);
}
if let Some(header) = header
&& !header.is_empty()
{
ret.push('_');
ret.push_str(header);
}
ret
}
@@ -560,15 +601,32 @@ impl PluginModel {
.map(|(name, _)| name.clone())
.collect();
for field_name in mmm_names {
let unit = self.fields.get(&field_name).and_then(|f| f.unit.clone()).unwrap_or_default();
let unit = self
.fields
.get(&field_name)
.and_then(|f| f.unit.clone())
.unwrap_or_default();
self.mmm.insert(
field_name.clone(),
MmmState { values: VecDeque::new(), min: None, max: None },
MmmState {
values: VecDeque::new(),
min: None,
max: None,
},
);
for (suffix, description) in [
("_min", format!("Minimum {field_name} observed since Glances startup.")),
("_max", format!("Maximum {field_name} observed since Glances startup.")),
("_mean", format!("Mean (average) {field_name} computed from the history.")),
(
"_min",
format!("Minimum {field_name} observed since Glances startup."),
),
(
"_max",
format!("Maximum {field_name} observed since Glances startup."),
),
(
"_mean",
format!("Mean (average) {field_name} computed from the history."),
),
] {
let generated = format!("{field_name}{suffix}");
let unit = unit.clone();
@@ -604,7 +662,10 @@ impl PluginModel {
let mean = state.values.iter().sum::<f64>() / state.values.len() as f64;
map.insert(format!("{field_name}_min"), json!(min));
map.insert(format!("{field_name}_max"), json!(max));
map.insert(format!("{field_name}_mean"), json!((mean * 100.0).round() / 100.0));
map.insert(
format!("{field_name}_mean"),
json!((mean * 100.0).round() / 100.0),
);
}
}
@@ -623,7 +684,8 @@ impl PluginModel {
.map(|list| {
list.iter()
.filter_map(|item| {
item.get(key).map(|v| (crate::units::py_str(v), item.clone()))
item.get(key)
.map(|v| (crate::units::py_str(v), item.clone()))
})
.collect()
})
@@ -633,7 +695,7 @@ impl PluginModel {
for item in items.iter_mut() {
let previous_item = key
.and_then(|k| item.get(k))
.map(|v| crate::units::py_str(v))
.map(crate::units::py_str)
.and_then(|k| previous_by_key.get(&k).cloned())
.unwrap_or(Value::Object(Map::new()));
self.compute_rate_on_stat(item, Some(&previous_item));
@@ -660,13 +722,20 @@ impl PluginModel {
let Value::Object(map) = stat else {
return;
};
let rate_fields: Vec<String> =
self.fields.iter().filter(|(_, f)| f.rate).map(|(name, _)| name.clone()).collect();
let rate_fields: Vec<String> = self
.fields
.iter()
.filter(|(_, f)| f.rate)
.map(|(name, _)| name.clone())
.collect();
for field in rate_fields {
if !map.contains_key(&field) {
continue;
}
map.insert("time_since_update".to_string(), json!(self.time_since_last_update));
map.insert(
"time_since_update".to_string(),
json!(self.time_since_last_update),
);
let current_value = map.get(&field).cloned().unwrap_or(Value::Null);
map.insert(format!("{field}_gauge"), current_value.clone());
let current = as_f64(&current_value);
@@ -703,7 +772,10 @@ impl PluginModel {
None => self.views.get(field),
Some(item) => self.views.get(item).and_then(|v| v.get(field)),
};
item_views.and_then(|v| v.get(option)).cloned().unwrap_or_else(|| json!("DEFAULT"))
item_views
.and_then(|v| v.get(option))
.cloned()
.unwrap_or_else(|| json!("DEFAULT"))
}
pub fn view_decoration(&self, item: Option<&str>, field: &str) -> Decoration {
@@ -714,7 +786,9 @@ impl PluginModel {
}
pub fn view_bool(&self, item: Option<&str>, field: &str, option: &str) -> bool {
self.get_view(item, field, option).as_bool().unwrap_or(false)
self.get_view(item, field, option)
.as_bool()
.unwrap_or(false)
}
/// Override the decoration of a view (used by plugins which add specific
@@ -733,18 +807,37 @@ impl PluginModel {
let Some(definition) = self.fields.get(field).cloned() else {
return Decoration::Default;
};
if definition.rate && self.stats.is_object() {
if self.stats.get("time_since_update").and_then(as_f64) == Some(0.0) {
return Decoration::Default;
}
if definition.rate
&& self.stats.is_object()
&& self.stats.get("time_since_update").and_then(as_f64) == Some(0.0)
{
return Decoration::Default;
}
if definition.log {
let (current, minimum, maximum) = numeric_alert_args(value);
return self.get_alert(current, minimum, maximum, true, false, Some(field), None, true);
return self.get_alert(
current,
minimum,
maximum,
true,
false,
Some(field),
None,
true,
);
}
if definition.alert {
let (current, minimum, maximum) = numeric_alert_args(value);
return self.get_alert(current, minimum, maximum, true, false, Some(field), None, false);
return self.get_alert(
current,
minimum,
maximum,
true,
false,
Some(field),
None,
false,
);
}
Decoration::Default
}
@@ -756,14 +849,18 @@ impl PluginModel {
let hidden = if !self.hide_zero {
false
} else if let Some(key) = key {
if let Some(existing) = self.views.get(key).and_then(|v| v.get(field)).and_then(|v| v.get("hidden")) {
if let Some(existing) = self
.views
.get(key)
.and_then(|v| v.get(field))
.and_then(|v| v.get("hidden"))
{
let mut hidden = existing.as_bool().unwrap_or(false);
if self.hide_zero_fields.iter().any(|f| f == field) {
if let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v)) {
if raw > self.hide_threshold_bytes {
hidden = false;
}
}
if self.hide_zero_fields.iter().any(|f| f == field)
&& let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v))
&& raw > self.hide_threshold_bytes
{
hidden = false;
}
hidden
} else {
@@ -860,29 +957,31 @@ impl PluginModel {
let value = current * 100.0 / maximum;
let stat_name = self.get_stat_name(header, action_key).to_lowercase();
let mut ret = if is_max { Decoration::Max } else { Decoration::Ok };
let mut ret = if is_max {
Decoration::Max
} else {
Decoration::Ok
};
let critical = self.get_limit("critical", &stat_name);
let warning = self.get_limit("warning", &stat_name);
let careful = self.get_limit("careful", &stat_name);
if let Some(critical) = critical {
if value >= critical {
ret = Decoration::Critical;
}
if let Some(critical) = critical
&& value >= critical
{
ret = Decoration::Critical;
}
if ret == Decoration::Ok {
if let Some(warning) = warning {
if value >= warning {
ret = Decoration::Warning;
}
}
if ret == Decoration::Ok
&& let Some(warning) = warning
&& value >= warning
{
ret = Decoration::Warning;
}
if ret == Decoration::Ok {
if let Some(careful) = careful {
if value >= careful {
ret = Decoration::Careful;
}
}
if ret == Decoration::Ok
&& let Some(careful) = careful
&& value >= careful
{
ret = Decoration::Careful;
}
if ret == Decoration::Ok && careful.is_none() && warning.is_none() && critical.is_none() {
ret = Decoration::Default;
@@ -903,12 +1002,20 @@ impl PluginModel {
Decoration::Max => "MAX",
_ => "OK",
};
events().lock().unwrap().add(log_ret, &stat_name.to_uppercase(), value, "");
events()
.lock()
.unwrap()
.add(log_ret, &stat_name.to_uppercase(), value, "");
}
thresholds().lock().unwrap().add(&stat_name, ret.name());
self.manage_action(&stat_name, ret.name().to_lowercase().as_str(), header, action_key);
self.manage_action(
&stat_name,
ret.name().to_lowercase().as_str(),
header,
action_key,
);
match (ret, log_str) {
(Decoration::Ok, true) => Decoration::OkLog,
@@ -919,11 +1026,22 @@ impl PluginModel {
}
}
pub fn get_alert_log(&mut self, current: f64, maximum: f64, header: Option<&str>) -> Decoration {
pub fn get_alert_log(
&mut self,
current: f64,
maximum: f64,
header: Option<&str>,
) -> Decoration {
self.get_alert(current, 0.0, maximum, false, false, header, None, true)
}
fn manage_action(&mut self, stat_name: &str, trigger: &str, header: Option<&str>, action_key: Option<&str>) {
fn manage_action(
&mut self,
stat_name: &str,
trigger: &str,
header: Option<&str>,
action_key: Option<&str>,
) {
let (command, repeat) = self.get_limit_action(trigger, stat_name);
match command {
None => {
@@ -935,12 +1053,11 @@ impl PluginModel {
let mut mustache_dict = match &stats_action {
Value::Array(items) if action_key.is_some() => {
let action_key = action_key.unwrap();
let key_field = self.item_key.clone();
let key_field = self.item_key;
items
.iter()
.find(|item| {
key_field
.as_deref()
.and_then(|k| item.get(k))
.map(crate::units::py_str)
.as_deref()
@@ -953,15 +1070,29 @@ impl PluginModel {
_ => Value::Object(Map::new()),
};
if let Value::Object(map) = &mut mustache_dict {
map.insert("critical".to_string(), json!(self.get_limit("critical", stat_name)));
map.insert("warning".to_string(), json!(self.get_limit("warning", stat_name)));
map.insert("careful".to_string(), json!(self.get_limit("careful", stat_name)));
map.insert(
"critical".to_string(),
json!(self.get_limit("critical", stat_name)),
);
map.insert(
"warning".to_string(),
json!(self.get_limit("warning", stat_name)),
);
map.insert(
"careful".to_string(),
json!(self.get_limit("careful", stat_name)),
);
map.insert(
"time".to_string(),
json!(chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.6f").to_string()),
json!(
chrono::Local::now()
.format("%Y-%m-%dT%H:%M:%S%.6f")
.to_string()
),
);
}
self.actions.run(stat_name, trigger, &commands, repeat, Some(&mustache_dict));
self.actions
.run(stat_name, trigger, &commands, repeat, Some(&mustache_dict));
}
}
}
@@ -994,7 +1125,14 @@ impl PluginModel {
}
/// Add a `key: value` pair to the TUI (port of `curse_add_stat`).
pub fn curse_add_stat(&self, key: &str, width: Option<usize>, header: &str, separator: &str, trailer: &str) -> Vec<StatLine> {
pub fn curse_add_stat(
&self,
key: &str,
width: Option<usize>,
header: &str,
separator: &str,
trailer: &str,
) -> Vec<StatLine> {
let Some(value) = self.stats.get(key) else {
return Vec::new();
};
@@ -1006,7 +1144,9 @@ impl PluginModel {
let unit_short = definition.map(|f| f.short_unit()).unwrap_or("");
let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float");
let value = if definition.map(|f| f.rate).unwrap_or(false) {
self.stats.get(&format!("{key}_rate_per_sec")).unwrap_or(&Value::Null)
self.stats
.get(format!("{key}_rate_per_sec"))
.unwrap_or(&Value::Null)
} else {
value
};
@@ -1020,7 +1160,9 @@ impl PluginModel {
Some(v) => {
if unit_type == "float" {
format!("{v:.1}{unit_short}")
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
} else if let Some(min_symbol) =
definition.and_then(|f| f.min_symbol.clone())
{
format!("{}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else {
format!("{}{unit_short}", v as i64)
@@ -1030,13 +1172,18 @@ impl PluginModel {
(item, value_text)
}
Some(width) => {
let item = format!("{header}{key_name:<width$}{separator}", width = width.saturating_sub(7));
let item = format!(
"{header}{key_name:<width$}{separator}",
width = width.saturating_sub(7)
);
let value_text = match value {
None => format!("{:>5}", "-"),
Some(v) => {
if unit_type == "float" {
format!("{v:>5.1}{unit_short}")
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
} else if let Some(min_symbol) =
definition.and_then(|f| f.min_symbol.clone())
{
format!("{:>5}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else {
format!("{:>5}{unit_short}", v as i64)
@@ -1048,7 +1195,11 @@ impl PluginModel {
};
let item = format!("{msg_item}{trailer}");
let decoration = if value.is_some() { self.view_decoration(None, key) } else { Decoration::Default };
let decoration = if value.is_some() {
self.view_decoration(None, key)
} else {
Decoration::Default
};
let optional = self.view_bool(None, key, "optional");
vec![
@@ -1088,12 +1239,14 @@ impl PluginModel {
}
let history_max_size = self.history_size();
let items = self.items_history_list.clone();
let key = self.item_key.clone();
let key = self.item_key;
for item in items {
match &export {
Value::Array(list) => {
for entry in list {
let Some(item_key) = key.as_deref().and_then(|k| entry.get(k)).map(crate::units::py_str) else {
let Some(item_key) =
key.and_then(|k| entry.get(k)).map(crate::units::py_str)
else {
continue;
};
if let Some(value) = entry.get(&item.name).and_then(as_f64) {
@@ -1108,7 +1261,12 @@ impl PluginModel {
}
Value::Object(_) => {
if let Some(value) = export.get(&item.name).and_then(as_f64) {
self.history.add(&item.name, value, &item.description, Some(history_max_size));
self.history.add(
&item.name,
value,
&item.description,
Some(history_max_size),
);
}
}
_ => {}
+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");
}
+18 -9
View File
@@ -24,16 +24,21 @@ pub fn sanitize_value(value: &serde_json::Value) -> serde_json::Value {
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(sanitize_value).collect())
}
serde_json::Value::Object(map) => {
serde_json::Value::Object(map.iter().map(|(k, v)| (k.clone(), sanitize_value(v))).collect())
}
serde_json::Value::Object(map) => serde_json::Value::Object(
map.iter()
.map(|(k, v)| (k.clone(), sanitize_value(v)))
.collect(),
),
other => other.clone(),
}
}
/// Split a command string into an argument list. Spaces are separators except
/// within single or double quotes (the surrounding quotes are removed).
pub fn split_args(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> Result<Vec<String>, String> {
pub fn split_args(
cmd: &str,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> Result<Vec<String>, String> {
let mut args: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_single = false;
@@ -182,16 +187,20 @@ fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, St
return stderr;
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(target) = stdout_redirect {
if let Ok(mut file) = std::fs::File::create(&target) {
let _ = file.write_all(stdout.as_bytes());
}
if let Some(target) = stdout_redirect
&& let Ok(mut file) = std::fs::File::create(&target)
{
let _ = file.write_all(stdout.as_bytes());
}
stdout
}
/// Run a command line, interpreting (or not) the shell operators.
pub fn secure_popen(cmd: &str, allow_operators: bool, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
pub fn secure_popen(
cmd: &str,
allow_operators: bool,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> String {
if !allow_operators {
return run_argv(cmd, render);
}
+1
View File
@@ -13,6 +13,7 @@ pub enum Threshold {
}
impl Threshold {
#[allow(clippy::should_implement_trait)]
pub fn from_str(name: &str) -> Option<Self> {
match name {
"OK" => Some(Threshold::Ok),
+12 -3
View File
@@ -14,7 +14,10 @@ pub struct Timer {
impl Timer {
pub fn new(duration: f64) -> Self {
let mut timer = Self { duration, target: Instant::now() };
let mut timer = Self {
duration,
target: Instant::now(),
};
timer.start();
timer
}
@@ -32,7 +35,11 @@ impl Timer {
/// Elapsed seconds since the timer was started.
pub fn get(&self) -> f64 {
self.duration - self.target.saturating_duration_since(Instant::now()).as_secs_f64()
self.duration
- self
.target
.saturating_duration_since(Instant::now())
.as_secs_f64()
}
pub fn set(&mut self, duration: f64) {
@@ -62,7 +69,9 @@ impl Default for Counter {
impl Counter {
pub fn new() -> Self {
Self { start: Instant::now() }
Self {
start: Instant::now(),
}
}
pub fn reset(&mut self) {
+33 -11
View File
@@ -69,7 +69,22 @@ pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol
/// Render a progress bar the way the Python `Bar` class does.
pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: bool) -> String {
let size = if display_value { size.saturating_sub(6) } else { size };
bar_custom(percent, size, bar_char, " ", display_value)
}
/// Render a progress bar with custom characters.
pub fn bar_custom(
percent: f64,
size: usize,
bar_char: &str,
empty_char: &str,
display_value: bool,
) -> String {
let size = if display_value {
size.saturating_sub(6)
} else {
size
};
let value = percent.clamp(0.0, 100.0);
let scaled = size as f64 * value / 100.0;
let whole = scaled.floor() as usize;
@@ -81,14 +96,18 @@ pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: boo
if (frac * 8.0) as usize >= 5 {
bar.push_str(bar_char);
} else {
bar.push(' ');
bar.push_str(empty_char);
}
whole += 1;
}
bar.push_str(&" ".repeat(size.saturating_sub(whole)));
bar.push_str(&empty_char.repeat(size.saturating_sub(whole)));
if display_value {
if percent >= 100.0 {
bar = format!("{bar} {}{:3.0}%", if percent > 100.0 { ">" } else { " " }, 100.0);
bar = format!(
"{bar} {}{:3.0}%",
if percent > 100.0 { ">" } else { " " },
100.0
);
} else {
bar = format!("{bar}{percent:5.1}%");
}
@@ -104,11 +123,9 @@ pub fn string_value_to_float(s: &str) -> Option<f64> {
let mut digits = String::new();
let mut rest = String::new();
for c in cleaned.chars() {
if c.is_ascii_digit() || c == '.' {
if rest.is_empty() {
digits.push(c);
continue;
}
if (c.is_ascii_digit() || c == '.') && rest.is_empty() {
digits.push(c);
continue;
}
rest.push(c);
}
@@ -195,7 +212,9 @@ pub fn py_repr(value: &Value) -> String {
pub fn as_f64(value: &Value) -> Option<f64> {
match value {
Value::Number(n) => n.as_f64(),
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => {
None
}
}
}
@@ -242,7 +261,10 @@ pub fn split_esc(input: &str, sep: char, esc: char) -> Vec<String> {
/// shell by adding a `$` prefix in front of them. (Only the replacement
/// flavour is needed by the plugins.)
pub fn replace_special_chars(input: &str, by: char) -> String {
input.replace("\r\n", &by.to_string()).replace('\n', &by.to_string()).replace('\t', &by.to_string())
input
.replace("\r\n", &by.to_string())
.replace('\n', &by.to_string())
.replace('\t', &by.to_string())
}
#[cfg(test)]