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:
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¤t_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),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user