M1: core engine, config, plugin model and core plugins
- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI - Python-compatible config file handling, limits, views, rates, MMM, history - process collection via sysinfo + procfs, filters, thresholds, events, actions - 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...) - stdout, stdout-csv, stdout-json, --issue, --modules-list output modes - differential test harness against the Python implementation
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "glances-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
|
||||
clap = { workspace = true, features = ["derive", "wrap_help"] }
|
||||
indexmap.workspace = true
|
||||
libc.workspace = true
|
||||
regex.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
shlex = "2.0.1"
|
||||
sysinfo = "0.39.6"
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
unicode-width.workspace = true
|
||||
@@ -0,0 +1,280 @@
|
||||
//! On-alert actions (port of `glances.actions`).
|
||||
|
||||
use crate::secure::{sanitize_value, secure_popen};
|
||||
use crate::timer::Timer;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Render a (small) Mustache template with JSON data.
|
||||
///
|
||||
/// Supported tags: `{{name}}` (HTML escaped), `{{{name}}}` and `{{&name}}`
|
||||
/// (raw), dotted names, and `{{#name}}...{{/name}}` / `{{^name}}...{{/name}}`
|
||||
/// sections. Anything else is refused so a malformed template can never be
|
||||
/// executed half-rendered.
|
||||
pub fn render_template(template: &str, data: &Value) -> Result<String, String> {
|
||||
render_range(template, data, &mut 0)
|
||||
}
|
||||
|
||||
fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result<String, String> {
|
||||
let mut out = String::new();
|
||||
while *pos < template.len() {
|
||||
let rest = &template[*pos..];
|
||||
let Some(open) = rest.find("{{") else {
|
||||
out.push_str(rest);
|
||||
*pos = template.len();
|
||||
break;
|
||||
};
|
||||
out.push_str(&rest[..open]);
|
||||
*pos += open;
|
||||
|
||||
// Standalone-ish: read the tag.
|
||||
let tag_start = *pos + 2;
|
||||
let after = &template[tag_start..];
|
||||
let (tag, raw, tag_end) = if let Some(stripped) = after.strip_prefix('{') {
|
||||
// Triple mustache
|
||||
let Some(close) = stripped.find("}}}") else {
|
||||
return Err("unclosed mustache tag".to_string());
|
||||
};
|
||||
(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)
|
||||
};
|
||||
|
||||
if let Some(name) = tag.strip_prefix('#') {
|
||||
// Section: find the matching close tag.
|
||||
let body_start = tag_end;
|
||||
let (body, body_end) = find_section(&template[body_start..], name.trim())?;
|
||||
*pos = body_start + body_end;
|
||||
let value = lookup(data, name.trim());
|
||||
match value {
|
||||
Some(Value::Array(items)) => {
|
||||
for item in &items {
|
||||
let mut inner_pos = 0;
|
||||
out.push_str(&render_range(body, item, &mut inner_pos)?);
|
||||
}
|
||||
}
|
||||
Some(Value::Bool(true)) => {
|
||||
let mut inner_pos = 0;
|
||||
out.push_str(&render_range(body, data, &mut inner_pos)?);
|
||||
}
|
||||
Some(Value::Object(map)) => {
|
||||
let merged = merge_context(data, &Value::Object(map.clone()));
|
||||
let mut inner_pos = 0;
|
||||
out.push_str(&render_range(body, &merged, &mut inner_pos)?);
|
||||
}
|
||||
Some(Value::String(s)) if !s.is_empty() => {
|
||||
let mut inner_pos = 0;
|
||||
out.push_str(&render_range(body, data, &mut inner_pos)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let Some(name) = tag.strip_prefix('^') {
|
||||
let body_start = tag_end;
|
||||
let (body, body_end) = find_section(&template[body_start..], name.trim())?;
|
||||
*pos = body_start + body_end;
|
||||
let value = lookup(data, name.trim());
|
||||
let falsy = match value {
|
||||
None | Some(Value::Null) | Some(Value::Bool(false)) => true,
|
||||
Some(Value::Array(items)) => items.is_empty(),
|
||||
Some(Value::String(s)) => s.is_empty(),
|
||||
_ => false,
|
||||
};
|
||||
if falsy {
|
||||
let mut inner_pos = 0;
|
||||
out.push_str(&render_range(body, data, &mut inner_pos)?);
|
||||
}
|
||||
} else if let Some(name) = tag.strip_prefix('/') {
|
||||
return Err(format!("unexpected closing tag {{{{/{}}}}}", name.trim()));
|
||||
} else {
|
||||
*pos = tag_end;
|
||||
let name = tag.strip_prefix('&').unwrap_or(&tag);
|
||||
let value = lookup(data, name.trim());
|
||||
let text = value_to_text(value);
|
||||
if raw || tag.starts_with('&') {
|
||||
out.push_str(&text);
|
||||
} else {
|
||||
out.push_str(&html_escape(&text));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Find the body and length of a section, starting after its opening tag.
|
||||
fn find_section<'a>(template: &'a str, name: &str) -> Result<(&'a str, usize), String> {
|
||||
let open = format!("{{{{#{name}}}}}");
|
||||
let close = format!("{{{{/{name}}}}}");
|
||||
let mut depth = 0usize;
|
||||
let mut cursor = 0usize;
|
||||
while cursor < template.len() {
|
||||
let next_open = template[cursor..].find(&open).map(|i| cursor + i);
|
||||
let next_close = template[cursor..].find(&close).map(|i| cursor + i);
|
||||
match (next_open, next_close) {
|
||||
(Some(o), Some(c)) if c < o => {
|
||||
if depth == 0 {
|
||||
return Ok((&template[..c], c + close.len()));
|
||||
}
|
||||
depth -= 1;
|
||||
cursor = c + close.len();
|
||||
}
|
||||
(Some(o), _) => {
|
||||
depth += 1;
|
||||
cursor = o + open.len();
|
||||
}
|
||||
(None, Some(c)) => {
|
||||
if depth == 0 {
|
||||
return Ok((&template[..c], c + close.len()));
|
||||
}
|
||||
depth -= 1;
|
||||
cursor = c + close.len();
|
||||
}
|
||||
(None, None) => return Err(format!("unclosed mustache section {name}")),
|
||||
}
|
||||
}
|
||||
Err(format!("unclosed mustache section {name}"))
|
||||
}
|
||||
|
||||
fn merge_context(data: &Value, extra: &Value) -> Value {
|
||||
match (data, extra) {
|
||||
(Value::Object(base), Value::Object(extra)) => {
|
||||
let mut merged = base.clone();
|
||||
for (k, v) in extra {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
Value::Object(merged)
|
||||
}
|
||||
_ => extra.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup(data: &Value, name: &str) -> Option<Value> {
|
||||
if name == "." {
|
||||
return Some(data.clone());
|
||||
}
|
||||
let mut current = data;
|
||||
for part in name.split('.') {
|
||||
current = current.get(part)?;
|
||||
}
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
fn value_to_text(value: Option<Value>) -> String {
|
||||
match value {
|
||||
None | Some(Value::Null) => String::new(),
|
||||
Some(Value::String(s)) => s,
|
||||
Some(Value::Bool(b)) => if b { "True" } else { "False" }.to_string(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
Some(other) => crate::units::py_str(&other),
|
||||
}
|
||||
}
|
||||
|
||||
fn html_escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
/// This class manages actions when an alert is reached.
|
||||
#[derive(Debug)]
|
||||
pub struct Actions {
|
||||
disable_config_exec: bool,
|
||||
status: HashMap<String, String>,
|
||||
start_timer: Timer,
|
||||
}
|
||||
|
||||
impl Actions {
|
||||
pub fn new(disable_config_exec: bool, refresh_time: f64) -> Self {
|
||||
Self {
|
||||
disable_config_exec,
|
||||
status: HashMap::new(),
|
||||
// Avoid any trigger when Glances is started (issue #732).
|
||||
start_timer: Timer::new(refresh_time * 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, stat_name: &str, criticality: &str) {
|
||||
self.status.insert(stat_name.to_string(), criticality.to_string());
|
||||
}
|
||||
|
||||
pub fn get(&self, stat_name: &str) -> Option<&str> {
|
||||
self.status.get(stat_name).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Run the commands (in background in Python; synchronously here).
|
||||
pub fn run(
|
||||
&mut self,
|
||||
stat_name: &str,
|
||||
criticality: &str,
|
||||
commands: &[String],
|
||||
repeat: bool,
|
||||
mustache_dict: Option<&Value>,
|
||||
) -> bool {
|
||||
if (self.get(stat_name) == Some(criticality) && !repeat) || !self.start_timer.finished() {
|
||||
return false;
|
||||
}
|
||||
tracing::debug!(
|
||||
"{} action {:?} for {} ({}) with stats {:?}",
|
||||
if repeat { "Repeat" } else { "Run" },
|
||||
commands,
|
||||
stat_name,
|
||||
criticality,
|
||||
mustache_dict
|
||||
);
|
||||
|
||||
let safe = mustache_dict.map(sanitize_value);
|
||||
let render = |arg: &str| -> Result<String, String> {
|
||||
if let Some(safe) = &safe {
|
||||
render_template(arg, safe)
|
||||
} else {
|
||||
Ok(arg.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
for cmd in commands {
|
||||
tracing::info!("Action triggered for {stat_name} ({criticality}): {cmd}");
|
||||
let ret = secure_popen(cmd, !self.disable_config_exec, Some(&render));
|
||||
tracing::debug!("Action result for {stat_name} ({criticality}): {ret}");
|
||||
}
|
||||
self.set(stat_name, criticality);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_escape() {
|
||||
let data = json!({"name": "a'b"});
|
||||
assert_eq!(render_template("{{name}}", &data).unwrap(), "a'b");
|
||||
assert_eq!(render_template("{{{name}}}", &data).unwrap(), "a'b");
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
//! Command line arguments (drop-in compatible with Glances standalone mode).
|
||||
//!
|
||||
//! Flags belonging to intentionally unsupported features (web server, client/
|
||||
//! server, exporters...) are still accepted so that the CLI can fail with a
|
||||
//! clear message instead of an "unknown argument" error.
|
||||
|
||||
use clap::{ArgAction, Parser, ValueEnum};
|
||||
|
||||
pub const GLANCES_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const GLANCES_API_VERSION: &str = "4";
|
||||
|
||||
/// Available process sort keys.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum SortProcesses {
|
||||
#[value(name = "cpu_percent")]
|
||||
CpuPercent,
|
||||
#[value(name = "memory_percent")]
|
||||
MemoryPercent,
|
||||
#[value(name = "username")]
|
||||
Username,
|
||||
#[value(name = "cpu_times")]
|
||||
CpuTimes,
|
||||
#[value(name = "io_counters")]
|
||||
IoCounters,
|
||||
#[value(name = "name")]
|
||||
Name,
|
||||
#[value(name = "cpu_num")]
|
||||
CpuNum,
|
||||
}
|
||||
|
||||
impl SortProcesses {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
SortProcesses::CpuPercent => "cpu_percent",
|
||||
SortProcesses::MemoryPercent => "memory_percent",
|
||||
SortProcesses::Username => "username",
|
||||
SortProcesses::CpuTimes => "cpu_times",
|
||||
SortProcesses::IoCounters => "io_counters",
|
||||
SortProcesses::Name => "name",
|
||||
SortProcesses::CpuNum => "cpu_num",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(
|
||||
name = "glances",
|
||||
disable_version_flag = true,
|
||||
after_help = EXAMPLES
|
||||
)]
|
||||
pub struct Args {
|
||||
/// enable debug mode
|
||||
#[arg(short = 'd', long = "debug", action = ArgAction::SetTrue)]
|
||||
pub debug: bool,
|
||||
|
||||
/// display the version and exit
|
||||
#[arg(short = 'V', long = "version", action = ArgAction::SetTrue)]
|
||||
pub version: bool,
|
||||
|
||||
/// path to the configuration file
|
||||
#[arg(short = 'C', long = "config", value_name = "FILE")]
|
||||
pub conf_file: Option<String>,
|
||||
|
||||
/// path to additional plugin directory (Rust plugins only; Python plugins are not supported)
|
||||
#[arg(short = 'P', long = "plugins", value_name = "DIR")]
|
||||
pub plugin_dir: Option<String>,
|
||||
|
||||
/// display modules (plugins & exports) list and exit
|
||||
#[arg(long = "modules-list", visible_alias = "module-list", action = ArgAction::SetTrue)]
|
||||
pub modules_list: bool,
|
||||
|
||||
/// disable plugin (comma-separated list or all)
|
||||
#[arg(
|
||||
long = "disable-plugin",
|
||||
visible_aliases = ["disable-plugins", "disable"],
|
||||
value_name = "PLUGINS"
|
||||
)]
|
||||
pub disable_plugin: Option<String>,
|
||||
|
||||
/// enable plugin (comma-separated list)
|
||||
#[arg(
|
||||
long = "enable-plugin",
|
||||
visible_aliases = ["enable-plugins", "enable"],
|
||||
value_name = "PLUGINS"
|
||||
)]
|
||||
pub enable_plugin: Option<String>,
|
||||
|
||||
/// disable process module
|
||||
#[arg(long = "disable-process", action = ArgAction::SetTrue)]
|
||||
pub disable_process: bool,
|
||||
|
||||
/// light mode for Curses UI (disable all but the top menu)
|
||||
#[arg(long = "light", visible_alias = "enable-light", action = ArgAction::SetTrue)]
|
||||
pub enable_light: bool,
|
||||
|
||||
/// task's cpu usage will be divided by the total number of CPUs
|
||||
#[arg(short = '0', long = "disable-irix", action = ArgAction::SetTrue)]
|
||||
pub disable_irix: bool,
|
||||
|
||||
/// start Glances in per CPU mode
|
||||
#[arg(short = '1', long = "percpu", visible_alias = "per-cpu", action = ArgAction::SetTrue)]
|
||||
pub percpu: bool,
|
||||
|
||||
/// disable network, disk I/O, FS and sensors modules
|
||||
#[arg(short = '2', long = "disable-left-sidebar", action = ArgAction::SetTrue)]
|
||||
pub disable_left_sidebar: bool,
|
||||
|
||||
/// disable quick look module
|
||||
#[arg(short = '3', long = "disable-quicklook", action = ArgAction::SetTrue)]
|
||||
pub disable_quicklook: bool,
|
||||
|
||||
/// disable all but quick look and load
|
||||
#[arg(short = '4', long = "full-quicklook", action = ArgAction::SetTrue)]
|
||||
pub full_quicklook: bool,
|
||||
|
||||
/// disable top menu (QL, CPU, MEM, SWAP and LOAD)
|
||||
#[arg(short = '5', long = "disable-top", action = ArgAction::SetTrue)]
|
||||
pub disable_top: bool,
|
||||
|
||||
/// start Glances in mean GPU mode
|
||||
#[arg(short = '6', long = "meangpu", action = ArgAction::SetTrue)]
|
||||
pub meangpu: bool,
|
||||
|
||||
/// disable stats history
|
||||
#[arg(long = "disable-history", action = ArgAction::SetTrue)]
|
||||
pub disable_history: bool,
|
||||
|
||||
/// disable bold mode in the terminal
|
||||
#[arg(long = "disable-bold", action = ArgAction::SetTrue)]
|
||||
pub disable_bold: bool,
|
||||
|
||||
/// disable background colors in the terminal
|
||||
#[arg(long = "disable-bg", action = ArgAction::SetTrue)]
|
||||
pub disable_bg: bool,
|
||||
|
||||
/// enable IRQ module
|
||||
#[arg(long = "enable-irq", action = ArgAction::SetTrue)]
|
||||
pub enable_irq: bool,
|
||||
|
||||
/// enable extended stats on top process
|
||||
#[arg(long = "enable-process-extended", action = ArgAction::SetTrue)]
|
||||
pub enable_process_extended: bool,
|
||||
|
||||
/// disable separator in the UI (between top and others modules)
|
||||
#[arg(long = "disable-separator", action = ArgAction::SetTrue)]
|
||||
pub disable_separator: bool,
|
||||
|
||||
/// disable cursor (process selection) in the UI
|
||||
#[arg(long = "disable-cursor", action = ArgAction::SetTrue)]
|
||||
pub disable_cursor: bool,
|
||||
|
||||
/// Use arrow keys to sort the process list instead of the SHIFT+key combinations
|
||||
#[arg(long = "arrow-keys-sort", action = ArgAction::SetTrue)]
|
||||
pub arrow_keys_sort: bool,
|
||||
|
||||
/// Sort processes by: cpu_percent, memory_percent, username, cpu_times, io_counters, name, cpu_num
|
||||
#[arg(long = "sort-processes", value_name = "KEY")]
|
||||
pub sort_processes_key: Option<SortProcesses>,
|
||||
|
||||
/// Accumulate processes by program
|
||||
#[arg(long = "programs", visible_alias = "program", action = ArgAction::SetTrue)]
|
||||
pub programs: bool,
|
||||
|
||||
/// stop Glances after n refresh
|
||||
#[arg(long = "stop-after", value_name = "N")]
|
||||
pub stop_after: Option<i64>,
|
||||
|
||||
/// do not display the curses interface
|
||||
#[arg(short = 'q', long = "quiet", action = ArgAction::SetTrue)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// set the process filter pattern (regular expression)
|
||||
#[arg(short = 'f', long = "process-filter", value_name = "PATTERN")]
|
||||
pub process_filter: Option<String>,
|
||||
|
||||
/// set a process list to focus on (comma-separated list of Glances filter)
|
||||
#[arg(long = "process-focus", value_name = "FILTER")]
|
||||
pub process_focus: Option<String>,
|
||||
|
||||
/// force short name for processes name
|
||||
#[arg(long = "process-short-name", overrides_with = "process_long_name", action = ArgAction::SetTrue)]
|
||||
pub process_short_name_flag: bool,
|
||||
|
||||
/// force long name for processes name
|
||||
#[arg(long = "process-long-name", overrides_with = "process_short_name_flag", action = ArgAction::SetTrue)]
|
||||
pub process_long_name: bool,
|
||||
|
||||
/// display stats to stdout, one stat per line (comma-separated list of plugins/plugins.attribute)
|
||||
#[arg(long = "stdout", value_name = "PLUGINS")]
|
||||
pub stdout: Option<String>,
|
||||
|
||||
/// display stats to stdout, JSON format (comma-separated list of plugins/plugins.attribute)
|
||||
#[arg(long = "stdout-json", value_name = "PLUGINS")]
|
||||
pub stdout_json: Option<String>,
|
||||
|
||||
/// display stats to stdout, CSV format (comma-separated list of plugins/plugins.attribute)
|
||||
#[arg(long = "stdout-csv", value_name = "PLUGINS")]
|
||||
pub stdout_csv: Option<String>,
|
||||
|
||||
/// test all plugins and exit (please copy/paste the output if you open an issue)
|
||||
#[arg(long = "issue", action = ArgAction::SetTrue)]
|
||||
pub stdout_issue: bool,
|
||||
|
||||
/// test memory leak (quiet mode during 60 seconds)
|
||||
#[arg(long = "memory-leak", action = ArgAction::SetTrue)]
|
||||
pub memory_leak: bool,
|
||||
|
||||
/// hide kernel threads in the process list (not available on Windows)
|
||||
#[arg(long = "hide-kernel-threads", action = ArgAction::SetTrue)]
|
||||
pub no_kernel_threads: bool,
|
||||
|
||||
/// display network rate in bytes per second
|
||||
#[arg(short = 'b', long = "byte", action = ArgAction::SetTrue)]
|
||||
pub byte: bool,
|
||||
|
||||
/// show RAM Fs in the DiskIO plugin
|
||||
#[arg(long = "diskio-show-ramfs", action = ArgAction::SetTrue)]
|
||||
pub diskio_show_ramfs: bool,
|
||||
|
||||
/// show IO per second in the DiskIO plugin
|
||||
#[arg(long = "diskio-iops", action = ArgAction::SetTrue)]
|
||||
pub diskio_iops: bool,
|
||||
|
||||
/// show IO latency in the DiskIO plugin
|
||||
#[arg(long = "diskio-latency", action = ArgAction::SetTrue)]
|
||||
pub diskio_latency: bool,
|
||||
|
||||
/// display temperature in Fahrenheit (default is Celsius)
|
||||
#[arg(long = "fahrenheit", action = ArgAction::SetTrue)]
|
||||
pub fahrenheit: bool,
|
||||
|
||||
/// display FS free space instead of used
|
||||
#[arg(long = "fs-free-space", action = ArgAction::SetTrue)]
|
||||
pub fs_free_space: bool,
|
||||
|
||||
/// display sparklines instead of bar in the curses interface
|
||||
#[arg(long = "sparkline", action = ArgAction::SetTrue)]
|
||||
pub sparkline: bool,
|
||||
|
||||
/// disable unicode characters in the curses interface
|
||||
#[arg(long = "disable-unicode", action = ArgAction::SetTrue)]
|
||||
pub disable_unicode: bool,
|
||||
|
||||
/// hide public information (like public IP)
|
||||
#[arg(long = "hide-public-info", action = ArgAction::SetTrue)]
|
||||
pub hide_public_info: bool,
|
||||
|
||||
/// disable backtick command execution in configuration values and shell operator interpretation (&&, |, >) in AMP commands
|
||||
#[arg(long = "disable-config-exec", action = ArgAction::SetTrue)]
|
||||
pub disable_config_exec: bool,
|
||||
|
||||
/// disable online Glances version check
|
||||
#[arg(long = "disable-check-update", action = ArgAction::SetTrue)]
|
||||
pub disable_check_update: bool,
|
||||
|
||||
/// strftime format string for displaying current date in standalone mode
|
||||
#[arg(long = "strftime", value_name = "FORMAT", default_value = "")]
|
||||
pub strftime_format: String,
|
||||
|
||||
/// set minimum refresh rate in seconds [default: 2 sec]
|
||||
#[arg(short = 't', long = "time", value_name = "SECOND")]
|
||||
pub time: Option<f64>,
|
||||
|
||||
/// display a (neo)fetch like summary and exit
|
||||
#[arg(long = "fetch", visible_alias = "stdout-fetch", action = ArgAction::SetTrue)]
|
||||
pub stdout_fetch: bool,
|
||||
|
||||
/// overwrite default fetch template file
|
||||
#[arg(
|
||||
long = "fetch-template",
|
||||
visible_alias = "stdout-fetch-template",
|
||||
value_name = "FILE",
|
||||
default_value = ""
|
||||
)]
|
||||
pub fetch_template: String,
|
||||
|
||||
/// print shell completion and exit (bash, zsh or fish)
|
||||
#[arg(long = "print-completion", value_name = "SHELL")]
|
||||
pub print_completion: Option<String>,
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Unsupported features: accepted for a clear error message.
|
||||
// ------------------------------------------------------------------
|
||||
#[arg(short = 'w', long = "webserver", action = ArgAction::SetTrue, hide = true)]
|
||||
pub webserver: bool,
|
||||
#[arg(long = "disable-webui", action = ArgAction::SetTrue, hide = true)]
|
||||
pub disable_webui: bool,
|
||||
#[arg(long = "enable-mcp", action = ArgAction::SetTrue, hide = true)]
|
||||
pub enable_mcp: bool,
|
||||
#[arg(long = "mcp-path", value_name = "PATH", hide = true)]
|
||||
pub mcp_path: Option<String>,
|
||||
#[arg(long = "open-web-browser", action = ArgAction::SetTrue, hide = true)]
|
||||
pub open_web_browser: bool,
|
||||
#[arg(short = 'c', long = "client", value_name = "SERVER", hide = true)]
|
||||
pub client: Option<String>,
|
||||
#[arg(short = 's', long = "server", action = ArgAction::SetTrue, hide = true)]
|
||||
pub server: bool,
|
||||
#[arg(long = "browser", action = ArgAction::SetTrue, hide = true)]
|
||||
pub browser: bool,
|
||||
#[arg(long = "disable-autodiscover", action = ArgAction::SetTrue, hide = true)]
|
||||
pub disable_autodiscover: bool,
|
||||
#[arg(short = 'p', long = "port", value_name = "PORT", hide = true)]
|
||||
pub port: Option<i64>,
|
||||
#[arg(short = 'B', long = "bind", value_name = "ADDRESS", hide = true)]
|
||||
pub bind_address: Option<String>,
|
||||
#[arg(short = 'u', value_name = "USERNAME", hide = true)]
|
||||
pub username_used: Option<String>,
|
||||
#[arg(long = "username", action = ArgAction::SetTrue, hide = true)]
|
||||
pub username_prompt: bool,
|
||||
#[arg(long = "password", action = ArgAction::SetTrue, hide = true)]
|
||||
pub password_prompt: bool,
|
||||
#[arg(long = "snmp-community", value_name = "COMMUNITY", hide = true)]
|
||||
pub snmp_community: Option<String>,
|
||||
#[arg(long = "snmp-port", value_name = "PORT", hide = true)]
|
||||
pub snmp_port: Option<i64>,
|
||||
#[arg(long = "snmp-version", value_name = "VERSION", hide = true)]
|
||||
pub snmp_version: Option<String>,
|
||||
#[arg(long = "snmp-user", value_name = "USER", hide = true)]
|
||||
pub snmp_user: Option<String>,
|
||||
#[arg(long = "snmp-auth", value_name = "AUTH", hide = true)]
|
||||
pub snmp_auth: Option<String>,
|
||||
#[arg(long = "snmp-force", action = ArgAction::SetTrue, hide = true)]
|
||||
pub snmp_force: bool,
|
||||
#[arg(long = "cached-time", value_name = "SEC", hide = true)]
|
||||
pub cached_time: Option<i64>,
|
||||
#[arg(long = "export", value_name = "EXPORTERS", hide = true)]
|
||||
pub export: Option<String>,
|
||||
#[arg(long = "export-csv-file", value_name = "FILE", hide = true)]
|
||||
pub export_csv_file: Option<String>,
|
||||
#[arg(long = "export-csv-overwrite", action = ArgAction::SetTrue, hide = true)]
|
||||
pub export_csv_overwrite: bool,
|
||||
#[arg(long = "export-json-file", value_name = "FILE", hide = true)]
|
||||
pub export_json_file: Option<String>,
|
||||
#[arg(long = "export-graph-path", value_name = "DIR", hide = true)]
|
||||
pub export_graph_path: Option<String>,
|
||||
#[arg(long = "export-process-filter", value_name = "FILTER", hide = true)]
|
||||
pub export_process_filter: Option<String>,
|
||||
#[arg(long = "api-doc", action = ArgAction::SetTrue, hide = true)]
|
||||
pub stdout_api_doc: bool,
|
||||
#[arg(long = "api-restful-doc", action = ArgAction::SetTrue, hide = true)]
|
||||
pub stdout_api_restful_doc: bool,
|
||||
#[arg(long = "trace-malloc", action = ArgAction::SetTrue, hide = true)]
|
||||
pub trace_malloc: bool,
|
||||
}
|
||||
|
||||
const EXAMPLES: &str = "\
|
||||
Examples of use:
|
||||
Monitor local machine (standalone mode):
|
||||
$ glances
|
||||
|
||||
Display all Glances modules (plugins) and exit:
|
||||
$ glances --module-list
|
||||
|
||||
Display stats to stdout (one stat per line, possible to go inside stats using plugin.attribute):
|
||||
$ glances --stdout now,cpu.user,mem.used,load
|
||||
|
||||
Display JSON stats to stdout (one stats per line):
|
||||
$ glances --stdout-json now,cpu,mem,load
|
||||
|
||||
Display CSV stats to stdout (all stats in one line):
|
||||
$ glances --stdout-csv now,cpu.user,mem.used,load
|
||||
|
||||
Enable some plugins disabled by default (comma-separated list):
|
||||
$ glances --enable-plugin sensors
|
||||
|
||||
Disable some plugins (comma-separated list):
|
||||
$ glances --disable-plugin network,ports
|
||||
|
||||
Disable all plugins except some (comma-separated list):
|
||||
$ glances --disable-plugin all --enable-plugin cpu,mem,load";
|
||||
|
||||
impl Args {
|
||||
/// Return the reason why this command line can not be handled, if any.
|
||||
pub fn unsupported(&self) -> Option<&'static str> {
|
||||
let checks: &[(bool, &'static str)] = &[
|
||||
(self.webserver, "web server mode (-w/--webserver)"),
|
||||
(self.enable_mcp, "the MCP server (--enable-mcp)"),
|
||||
(self.client.is_some(), "client mode (-c/--client)"),
|
||||
(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.trace_malloc, "Python memory tracing (--trace-malloc)"),
|
||||
];
|
||||
checks.iter().find(|(used, _)| *used).map(|(_, name)| *name)
|
||||
}
|
||||
|
||||
pub fn process_short_name(&self) -> bool {
|
||||
!self.process_long_name
|
||||
}
|
||||
|
||||
pub fn enable_separator(&self) -> bool {
|
||||
!self.disable_separator
|
||||
}
|
||||
|
||||
/// Python-compatible version message.
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(argv: &[&str]) -> Args {
|
||||
Args::try_parse_from(argv).expect("args should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kept_flags_parse() {
|
||||
let args = parse(&[
|
||||
"glances",
|
||||
"-d",
|
||||
"-C",
|
||||
"/tmp/glances.conf",
|
||||
"--disable-plugin",
|
||||
"all",
|
||||
"--enable-plugin",
|
||||
"cpu,mem",
|
||||
"--light",
|
||||
"-0",
|
||||
"-1",
|
||||
"-2",
|
||||
"-3",
|
||||
"-4",
|
||||
"-5",
|
||||
"-6",
|
||||
"--disable-history",
|
||||
"--disable-bold",
|
||||
"--disable-bg",
|
||||
"--enable-irq",
|
||||
"--enable-process-extended",
|
||||
"--disable-separator",
|
||||
"--disable-cursor",
|
||||
"--arrow-keys-sort",
|
||||
"--sort-processes",
|
||||
"memory_percent",
|
||||
"--programs",
|
||||
"--stop-after",
|
||||
"3",
|
||||
"-q",
|
||||
"-f",
|
||||
"python",
|
||||
"--process-focus",
|
||||
"user:root",
|
||||
"--process-long-name",
|
||||
"--stdout",
|
||||
"cpu,mem",
|
||||
"--stdout-json",
|
||||
"cpu",
|
||||
"--stdout-csv",
|
||||
"cpu",
|
||||
"--issue",
|
||||
"--hide-kernel-threads",
|
||||
"-b",
|
||||
"--diskio-show-ramfs",
|
||||
"--diskio-iops",
|
||||
"--diskio-latency",
|
||||
"--fahrenheit",
|
||||
"--fs-free-space",
|
||||
"--sparkline",
|
||||
"--disable-unicode",
|
||||
"--hide-public-info",
|
||||
"--disable-config-exec",
|
||||
"--disable-check-update",
|
||||
"--strftime",
|
||||
"%Y",
|
||||
"-t",
|
||||
"5",
|
||||
"--fetch",
|
||||
"--fetch-template",
|
||||
"/tmp/tpl",
|
||||
"--print-completion",
|
||||
"bash",
|
||||
]);
|
||||
assert!(args.export.is_none());
|
||||
assert!(!args.process_short_name());
|
||||
assert_eq!(args.sort_processes_key, Some(SortProcesses::MemoryPercent));
|
||||
assert_eq!(args.time, Some(5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_flags_detected() {
|
||||
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"]).unsupported().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_flag_parses() {
|
||||
assert!(parse(&["glances", "-V"]).version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
//! Configuration file handling (drop-in compatible with `glances.conf`).
|
||||
//!
|
||||
//! The file format is the Python `ConfigParser` INI dialect: sections in
|
||||
//! brackets, `key = value` or `key: value` options, `#`/`;` comments and
|
||||
//! indented continuation lines. Option names are lower-cased; section names
|
||||
//! are kept as is.
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub const CONFIG_FILENAME: &str = "glances.conf";
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
sections: IndexMap<String, IndexMap<String, String>>,
|
||||
loaded_file: Option<PathBuf>,
|
||||
disable_config_exec: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Build the configuration by searching the standard paths (unless a
|
||||
/// custom `-C` path is given) and reading the first existing file.
|
||||
pub fn new(config_dir: Option<&str>, disable_config_exec: bool) -> Self {
|
||||
let mut config = Config {
|
||||
sections: IndexMap::new(),
|
||||
loaded_file: None,
|
||||
disable_config_exec,
|
||||
};
|
||||
config.read(config_dir);
|
||||
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;
|
||||
config.set_defaults();
|
||||
config
|
||||
}
|
||||
|
||||
/// Parse a configuration from a string (used by tests).
|
||||
pub fn from_str(content: &str, disable_config_exec: bool) -> Self {
|
||||
let mut config = Config {
|
||||
sections: parse_ini(content),
|
||||
loaded_file: None,
|
||||
disable_config_exec,
|
||||
};
|
||||
config.set_defaults();
|
||||
config
|
||||
}
|
||||
|
||||
fn config_file_paths(config_dir: Option<&str>) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// Custom path given with -C.
|
||||
if let Some(dir) = config_dir {
|
||||
paths.push(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
// User config dir: ~/.config/glances
|
||||
let user_base = std::env::var("XDG_CONFIG_HOME")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.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));
|
||||
}
|
||||
|
||||
// System-wide config dir: /etc/glances
|
||||
paths.push(PathBuf::from("/etc/glances").join(CONFIG_FILENAME));
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn read(&mut self, config_dir: Option<&str>) {
|
||||
for config_file in Self::config_file_paths(config_dir) {
|
||||
if config_file.is_file() {
|
||||
match std::fs::read_to_string(&config_file) {
|
||||
Ok(content) => {
|
||||
self.sections = parse_ini(&content);
|
||||
self.loaded_file = Some(config_file.clone());
|
||||
tracing::info!("Read configuration file '{}'", config_file.display());
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Can not read configuration file '{}': {err}", config_file.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.set_defaults();
|
||||
}
|
||||
|
||||
/// Fill in the default values exactly like the Python implementation.
|
||||
fn set_defaults(&mut self) {
|
||||
// Globals
|
||||
self.set_default("global", "strftime_format", "");
|
||||
self.set_default("global", "check_update", "true");
|
||||
|
||||
// Quicklook
|
||||
self.set_default_cwc("quicklook", Some("cpu"), ["50", "70", "90"]);
|
||||
self.set_default_cwc("quicklook", Some("mem"), ["50", "70", "90"]);
|
||||
self.set_default_cwc("quicklook", Some("swap"), ["50", "70", "90"]);
|
||||
|
||||
// CPU
|
||||
self.set_default_cwc("cpu", Some("user"), ["50", "70", "90"]);
|
||||
self.set_default_cwc("cpu", Some("system"), ["50", "70", "90"]);
|
||||
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 critical = format!("{ctx_switches_bottleneck}");
|
||||
self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]);
|
||||
|
||||
// Per-CPU
|
||||
self.set_default_cwc("percpu", Some("user"), ["50", "70", "90"]);
|
||||
self.set_default_cwc("percpu", Some("system"), ["50", "70", "90"]);
|
||||
|
||||
// Load
|
||||
self.set_default_cwc("load", None, ["0.7", "1.0", "5.0"]);
|
||||
|
||||
// Mem
|
||||
self.set_default_cwc("mem", None, ["50", "70", "90"]);
|
||||
|
||||
// Swap
|
||||
self.set_default_cwc("memswap", None, ["50", "70", "90"]);
|
||||
|
||||
// Network
|
||||
self.set_default_cwc("network", Some("rx"), ["50", "70", "90"]);
|
||||
self.set_default_cwc("network", Some("tx"), ["50", "70", "90"]);
|
||||
|
||||
// FS
|
||||
self.set_default_cwc("fs", None, ["50", "70", "90"]);
|
||||
|
||||
// Sensors
|
||||
self.set_default_cwc("sensors", Some("temperature_hdd"), ["45", "52", "60"]);
|
||||
self.set_default_cwc("sensors", Some("battery"), ["70", "80", "90"]);
|
||||
|
||||
// Process list
|
||||
self.set_default_cwc("processlist", Some("cpu"), ["50", "70", "90"]);
|
||||
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]) {
|
||||
let header = match option_header {
|
||||
Some(header) => format!("{header}_"),
|
||||
None => String::new(),
|
||||
};
|
||||
self.set_default(section, &format!("{header}careful"), cwc[0]);
|
||||
self.set_default(section, &format!("{header}warning"), cwc[1]);
|
||||
self.set_default(section, &format!("{header}critical"), cwc[2]);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
pub fn loaded_config_file(&self) -> Option<&Path> {
|
||||
self.loaded_file.as_deref()
|
||||
}
|
||||
|
||||
/// List all section names.
|
||||
pub fn sections(&self) -> Vec<&str> {
|
||||
self.sections.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
pub fn has_section(&self, section: &str) -> bool {
|
||||
self.sections.contains_key(section)
|
||||
}
|
||||
|
||||
pub fn has_option(&self, section: &str, option: &str) -> bool {
|
||||
self.sections
|
||||
.get(section)
|
||||
.map(|s| s.contains_key(&option.to_lowercase()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// List `(key, value)` pairs of a section.
|
||||
pub fn items(&self, section: &str) -> Vec<(String, String)> {
|
||||
self.sections
|
||||
.get(section)
|
||||
.map(|s| s.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get a raw value. Dynamic values between backticks are executed unless
|
||||
/// `--disable-config-exec` is set.
|
||||
pub fn get_value(&self, section: &str, option: &str, default: Option<&str>) -> Option<String> {
|
||||
let mut ret = self
|
||||
.sections
|
||||
.get(section)
|
||||
.and_then(|s| s.get(&option.to_lowercase()))
|
||||
.cloned()
|
||||
.or_else(|| default.map(|d| d.to_string()));
|
||||
|
||||
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}");
|
||||
} else {
|
||||
tracing::warn!("Executing config command `{command}` for [{section}] {option}");
|
||||
let output = system_exec(&command);
|
||||
*value = value.replace(&format!("`{command}`"), &output);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_list_value(
|
||||
&self,
|
||||
section: &str,
|
||||
option: &str,
|
||||
default: Option<Vec<String>>,
|
||||
separator: char,
|
||||
) -> Option<Vec<String>> {
|
||||
self.get_value(section, option, None)
|
||||
.map(|v| v.split(separator).map(|s| s.to_string()).collect())
|
||||
.or(default)
|
||||
}
|
||||
|
||||
pub fn get_int_value(&self, section: &str, option: &str, default: i64) -> i64 {
|
||||
self.get_value(section, option, None)
|
||||
.and_then(|v| v.trim().parse::<i64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn get_float_value(&self, section: &str, option: &str, default: f64) -> f64 {
|
||||
self.get_value(section, option, None)
|
||||
.and_then(|v| v.trim().parse::<f64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn get_bool_value(&self, section: &str, option: &str, default: bool) -> bool {
|
||||
match self.get_value(section, option, None) {
|
||||
Some(v) => parse_bool(&v).unwrap_or(default),
|
||||
None => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the true/false flavours accepted by Python's `ConfigParser`.
|
||||
pub fn parse_bool(value: &str) -> Option<bool> {
|
||||
match value.trim().to_lowercase().as_str() {
|
||||
"1" | "yes" | "true" | "on" => Some(true),
|
||||
"0" | "no" | "false" | "off" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ini(content: &str) -> IndexMap<String, IndexMap<String, String>> {
|
||||
let mut sections: IndexMap<String, IndexMap<String, String>> = IndexMap::new();
|
||||
let mut current_section: Option<String> = None;
|
||||
let mut last_key: Option<String> = None;
|
||||
|
||||
for raw_line in content.lines() {
|
||||
let line = raw_line.trim_end();
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
// Blank line: keep the last key for continuation values.
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Comment.
|
||||
if trimmed.starts_with('#') || trimmed.starts_with(';') {
|
||||
continue;
|
||||
}
|
||||
// Section header.
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.starts_with("[[") {
|
||||
let name = trimmed[1..trimmed.len() - 1].trim().to_string();
|
||||
sections.entry(name.clone()).or_default();
|
||||
current_section = Some(name);
|
||||
last_key = None;
|
||||
continue;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Key/value.
|
||||
if let Some(pos) = trimmed.find(['=', ':']) {
|
||||
let key = trimmed[..pos].trim().to_lowercase();
|
||||
let value = trimmed[pos + 1..].trim();
|
||||
if let Some(section) = current_section.as_ref() {
|
||||
sections
|
||||
.entry(section.clone())
|
||||
.or_default()
|
||||
.insert(key.clone(), value.to_string());
|
||||
last_key = Some(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
sections
|
||||
}
|
||||
|
||||
fn find_backticks(value: &str) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let mut chars = value.char_indices().peekable();
|
||||
while let Some((start, c)) = chars.next() {
|
||||
if c == '`' {
|
||||
for (end, c2) in chars.by_ref() {
|
||||
if c2 == '`' {
|
||||
result.push(value[start + 1..end].to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute a system command without a shell and return its trimmed stdout.
|
||||
fn system_exec(command: &str) -> String {
|
||||
let parts: Vec<&str> = command.split(' ').collect();
|
||||
let Some((program, args)) = parts.split_first() else {
|
||||
return String::new();
|
||||
};
|
||||
match Command::new(program).args(args).output() {
|
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).trim_end().to_string(),
|
||||
Err(e) => format!("ERROR: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_ini() {
|
||||
let config = Config::from_str(
|
||||
"# comment\n[global]\nrefresh = 5\nhistory_size: 100\n\n[cpu]\nuser_careful=10\n",
|
||||
false,
|
||||
);
|
||||
assert!(config.has_section("global"));
|
||||
assert_eq!(config.get_int_value("global", "refresh", 2), 5);
|
||||
assert_eq!(config.get_int_value("global", "history_size", 0), 100);
|
||||
assert_eq!(config.get_float_value("cpu", "user_careful", 0.0), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_defaults() {
|
||||
let config = Config::empty(false);
|
||||
assert_eq!(config.get_float_value("cpu", "user_careful", 0.0), 50.0);
|
||||
assert_eq!(config.get_float_value("cpu", "user_warning", 0.0), 70.0);
|
||||
assert_eq!(config.get_float_value("cpu", "user_critical", 0.0), 90.0);
|
||||
assert_eq!(config.get_float_value("load", "careful", 0.0), 0.7);
|
||||
assert_eq!(config.get_float_value("load", "critical", 0.0), 5.0);
|
||||
assert!(config.has_section("quicklook"));
|
||||
assert!(config.has_section("processlist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool_values() {
|
||||
let config = Config::from_str("[global]\ncheck_update=off\n", false);
|
||||
assert!(!config.get_bool_value("global", "check_update", true));
|
||||
let config = Config::from_str("[x]\ndisable=True\n", false);
|
||||
assert!(config.get_bool_value("x", "disable", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backtick_exec() {
|
||||
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`");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
//! Events list (port of `glances.events_list` and `glances.event`).
|
||||
|
||||
use crate::processes::{processes, sort_stats};
|
||||
use crate::thresholds::thresholds;
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::Value;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Static decision tree for the global alert message.
|
||||
const TREE: [(&str, &[&str], u32); 9] = [
|
||||
("EVENTS history", &[], 0),
|
||||
("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),
|
||||
("High CPU niced value", &["cpu_niced"], 2),
|
||||
("System overloaded in the last 5 minutes", &["load"], 2),
|
||||
("High swap (paging) usage", &["memswap"], 2),
|
||||
("High memory consumption", &["mem"], 2),
|
||||
];
|
||||
|
||||
/// Parse the decision tree and return the message for the current thresholds.
|
||||
pub fn build_global_message() -> String {
|
||||
let guard = thresholds().lock().unwrap();
|
||||
let current = guard.all();
|
||||
let mut best_weight = 0u32;
|
||||
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();
|
||||
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() }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event {
|
||||
pub begin: f64,
|
||||
pub end: f64,
|
||||
pub state: String,
|
||||
pub r#type: String,
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub sum: f64,
|
||||
pub count: i64,
|
||||
pub avg: f64,
|
||||
pub top: Vec<String>,
|
||||
pub desc: String,
|
||||
pub sort: String,
|
||||
pub global_msg: String,
|
||||
top_dict: IndexMap<String, i64>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn is_ongoing(&self) -> bool {
|
||||
self.end == -1.0
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.end != -1.0
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
serde_json::json!({
|
||||
"begin": self.begin,
|
||||
"end": self.end,
|
||||
"state": self.state,
|
||||
"type": self.r#type,
|
||||
"min": self.min,
|
||||
"max": self.max,
|
||||
"sum": self.sum,
|
||||
"count": self.count,
|
||||
"avg": self.avg,
|
||||
"top": self.top,
|
||||
"desc": self.desc,
|
||||
"sort": self.sort,
|
||||
"global": self.global_msg,
|
||||
})
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
state: &str,
|
||||
value: f64,
|
||||
sort_key: &str,
|
||||
proc_list: &[Value],
|
||||
proc_desc: &str,
|
||||
global_msg: &str,
|
||||
) {
|
||||
self.end = -1.0;
|
||||
self.min = self.min.min(value);
|
||||
self.max = self.max.max(value);
|
||||
self.sum += value;
|
||||
self.count += 1;
|
||||
self.avg = self.sum / self.count as f64;
|
||||
|
||||
if state == "CRITICAL" {
|
||||
// Avoid changing from CRITICAL to WARNING.
|
||||
self.state = state.to_string();
|
||||
self.sort = sort_key.to_string();
|
||||
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();
|
||||
*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));
|
||||
self.top = counts.into_iter().take(3).map(|(name, _)| name).collect();
|
||||
} else {
|
||||
self.top_dict.clear();
|
||||
}
|
||||
self.desc = proc_desc.to_string();
|
||||
self.global_msg = global_msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventsList {
|
||||
pub events: Vec<Event>,
|
||||
pub max_events: usize,
|
||||
pub min_duration: f64,
|
||||
pub min_interval: f64,
|
||||
}
|
||||
|
||||
impl Default for EventsList {
|
||||
fn default() -> Self {
|
||||
Self::new(10, 6.0, 6.0)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Vec<Value> {
|
||||
self.events.iter().map(Event::to_json).collect()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.events.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.events.is_empty()
|
||||
}
|
||||
|
||||
fn event_exist(&self, event_time: f64, event_type: &str) -> Option<usize> {
|
||||
self.events.iter().position(|e| {
|
||||
(e.is_ongoing() || (event_time - e.end) < self.min_interval) && e.r#type == event_type
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_event_sort_key(event_type: &str) -> &'static str {
|
||||
if event_type.starts_with("MEM") {
|
||||
"memory_percent"
|
||||
} else if event_type.starts_with("CPU_IOWAIT") {
|
||||
"io_counters"
|
||||
} else {
|
||||
"cpu_percent"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_process_sort(event_type: &str) {
|
||||
let mut procs = processes().lock().unwrap();
|
||||
if procs.auto_sort {
|
||||
let key = Self::get_event_sort_key(event_type);
|
||||
procs.set_sort_key(key, true);
|
||||
procs.auto_sort = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_process_sort() {
|
||||
let mut procs = processes().lock().unwrap();
|
||||
if procs.auto_sort {
|
||||
procs.set_sort_key("auto", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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);
|
||||
}
|
||||
Some(index) => {
|
||||
self.update_event(
|
||||
event_time,
|
||||
index,
|
||||
event_state,
|
||||
event_type,
|
||||
event_value,
|
||||
&proc_list,
|
||||
proc_desc,
|
||||
&global_message,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn create_event(
|
||||
&mut self,
|
||||
event_time: f64,
|
||||
event_state: &str,
|
||||
event_type: &str,
|
||||
event_value: f64,
|
||||
proc_desc: &str,
|
||||
global_message: &str,
|
||||
) {
|
||||
if event_state != "WARNING" && event_state != "CRITICAL" {
|
||||
return;
|
||||
}
|
||||
Self::set_process_sort(event_type);
|
||||
let sort = processes().lock().unwrap().sort_key.clone();
|
||||
let event = Event {
|
||||
begin: event_time,
|
||||
end: -1.0,
|
||||
state: event_state.to_string(),
|
||||
r#type: event_type.to_string(),
|
||||
min: event_value,
|
||||
max: event_value,
|
||||
sum: event_value,
|
||||
count: 1,
|
||||
avg: event_value,
|
||||
top: Vec::new(),
|
||||
desc: proc_desc.to_string(),
|
||||
sort,
|
||||
global_msg: global_message.to_string(),
|
||||
top_dict: IndexMap::new(),
|
||||
};
|
||||
self.events.insert(0, event);
|
||||
if self.len() > self.max_events {
|
||||
self.events.pop();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_event(
|
||||
&mut self,
|
||||
event_time: f64,
|
||||
event_index: usize,
|
||||
event_state: &str,
|
||||
event_type: &str,
|
||||
event_value: f64,
|
||||
proc_list: &[Value],
|
||||
proc_desc: &str,
|
||||
global_message: &str,
|
||||
) {
|
||||
let is_ongoing = self.events[event_index].is_ongoing();
|
||||
if (event_state == "OK" || event_state == "CAREFUL") && is_ongoing {
|
||||
self.close_event(event_time, event_index);
|
||||
} else if event_state == "OK" || event_state == "CAREFUL" {
|
||||
// Event is already closed, do nothing.
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn close_event(&mut self, event_time: f64, event_index: usize) {
|
||||
Self::reset_process_sort();
|
||||
if event_time - self.events[event_index].begin >= self.min_duration {
|
||||
self.events[event_index].end = event_time;
|
||||
} else {
|
||||
self.events.remove(event_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean the events list by deleting finished items.
|
||||
pub fn clean(&mut self, critical: bool) -> usize {
|
||||
let mut clean_events = Vec::new();
|
||||
while let Some(event) = self.events.pop() {
|
||||
if event.end < 0.0 || (!critical && event.state.starts_with("CRITICAL")) {
|
||||
clean_events.insert(0, event);
|
||||
}
|
||||
}
|
||||
self.events = clean_events;
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Global events list shared by all plugins.
|
||||
pub fn events() -> &'static Mutex<EventsList> {
|
||||
static EVENTS: OnceLock<Mutex<EventsList>> = OnceLock::new();
|
||||
EVENTS.get_or_init(|| Mutex::new(EventsList::default()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_events_add() {
|
||||
let mut events = EventsList::new(10, 0.0, 6.0);
|
||||
events.add("WARNING", "CPU", 42.0, "");
|
||||
assert_eq!(events.len(), 1);
|
||||
let json = events.get();
|
||||
assert_eq!(json[0]["type"], "CPU");
|
||||
assert_eq!(json[0]["state"], "WARNING");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean() {
|
||||
let mut events = EventsList::new(10, 0.0, 6.0);
|
||||
events.add("WARNING", "CPU", 42.0, "");
|
||||
events.add("CRITICAL", "MEM", 80.0, "");
|
||||
// Close both events.
|
||||
events.events[0].end = 100.0;
|
||||
events.events[1].end = 100.0;
|
||||
// clean(false) removes the finished WARNING but keeps CRITICAL.
|
||||
assert_eq!(events.clean(false), 1);
|
||||
assert_eq!(events.get()[0]["state"], "CRITICAL");
|
||||
assert_eq!(events.clean(true), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Process filtering (port of `glances.filter`).
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Filter {
|
||||
filter_input: Option<String>,
|
||||
filter: Option<String>,
|
||||
filter_re: Option<Regex>,
|
||||
filter_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn filter_input(&self) -> Option<&str> {
|
||||
self.filter_input.as_deref()
|
||||
}
|
||||
|
||||
pub fn filter(&self) -> Option<&str> {
|
||||
self.filter.as_deref()
|
||||
}
|
||||
|
||||
pub fn filter_key(&self) -> Option<&str> {
|
||||
self.filter_key.as_deref()
|
||||
}
|
||||
|
||||
/// Set the filter (as a string) and compile the regular expression.
|
||||
pub fn set_filter(&mut self, value: Option<&str>) {
|
||||
self.filter_input = value.map(|v| v.to_string());
|
||||
match value {
|
||||
None => {
|
||||
self.filter = None;
|
||||
self.filter_key = None;
|
||||
}
|
||||
Some(value) => {
|
||||
// Split once: everything after the first colon is the regex.
|
||||
match value.split_once(':') {
|
||||
Some((key, rest)) => {
|
||||
self.filter = Some(rest.to_string());
|
||||
self.filter_key = Some(key.to_string());
|
||||
}
|
||||
None => {
|
||||
self.filter = Some(value.to_string());
|
||||
self.filter_key = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.filter_re = None;
|
||||
if let Some(filter) = &self.filter {
|
||||
tracing::debug!(
|
||||
"Set filter to {} on {}",
|
||||
filter,
|
||||
self.filter_key.as_deref().unwrap_or("name or cmdline")
|
||||
);
|
||||
match Regex::new(filter) {
|
||||
Ok(re) => self.filter_re = Some(re),
|
||||
Err(e) => {
|
||||
tracing::error!("Cannot compile filter regex: {filter} ({e})");
|
||||
self.filter = None;
|
||||
self.filter_re = None;
|
||||
self.filter_key = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return True if the process item matches the current filter.
|
||||
pub fn is_filtered(&self, process: &Value) -> bool {
|
||||
let Some(filter_re) = self.filter_re.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
if self.filter.is_none() {
|
||||
return false;
|
||||
}
|
||||
if self.filter_key.is_none() {
|
||||
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)
|
||||
}
|
||||
|
||||
fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool {
|
||||
let Some(value) = process.get(key) else {
|
||||
return false;
|
||||
};
|
||||
let text = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Array(items) => {
|
||||
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(" ")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FilterList {
|
||||
filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
impl FilterList {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.filters.is_empty()
|
||||
}
|
||||
|
||||
pub fn filters(&self) -> &[Filter] {
|
||||
&self.filters
|
||||
}
|
||||
|
||||
/// Set the filter list from a comma separated list of filters.
|
||||
pub fn set_filter(&mut self, value: Option<&str>) {
|
||||
self.filters.clear();
|
||||
if let Some(value) = value {
|
||||
for f in value.split(',') {
|
||||
let mut filter = Filter::default();
|
||||
filter.set_filter(Some(f));
|
||||
self.filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return True if the process is filtered by at least one filter.
|
||||
pub fn is_filtered(&self, process: &Value) -> bool {
|
||||
self.filters.iter().any(|f| f.is_filtered(process))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_name() {
|
||||
let mut f = Filter::default();
|
||||
f.set_filter(Some(".*python.*"));
|
||||
assert!(f.is_filtered(&json!({"name": "python3", "cmdline": ["python3"]})));
|
||||
assert!(!f.is_filtered(&json!({"name": "bash", "cmdline": ["bash"]})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_key() {
|
||||
let mut f = Filter::default();
|
||||
f.set_filter(Some("username:root"));
|
||||
assert!(f.is_filtered(&json!({"name": "x", "username": "root"})));
|
||||
assert!(!f.is_filtered(&json!({"name": "x", "username": "bob"})));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Stats history (port of `glances.attribute` / `glances.history`).
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// One historized attribute: a ring buffer of `(date, value)` tuples.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Attribute {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub value: Option<f64>,
|
||||
pub history: VecDeque<(DateTime<Utc>, f64)>,
|
||||
pub history_max_size: Option<usize>,
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub fn new(name: impl Into<String>, description: impl Into<String>, history_max_size: Option<usize>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
value: None,
|
||||
history: VecDeque::new(),
|
||||
history_max_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: f64) {
|
||||
self.value = Some(value);
|
||||
let now = Utc::now();
|
||||
if let Some(max) = self.history_max_size {
|
||||
if self.history.len() >= max {
|
||||
self.history.pop_front();
|
||||
}
|
||||
self.history.push_back((now, value));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn history_reset(&mut self) {
|
||||
self.history.clear();
|
||||
}
|
||||
|
||||
pub fn history_raw(&self, nb: usize) -> Vec<(DateTime<Utc>, f64)> {
|
||||
if nb == 0 {
|
||||
self.history.iter().cloned().collect()
|
||||
} else {
|
||||
self.history.iter().rev().take(nb).collect::<Vec<_>>().into_iter().rev().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn history_json(&self, nb: usize) -> Vec<(String, f64)> {
|
||||
self.history_raw(nb)
|
||||
.into_iter()
|
||||
.map(|(date, value)| (date.to_rfc3339(), value))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A dict of historized attributes.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct History {
|
||||
pub attributes: IndexMap<String, Attribute>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option<usize>) {
|
||||
let attribute = self
|
||||
.attributes
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(|| Attribute::new(key, description, history_max_size));
|
||||
attribute.set_value(value);
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
for attribute in self.attributes.values_mut() {
|
||||
attribute.history_reset();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_history_ring_buffer() {
|
||||
let mut history = History::default();
|
||||
for i in 0..5 {
|
||||
history.add("x", i as f64, "", Some(3));
|
||||
}
|
||||
let attr = history.attributes.get("x").unwrap();
|
||||
assert_eq!(attr.history.len(), 3);
|
||||
assert_eq!(attr.value, Some(4.0));
|
||||
let raw = history.get(0);
|
||||
assert_eq!(raw["x"].len(), 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! glances-core: shared foundations of the glances-rs rewrite.
|
||||
//!
|
||||
//! 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
|
||||
//! machinery and filtering.
|
||||
|
||||
pub mod actions;
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
pub mod filter;
|
||||
pub mod history;
|
||||
pub mod plugin;
|
||||
pub mod processes;
|
||||
pub mod secure;
|
||||
pub mod state;
|
||||
pub mod thresholds;
|
||||
pub mod timer;
|
||||
pub mod units;
|
||||
|
||||
pub use args::Args;
|
||||
pub use config::Config;
|
||||
pub use plugin::{
|
||||
Align, Decoration, Field, HistoryItem, Limit, Plugin, PluginModel, RenderContext, StatLine,
|
||||
UpdateContext,
|
||||
};
|
||||
pub use state::Runtime;
|
||||
pub use units::{auto_unit, py_repr, py_str, to_fahrenheit};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,719 @@
|
||||
//! Process collection and management (port of `glances.processes`).
|
||||
//!
|
||||
//! The process list is shared between the processlist, processcount,
|
||||
//! programlist, amps plugins and the event/action machinery, exactly like the
|
||||
//! `glances_processes` global in the Python implementation.
|
||||
|
||||
use crate::filter::{Filter, FilterList};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use sysinfo::{ProcessesToUpdate, System, Users};
|
||||
|
||||
/// Mandatory process stats which can not be disabled by the user.
|
||||
pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"];
|
||||
|
||||
/// Available process sort keys.
|
||||
pub const SORT_PROCESSES_STATS: [&str; 7] =
|
||||
["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
|
||||
|
||||
/// Sort keys as displayed to humans.
|
||||
pub fn sort_for_human(key: &str) -> &str {
|
||||
match key {
|
||||
"io_counters" => "disk IO",
|
||||
"cpu_percent" => "CPU consumption",
|
||||
"memory_percent" => "memory consumption",
|
||||
"cpu_times" => "process time",
|
||||
"username" => "user name",
|
||||
"name" => "processs name",
|
||||
"cpu_num" => "CPU core number",
|
||||
"auto" => "auto",
|
||||
_ => "None",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessCount {
|
||||
pub total: i64,
|
||||
pub running: i64,
|
||||
pub sleeping: i64,
|
||||
pub thread: i64,
|
||||
pub pid_max: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Processes {
|
||||
system: System,
|
||||
users: Users,
|
||||
users_timer: f64,
|
||||
|
||||
pub processlist: Vec<Value>,
|
||||
pub processlist_export: Vec<Value>,
|
||||
pub processcount: ProcessCount,
|
||||
|
||||
pub sort_key: String,
|
||||
pub auto_sort: bool,
|
||||
pub sort_reverse: bool,
|
||||
|
||||
pub disable_tag: bool,
|
||||
pub disable_extended_tag: bool,
|
||||
pub extended_process_pid: Option<i64>,
|
||||
pub extended_stats: Option<Value>,
|
||||
|
||||
pub no_kernel_threads: bool,
|
||||
pub disable_io_counters: bool,
|
||||
pub disable_gids: bool,
|
||||
pub disable_cpu_num: bool,
|
||||
pub disable_stats: Vec<String>,
|
||||
|
||||
/// List of processes to focus on.
|
||||
pub filter_focus: FilterList,
|
||||
/// Process filter (regular expression).
|
||||
pub filter: Filter,
|
||||
/// List of processes stats to export.
|
||||
pub filter_export: FilterList,
|
||||
|
||||
pub max_values: HashMap<String, f64>,
|
||||
io_old: HashMap<i64, [u64; 2]>,
|
||||
programs: Vec<Value>,
|
||||
programs_valid: bool,
|
||||
}
|
||||
|
||||
impl Default for Processes {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Processes {
|
||||
pub fn new() -> Self {
|
||||
let system = System::new();
|
||||
let users = Users::new_with_refreshed_list();
|
||||
let mut p = Self {
|
||||
system,
|
||||
users,
|
||||
users_timer: 0.0,
|
||||
processlist: Vec::new(),
|
||||
processlist_export: Vec::new(),
|
||||
processcount: ProcessCount::default(),
|
||||
sort_key: "cpu_percent".to_string(),
|
||||
auto_sort: true,
|
||||
sort_reverse: true,
|
||||
disable_tag: false,
|
||||
disable_extended_tag: false,
|
||||
extended_process_pid: None,
|
||||
extended_stats: None,
|
||||
no_kernel_threads: false,
|
||||
disable_io_counters: false,
|
||||
disable_gids: false,
|
||||
disable_cpu_num: false,
|
||||
disable_stats: Vec::new(),
|
||||
filter_focus: FilterList::default(),
|
||||
filter: Filter::default(),
|
||||
filter_export: FilterList::default(),
|
||||
max_values: HashMap::new(),
|
||||
io_old: HashMap::new(),
|
||||
programs: Vec::new(),
|
||||
programs_valid: false,
|
||||
};
|
||||
p.reset_max_values();
|
||||
p
|
||||
}
|
||||
|
||||
pub fn reset_max_values(&mut self) {
|
||||
self.max_values.clear();
|
||||
self.max_values.insert("cpu_percent".to_string(), 0.0);
|
||||
self.max_values.insert("memory_percent".to_string(), 0.0);
|
||||
}
|
||||
|
||||
pub fn set_sort_key(&mut self, key: &str, auto: bool) {
|
||||
if key == "auto" {
|
||||
self.auto_sort = true;
|
||||
self.sort_key = "cpu_percent".to_string();
|
||||
} else {
|
||||
self.auto_sort = auto;
|
||||
self.sort_key = key.to_string();
|
||||
}
|
||||
self.sort_reverse = !matches!(self.sort_key.as_str(), "name" | "username" | "cpu_num");
|
||||
}
|
||||
|
||||
pub fn enable(&mut self) {
|
||||
self.disable_tag = false;
|
||||
}
|
||||
|
||||
pub fn disable(&mut self) {
|
||||
self.disable_tag = true;
|
||||
}
|
||||
|
||||
pub fn enable_extended(&mut self) {
|
||||
self.disable_extended_tag = false;
|
||||
}
|
||||
|
||||
pub fn disable_extended(&mut self) {
|
||||
self.disable_extended_tag = true;
|
||||
}
|
||||
|
||||
pub fn pid_max(&self) -> Option<i64> {
|
||||
std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::<i64>().ok())
|
||||
}
|
||||
|
||||
pub fn processes_count(&self, max_processes: Option<i64>) -> i64 {
|
||||
match max_processes {
|
||||
Some(max) => (max - 2).min(self.processcount.total - 1),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the processes stats.
|
||||
pub fn update(&mut self, cursor_position: i64, args: &crate::args::Args) -> &[Value] {
|
||||
if self.disable_tag {
|
||||
return &[];
|
||||
}
|
||||
self.no_kernel_threads = args.no_kernel_threads;
|
||||
|
||||
let time_since_update = crate::timer::get_time_since_last_update("process_disk");
|
||||
|
||||
// Refresh the user list at most every 60 seconds.
|
||||
if self.users_timer <= 0.0 {
|
||||
self.users = Users::new_with_refreshed_list();
|
||||
self.users_timer = 60.0;
|
||||
}
|
||||
self.users_timer -= 1.0;
|
||||
|
||||
self.system.refresh_memory();
|
||||
self.system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
let total_mem = self.system.total_memory().max(1) as f64;
|
||||
let mut processlist: Vec<Value> = Vec::with_capacity(self.system.processes().len());
|
||||
|
||||
for (pid, process) in self.system.processes() {
|
||||
// Skip threads: the process list shows processes only.
|
||||
if process.thread_kind().is_some() {
|
||||
continue;
|
||||
}
|
||||
let pid = pid.as_u32() as i64;
|
||||
if self.no_kernel_threads && process.parent().map(|p| p.as_u32()) == Some(2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stat = read_proc_stat(pid);
|
||||
let name = process.name().to_string_lossy().to_string();
|
||||
let cmdline: Vec<String> =
|
||||
process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect();
|
||||
let username = process
|
||||
.user_id()
|
||||
.and_then(|uid| self.users.get_user_by_id(uid))
|
||||
.map(|u| u.name().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let status = stat.as_ref().map(|s| s.state.clone()).unwrap_or_else(|| "?".to_string());
|
||||
let nice = stat.as_ref().map(|s| s.nice).unwrap_or(0);
|
||||
let num_threads = stat.as_ref().map(|s| s.num_threads).unwrap_or(0);
|
||||
let cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0);
|
||||
let (user_time, system_time) =
|
||||
stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0));
|
||||
|
||||
let rss = process.memory() as f64;
|
||||
let vms = process.virtual_memory() as f64;
|
||||
let memory_percent = rss * 100.0 / total_mem;
|
||||
|
||||
let mut proc = Map::new();
|
||||
proc.insert("pid".to_string(), json!(pid));
|
||||
proc.insert("name".to_string(), json!(name));
|
||||
proc.insert("cmdline".to_string(), json!(cmdline));
|
||||
proc.insert("username".to_string(), json!(username));
|
||||
proc.insert("num_threads".to_string(), json!(num_threads));
|
||||
proc.insert("cpu_percent".to_string(), json!(process.cpu_usage() as f64));
|
||||
proc.insert("memory_percent".to_string(), json!(memory_percent));
|
||||
proc.insert("memory_info".to_string(), json!({"rss": rss, "vms": vms}));
|
||||
proc.insert("status".to_string(), json!(status));
|
||||
proc.insert("nice".to_string(), json!(nice));
|
||||
proc.insert(
|
||||
"cpu_times".to_string(),
|
||||
json!({"user": user_time, "system": system_time, "iowait": 0.0}),
|
||||
);
|
||||
proc.insert("cpu_num".to_string(), json!(cpu_num));
|
||||
|
||||
// IO counters: [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
|
||||
let (read_bytes, write_bytes) = match &stat {
|
||||
Some(stat) => (stat.read_bytes, stat.write_bytes),
|
||||
None => {
|
||||
let disk = process.disk_usage();
|
||||
(disk.total_read_bytes, disk.total_written_bytes)
|
||||
}
|
||||
};
|
||||
let (io_tag, io_old) = match self.io_old.get(&pid) {
|
||||
Some(old) => (1, *old),
|
||||
None => (0, [0, 0]),
|
||||
};
|
||||
self.io_old.insert(pid, [read_bytes, write_bytes]);
|
||||
proc.insert(
|
||||
"io_counters".to_string(),
|
||||
json!([read_bytes, write_bytes, io_old[0], io_old[1], io_tag]),
|
||||
);
|
||||
|
||||
// GIDs
|
||||
if let Some(stat) = &stat {
|
||||
proc.insert(
|
||||
"gids".to_string(),
|
||||
json!({"real": stat.gid_real, "effective": stat.gid_effective, "saved": stat.gid_saved}),
|
||||
);
|
||||
}
|
||||
|
||||
// Metadata
|
||||
proc.insert("key".to_string(), json!("pid"));
|
||||
proc.insert("time_since_update".to_string(), json!(time_since_update));
|
||||
|
||||
processlist.push(Value::Object(proc));
|
||||
}
|
||||
|
||||
// Sort processes by the current sort key.
|
||||
sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse);
|
||||
|
||||
self.update_processcount(&processlist);
|
||||
|
||||
// Extended stats for the selected process.
|
||||
for (position, proc) in processlist.iter_mut().enumerate() {
|
||||
let pid = proc.get("pid").and_then(Value::as_i64).unwrap_or(0);
|
||||
if args.enable_process_extended
|
||||
&& !args.programs
|
||||
&& !self.disable_extended_tag
|
||||
&& position as i64 == cursor_position
|
||||
&& !args.disable_cursor
|
||||
{
|
||||
self.extended_process_pid = Some(pid);
|
||||
}
|
||||
if let Some(target) = self.extended_process_pid {
|
||||
if target == pid {
|
||||
let extended = self.set_extended_stats(pid, proc);
|
||||
if let Value::Object(map) = extended {
|
||||
if let Some(obj) = proc.as_object_mut() {
|
||||
for (k, v) in map {
|
||||
obj.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove non running processes from the IO cache.
|
||||
let running: std::collections::HashSet<i64> =
|
||||
processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect();
|
||||
self.io_old.retain(|pid, _| running.contains(pid));
|
||||
|
||||
// Export list.
|
||||
self.processlist_export = self.update_export_list(&processlist);
|
||||
|
||||
// Filtered process list.
|
||||
let filtered = self.update_list(&processlist);
|
||||
self.programs_valid = false;
|
||||
|
||||
// Compute the maximum values for CPU and MEM.
|
||||
for key in ["cpu_percent", "memory_percent"] {
|
||||
if self.disable_stats.iter().any(|s| s == key) {
|
||||
continue;
|
||||
}
|
||||
let max = filtered
|
||||
.iter()
|
||||
.filter_map(|p| p.get(key).and_then(Value::as_f64))
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
if max.is_finite() {
|
||||
self.max_values.insert(key.to_string(), max);
|
||||
}
|
||||
}
|
||||
|
||||
self.processlist = filtered;
|
||||
&self.processlist
|
||||
}
|
||||
|
||||
fn set_extended_stats(&mut self, pid: i64, proc: &Value) -> Value {
|
||||
let mut ret = Map::new();
|
||||
// CPU and MEM min/max/mean
|
||||
let previous = self.extended_stats.clone();
|
||||
let prefix = |ret: &mut Map<String, Value>,
|
||||
stat_prefix: &str,
|
||||
current: f64,
|
||||
previous: Option<&Value>| {
|
||||
let prev = previous.and_then(|p| p.as_object());
|
||||
let min_key = format!("{stat_prefix}_min");
|
||||
let max_key = format!("{stat_prefix}_max");
|
||||
let mean_sum_key = format!("{stat_prefix}_mean_sum");
|
||||
let mean_counter_key = format!("{stat_prefix}_mean_counter");
|
||||
let prev_min = prev.and_then(|p| p.get(&min_key)).and_then(Value::as_f64);
|
||||
let prev_max = prev.and_then(|p| p.get(&max_key)).and_then(Value::as_f64);
|
||||
let prev_sum = prev.and_then(|p| p.get(&mean_sum_key)).and_then(Value::as_f64);
|
||||
let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64);
|
||||
let min = prev_min.map(|m| m.min(current)).unwrap_or(current);
|
||||
let max = prev_max.map(|m| m.max(current)).unwrap_or(current);
|
||||
let sum = prev_sum.map(|s| s + current).unwrap_or(current);
|
||||
let count = prev_count.map(|c| c + 1).unwrap_or(1);
|
||||
ret.insert(min_key, json!(min));
|
||||
ret.insert(max_key, json!(max));
|
||||
ret.insert(mean_sum_key, json!(sum));
|
||||
ret.insert(mean_counter_key, json!(count));
|
||||
ret.insert(format!("{stat_prefix}_mean"), json!(sum / count as f64));
|
||||
};
|
||||
|
||||
let cpu_percent = proc.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
prefix(&mut ret, "cpu", cpu_percent, previous.as_ref());
|
||||
prefix(&mut ret, "memory", memory_percent, previous.as_ref());
|
||||
ret.insert("extended_stats".to_string(), json!(true));
|
||||
|
||||
let extended = Value::Object(ret);
|
||||
self.extended_stats = Some(extended.clone());
|
||||
self.extended_process_pid = Some(pid);
|
||||
extended
|
||||
}
|
||||
|
||||
pub fn update_processcount(&mut self, processlist: &[Value]) {
|
||||
self.processcount.pid_max = self.pid_max();
|
||||
self.processcount.running = processlist
|
||||
.iter()
|
||||
.filter(|p| p.get("status").and_then(Value::as_str) == Some("R"))
|
||||
.count() as i64;
|
||||
self.processcount.sleeping = processlist
|
||||
.iter()
|
||||
.filter(|p| p.get("status").and_then(Value::as_str) == Some("S"))
|
||||
.count() as i64;
|
||||
self.processcount.thread = processlist
|
||||
.iter()
|
||||
.filter_map(|p| p.get("num_threads").and_then(Value::as_i64))
|
||||
.sum();
|
||||
self.processcount.total = processlist.len() as i64;
|
||||
}
|
||||
|
||||
fn update_list(&self, processlist: &[Value]) -> Vec<Value> {
|
||||
if !self.filter_focus.is_empty() {
|
||||
return processlist.iter().filter(|p| self.filter_focus.is_filtered(p)).cloned().collect();
|
||||
}
|
||||
if self.filter.filter().is_none() {
|
||||
return processlist.to_vec();
|
||||
}
|
||||
processlist.iter().filter(|p| self.filter.is_filtered(p)).cloned().collect()
|
||||
}
|
||||
|
||||
fn update_export_list(&self, processlist: &[Value]) -> Vec<Value> {
|
||||
if self.filter_export.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
processlist.iter().filter(|p| self.filter_export.is_filtered(p)).cloned().collect()
|
||||
}
|
||||
|
||||
/// Get the process list (sorted or not).
|
||||
pub fn get_list(&self, sorted: bool) -> &[Value] {
|
||||
let _ = sorted; // the list is always kept sorted after update()
|
||||
&self.processlist
|
||||
}
|
||||
|
||||
/// Get the process list as programs (aggregated by name).
|
||||
pub fn get_list_as_programs(&mut self) -> &[Value] {
|
||||
if !self.programs_valid {
|
||||
self.programs = processes_to_programs(&self.processlist);
|
||||
self.programs_valid = true;
|
||||
}
|
||||
&self.programs
|
||||
}
|
||||
|
||||
pub fn get_export(&self) -> &[Value] {
|
||||
&self.processlist_export
|
||||
}
|
||||
|
||||
pub fn get_stats(&self, pid: i64) -> Option<&Value> {
|
||||
self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid))
|
||||
}
|
||||
|
||||
/// Decrease the nice level of a process (higher priority).
|
||||
pub fn nice_decrease(&self, pid: i64) {
|
||||
let current = read_nice(pid).unwrap_or(0) as i32;
|
||||
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current - 1) } == 0 {
|
||||
tracing::info!("Set nice level of process {pid} to {} (higher the priority)", current - 1);
|
||||
} else {
|
||||
tracing::warn!("Can not decrease (higher the priority) the nice level of process {pid} (access denied)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Increase the nice level of a process (lower priority).
|
||||
pub fn nice_increase(&self, pid: i64) {
|
||||
let current = read_nice(pid).unwrap_or(0) as i32;
|
||||
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current + 1) } == 0 {
|
||||
tracing::info!("Set nice level of process {pid} to {} (lower the priority)", current + 1);
|
||||
} else {
|
||||
tracing::warn!("Can not increase (lower the priority) the nice level of process {pid} (access denied)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill a process.
|
||||
pub fn kill(&self, pid: i64) {
|
||||
assert!(pid != std::process::id() as i64, "Glances can kill itself...");
|
||||
tracing::debug!("Send kill signal to process: {pid}");
|
||||
unsafe {
|
||||
libc::kill(pid as libc::pid_t, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The global process manager (equivalent of `glances_processes`).
|
||||
pub fn processes() -> &'static Mutex<Processes> {
|
||||
static PROCESSES: OnceLock<Mutex<Processes>> = OnceLock::new();
|
||||
PROCESSES.get_or_init(|| Mutex::new(Processes::new()))
|
||||
}
|
||||
|
||||
fn weighted(value: Option<f64>) -> f64 {
|
||||
value.unwrap_or(f64::NEG_INFINITY)
|
||||
}
|
||||
|
||||
/// Sort a list of process dicts by the given key.
|
||||
pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &str, reverse: bool) {
|
||||
let key_of = |process: &Value, key: &str| -> f64 {
|
||||
match key {
|
||||
"io_counters" => {
|
||||
// [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
|
||||
let counters = process.get("io_counters").and_then(Value::as_array);
|
||||
let read = counters.and_then(|c| c.first()).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
read - read_old + write - write_old
|
||||
}
|
||||
"cpu_times" => {
|
||||
let times = process.get("cpu_times");
|
||||
let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
user + system
|
||||
}
|
||||
_ => process.get(key).and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY),
|
||||
}
|
||||
};
|
||||
let string_key = |process: &Value, key: &str| -> String {
|
||||
process.get(key).and_then(Value::as_str).unwrap_or("~").to_string()
|
||||
};
|
||||
|
||||
stats.sort_by(|a, b| {
|
||||
let (primary, secondary) = match sorted_by {
|
||||
"name" | "username" => {
|
||||
let cmp = string_key(a, sorted_by).cmp(&string_key(b, sorted_by));
|
||||
let ord = if reverse { cmp.reverse() } else { cmp };
|
||||
return ord;
|
||||
}
|
||||
"cpu_num" => {
|
||||
let av = a.get("cpu_num").and_then(Value::as_i64).unwrap_or(0);
|
||||
let bv = b.get("cpu_num").and_then(Value::as_i64).unwrap_or(0);
|
||||
let ord = av.cmp(&bv);
|
||||
return if reverse { ord.reverse() } else { ord };
|
||||
}
|
||||
_ => (key_of(a, sorted_by), key_of(b, sorted_by)),
|
||||
};
|
||||
let ord = weighted(Some(primary))
|
||||
.partial_cmp(&weighted(Some(secondary)))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
weighted(Some(key_of(a, sorted_by_secondary)))
|
||||
.partial_cmp(&weighted(Some(key_of(b, sorted_by_secondary))))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
if reverse { ord.reverse() } else { ord }
|
||||
});
|
||||
}
|
||||
|
||||
/// Aggregate processes by program name (port of `processes_to_programs`).
|
||||
pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
|
||||
let no_io = [0u64; 5];
|
||||
let mut programs: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
|
||||
for process in processlist {
|
||||
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
|
||||
let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0);
|
||||
let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({}));
|
||||
let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({}));
|
||||
let io_counters: Vec<u64> = process
|
||||
.get("io_counters")
|
||||
.and_then(Value::as_array)
|
||||
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
|
||||
.unwrap_or_else(|| no_io.to_vec());
|
||||
let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0);
|
||||
let username = process.get("username").and_then(Value::as_str).unwrap_or("_").to_string();
|
||||
let nice = process.get("nice").and_then(Value::as_i64).unwrap_or(0);
|
||||
let status = process.get("status").and_then(Value::as_str).unwrap_or("?").to_string();
|
||||
let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null);
|
||||
|
||||
match programs.get_mut(&name) {
|
||||
None => {
|
||||
programs.insert(
|
||||
name.clone(),
|
||||
json!({
|
||||
"time_since_update": time_since_update,
|
||||
"num_threads": num_threads,
|
||||
"cpu_percent": cpu_percent,
|
||||
"memory_percent": memory_percent,
|
||||
"cpu_times": cpu_times,
|
||||
"memory_info": memory_info,
|
||||
"io_counters": io_counters,
|
||||
"childrens": [pid],
|
||||
"name": name,
|
||||
"cmdline": [name],
|
||||
"pid": "_",
|
||||
"username": username,
|
||||
"nice": nice,
|
||||
"status": status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Some(program) => {
|
||||
let Some(program) = program.as_object_mut() else {
|
||||
continue;
|
||||
};
|
||||
let add = |program: &mut Map<String, Value>, key: &str, value: f64| {
|
||||
let current = program.get(key).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
program.insert(key.to_string(), json!(current + value));
|
||||
};
|
||||
add(program, "num_threads", num_threads);
|
||||
add(program, "cpu_percent", cpu_percent);
|
||||
add(program, "memory_percent", memory_percent);
|
||||
for key in ["user", "system", "iowait"] {
|
||||
let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
if value != 0.0 || cpu_times.get(key).is_some() {
|
||||
if let Some(times) = program.get_mut("cpu_times").and_then(Value::as_object_mut) {
|
||||
let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
times.insert(key.to_string(), json!(current + value));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(program_memory) = program.get_mut("memory_info").and_then(Value::as_object_mut) {
|
||||
if let Some(addition) = memory_info.as_object() {
|
||||
for (key, value) in addition {
|
||||
let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let value = value.as_f64().unwrap_or(0.0);
|
||||
program_memory.insert(key.clone(), json!(current + value));
|
||||
}
|
||||
}
|
||||
}
|
||||
// io_counters: sum slots 0..4, OR the tag.
|
||||
let mut summed: Vec<u64> = vec![0; 5];
|
||||
let existing: Vec<u64> = program
|
||||
.get("io_counters")
|
||||
.and_then(Value::as_array)
|
||||
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
|
||||
.unwrap_or_else(|| no_io.to_vec());
|
||||
for index in 0..4 {
|
||||
summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0);
|
||||
}
|
||||
summed[4] = existing.get(4).copied().unwrap_or(0).max(io_counters.get(4).copied().unwrap_or(0));
|
||||
program.insert("io_counters".to_string(), json!(summed));
|
||||
if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) {
|
||||
children.push(json!(pid));
|
||||
}
|
||||
let same = |program: &Map<String, Value>, key: &str, value: &str| {
|
||||
program.get(key).and_then(Value::as_str) == Some(value)
|
||||
};
|
||||
if !same(program, "username", &username) {
|
||||
program.insert("username".to_string(), json!("_"));
|
||||
}
|
||||
if program.get("nice").and_then(Value::as_i64) != Some(nice) {
|
||||
program.insert("nice".to_string(), json!("_"));
|
||||
}
|
||||
if !same(program, "status", &status) {
|
||||
program.insert("status".to_string(), json!("_"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
programs
|
||||
.into_values()
|
||||
.map(|mut program| {
|
||||
if let Some(program) = program.as_object_mut() {
|
||||
let nprocs = program.get("childrens").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0);
|
||||
program.insert("nprocs".to_string(), json!(nprocs));
|
||||
}
|
||||
program
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A few fields grabbed straight from `/proc/<pid>/stat` and `/proc/<pid>/io`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct ProcStat {
|
||||
state: String,
|
||||
nice: i64,
|
||||
num_threads: i64,
|
||||
processor: i64,
|
||||
utime: f64,
|
||||
stime: f64,
|
||||
read_bytes: u64,
|
||||
write_bytes: u64,
|
||||
gid_real: i64,
|
||||
gid_effective: i64,
|
||||
gid_saved: i64,
|
||||
}
|
||||
|
||||
fn read_proc_stat(pid: i64) -> Option<ProcStat> {
|
||||
let content = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
|
||||
// The process name may contain spaces and parenthesis: strip it.
|
||||
let close = content.rfind(')')?;
|
||||
let name = &content[..=close];
|
||||
let fields: Vec<&str> = content[close + 1..].split_whitespace().collect();
|
||||
// fields[0] = state (stat field 3), so stat field N (1-based) is fields[N-3].
|
||||
let field = |n: usize| fields.get(n).copied().unwrap_or("");
|
||||
let state = field(0).chars().next().unwrap_or('?').to_ascii_uppercase().to_string();
|
||||
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
|
||||
let ticks = if ticks <= 0.0 { 100.0 } else { ticks };
|
||||
let mut stat = ProcStat {
|
||||
state,
|
||||
nice: field(16).parse().unwrap_or(0),
|
||||
num_threads: field(17).parse().unwrap_or(0),
|
||||
processor: field(36).parse().unwrap_or(0),
|
||||
utime: field(11).parse::<f64>().unwrap_or(0.0) / ticks,
|
||||
stime: field(12).parse::<f64>().unwrap_or(0.0) / ticks,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = name;
|
||||
|
||||
if let Ok(io) = std::fs::read_to_string(format!("/proc/{pid}/io")) {
|
||||
for line in io.lines() {
|
||||
if let Some(value) = line.strip_prefix("read_bytes:") {
|
||||
stat.read_bytes = value.trim().parse().unwrap_or(0);
|
||||
} else if let Some(value) = line.strip_prefix("write_bytes:") {
|
||||
stat.write_bytes = value.trim().parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) {
|
||||
if let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) {
|
||||
let gids: Vec<i64> = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect();
|
||||
stat.gid_real = gids.first().copied().unwrap_or(0);
|
||||
stat.gid_effective = gids.get(1).copied().unwrap_or(0);
|
||||
stat.gid_saved = gids.get(2).copied().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
Some(stat)
|
||||
}
|
||||
|
||||
fn read_nice(pid: i64) -> Option<i64> {
|
||||
read_proc_stat(pid).map(|s| s.nice)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_processes_update() {
|
||||
use clap::Parser;
|
||||
let args = crate::args::Args::parse_from(["glances"]);
|
||||
let mut processes = Processes::new();
|
||||
let list = processes.update(0, &args).to_vec();
|
||||
assert!(!list.is_empty());
|
||||
assert!(list[0].get("pid").is_some());
|
||||
assert!(list[0].get("name").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_stats() {
|
||||
let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})];
|
||||
sort_stats(&mut stats, "cpu_percent", "memory_percent", true);
|
||||
assert_eq!(stats[0]["name"], "b");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Secure shell helpers (port of `glances.secure`).
|
||||
//!
|
||||
//! By default the shell-like operators `&&`, `|` and `>` are interpreted.
|
||||
//! When `allow_operators` is false the command is run as a single process and
|
||||
//! the operators are passed verbatim. `render` is applied to every argument
|
||||
//! *after* the command line has been tokenized so that untrusted values can
|
||||
//! not change the argument boundaries.
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
const OPERATORS: [&str; 5] = ["&&", "|", ">>", ">", "&"];
|
||||
|
||||
/// Replace shell operators by spaces, recursing into containers.
|
||||
pub fn sanitize_value(value: &serde_json::Value) -> serde_json::Value {
|
||||
match value {
|
||||
serde_json::Value::String(s) => {
|
||||
let mut s = s.clone();
|
||||
for op in OPERATORS {
|
||||
s = s.replace(op, " ");
|
||||
}
|
||||
serde_json::Value::String(s)
|
||||
}
|
||||
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())
|
||||
}
|
||||
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> {
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_single = false;
|
||||
let mut in_double = false;
|
||||
let mut has_content = false;
|
||||
|
||||
for c in cmd.chars() {
|
||||
if in_single {
|
||||
if c == '\'' {
|
||||
in_single = false;
|
||||
} else {
|
||||
current.push(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if in_double {
|
||||
if c == '"' {
|
||||
in_double = false;
|
||||
} else {
|
||||
current.push(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'\'' => {
|
||||
in_single = true;
|
||||
has_content = true;
|
||||
}
|
||||
'"' => {
|
||||
in_double = true;
|
||||
has_content = true;
|
||||
}
|
||||
c if c.is_whitespace() => {
|
||||
if has_content {
|
||||
args.push(std::mem::take(&mut current));
|
||||
has_content = false;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
current.push(c);
|
||||
has_content = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_content {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
match render {
|
||||
None => Ok(args),
|
||||
Some(render) => args.into_iter().map(|arg| render(&arg)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_argv(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
|
||||
let args = match split_args(cmd, render) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return format!("Glances error: {e}"),
|
||||
};
|
||||
if args.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut command = Command::new(&args[0]);
|
||||
command.args(&args[1..]);
|
||||
match command.output() {
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
if stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
stderr
|
||||
}
|
||||
}
|
||||
Err(e) => format!("ERROR: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
|
||||
let mut cmd = cmd;
|
||||
let mut stdout_redirect: Option<String> = None;
|
||||
|
||||
let mut redirect_split = cmd.splitn(3, '>');
|
||||
let first = redirect_split.next().unwrap_or_default();
|
||||
let second = redirect_split.next();
|
||||
let third = redirect_split.next();
|
||||
if third.is_some() {
|
||||
return format!("Glances error: Only one file redirection allowed ({cmd})");
|
||||
}
|
||||
if let Some(target) = second {
|
||||
let target = target.trim();
|
||||
let target = match render {
|
||||
Some(render) => match render(target) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return format!("Glances error: {e}"),
|
||||
},
|
||||
None => target.to_string(),
|
||||
};
|
||||
stdout_redirect = Some(target);
|
||||
cmd = first;
|
||||
}
|
||||
|
||||
// Tokenize (and render) every stage before spawning anything.
|
||||
let mut stages: Vec<Vec<String>> = Vec::new();
|
||||
for sub_cmd in cmd.split('|') {
|
||||
match split_args(sub_cmd, render) {
|
||||
Ok(args) => stages.push(args),
|
||||
Err(e) => return format!("Glances error: {e}"),
|
||||
}
|
||||
}
|
||||
if stages.is_empty() || stages.iter().any(|s| s.is_empty()) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut children: Vec<std::process::Child> = Vec::new();
|
||||
let mut previous_stdout: Option<std::process::ChildStdout> = None;
|
||||
for args in &stages {
|
||||
let mut command = Command::new(&args[0]);
|
||||
command.args(&args[1..]).stderr(Stdio::piped());
|
||||
match previous_stdout.take() {
|
||||
Some(stdout) => {
|
||||
command.stdin(Stdio::from(stdout));
|
||||
}
|
||||
None => {
|
||||
command.stdin(Stdio::null());
|
||||
}
|
||||
}
|
||||
let mut child = match command.stdout(Stdio::piped()).spawn() {
|
||||
Ok(child) => child,
|
||||
Err(e) => return format!("ERROR: {e}"),
|
||||
};
|
||||
previous_stdout = child.stdout.take();
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
let last = children.pop().expect("at least one child");
|
||||
let output = match last.wait_with_output() {
|
||||
Ok(output) => output,
|
||||
Err(e) => return format!("ERROR: {e}"),
|
||||
};
|
||||
for mut child in children {
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
if !stderr.is_empty() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if !allow_operators {
|
||||
return run_argv(cmd, render);
|
||||
}
|
||||
let mut ret = String::new();
|
||||
for part in cmd.split("&&") {
|
||||
ret.push_str(&secure_popen_one(part, render));
|
||||
}
|
||||
ret
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Mutable runtime state (hotkey toggles and shared UI state).
|
||||
|
||||
/// Runtime state shared between the engine, the plugins and the TUI.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Runtime {
|
||||
/// Display Rx and Tx sum instead of per interface values.
|
||||
pub network_sum: bool,
|
||||
/// Display cumulative network values instead of rates.
|
||||
pub network_cumul: bool,
|
||||
/// Per CPU mode.
|
||||
pub percpu: bool,
|
||||
/// Mean GPU mode.
|
||||
pub meangpu: bool,
|
||||
/// Accumulate processes by program.
|
||||
pub programs: bool,
|
||||
/// Force short process names.
|
||||
pub process_short_name: bool,
|
||||
/// Generate graph (influxdb export in Python; kept for compatibility).
|
||||
pub generate_graph: bool,
|
||||
/// Help screen tag.
|
||||
pub help_tag: bool,
|
||||
/// DiskIO: show IOPS instead of bytes.
|
||||
pub diskio_iops: bool,
|
||||
/// DiskIO: show latency.
|
||||
pub diskio_latency: bool,
|
||||
/// FS: show free space instead of used.
|
||||
pub fs_free_space: bool,
|
||||
/// Sparklines instead of bars.
|
||||
pub sparkline: bool,
|
||||
/// Cursor position in the process list.
|
||||
pub cursor_position: i64,
|
||||
/// Maximum number of processes displayed.
|
||||
pub max_processes: Option<i64>,
|
||||
/// Reset min/max tag (TUI 'M' key).
|
||||
pub reset_minmax_tag: bool,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub fn new(args: &crate::args::Args, fs_free_space_config: bool) -> Self {
|
||||
Self {
|
||||
network_sum: false,
|
||||
network_cumul: false,
|
||||
percpu: args.percpu,
|
||||
meangpu: args.meangpu,
|
||||
programs: args.programs,
|
||||
process_short_name: args.process_short_name(),
|
||||
generate_graph: false,
|
||||
help_tag: false,
|
||||
diskio_iops: args.diskio_iops,
|
||||
diskio_latency: args.diskio_latency,
|
||||
fs_free_space: args.fs_free_space || fs_free_space_config,
|
||||
sparkline: args.sparkline,
|
||||
cursor_position: 0,
|
||||
max_processes: None,
|
||||
reset_minmax_tag: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Thresholds (OK, CAREFUL, WARNING, CRITICAL).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Threshold {
|
||||
Ok,
|
||||
Careful,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
impl Threshold {
|
||||
pub fn from_str(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"OK" => Some(Threshold::Ok),
|
||||
"CAREFUL" => Some(Threshold::Careful),
|
||||
"WARNING" => Some(Threshold::Warning),
|
||||
"CRITICAL" => Some(Threshold::Critical),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(self) -> u8 {
|
||||
match self {
|
||||
Threshold::Ok => 0,
|
||||
Threshold::Careful => 1,
|
||||
Threshold::Warning => 2,
|
||||
Threshold::Critical => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(self) -> &'static str {
|
||||
match self {
|
||||
Threshold::Ok => "OK",
|
||||
Threshold::Careful => "CAREFUL",
|
||||
Threshold::Warning => "WARNING",
|
||||
Threshold::Critical => "CRITICAL",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Thresholds {
|
||||
map: HashMap<String, Threshold>,
|
||||
}
|
||||
|
||||
impl Thresholds {
|
||||
pub fn get(&self, stat_name: &str) -> Option<Threshold> {
|
||||
self.map.get(stat_name).copied()
|
||||
}
|
||||
|
||||
pub fn all(&self) -> &HashMap<String, Threshold> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
pub fn add(&mut self, stat_name: &str, threshold: &str) -> bool {
|
||||
match Threshold::from_str(threshold) {
|
||||
Some(t) => {
|
||||
self.map.insert(stat_name.to_string(), t);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Global thresholds registry shared by all plugins.
|
||||
pub fn thresholds() -> &'static Mutex<Thresholds> {
|
||||
static THRESHOLDS: OnceLock<Mutex<Thresholds>> = OnceLock::new();
|
||||
THRESHOLDS.get_or_init(|| Mutex::new(Thresholds::default()))
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Simple timers and the global "time since last update" registry.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
|
||||
/// The timer class. A simple chronometer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timer {
|
||||
duration: f64,
|
||||
target: Instant,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn new(duration: f64) -> Self {
|
||||
let mut timer = Self { duration, target: Instant::now() };
|
||||
timer.start();
|
||||
timer
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.target = Instant::now() + std::time::Duration::from_secs_f64(self.duration.max(0.0));
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, duration: Option<f64>) {
|
||||
if let Some(duration) = duration {
|
||||
self.set(duration);
|
||||
}
|
||||
self.start();
|
||||
}
|
||||
|
||||
/// Elapsed seconds since the timer was started.
|
||||
pub fn get(&self) -> f64 {
|
||||
self.duration - self.target.saturating_duration_since(Instant::now()).as_secs_f64()
|
||||
}
|
||||
|
||||
pub fn set(&mut self, duration: f64) {
|
||||
self.duration = duration;
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> f64 {
|
||||
self.duration
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> bool {
|
||||
Instant::now() > self.target
|
||||
}
|
||||
}
|
||||
|
||||
/// The counter class, used to log durations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Counter {
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl Default for Counter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new() -> Self {
|
||||
Self { start: Instant::now() }
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.start = Instant::now();
|
||||
}
|
||||
|
||||
pub fn get(&self) -> f64 {
|
||||
self.start.elapsed().as_secs_f64()
|
||||
}
|
||||
}
|
||||
|
||||
fn last_update_times() -> &'static Mutex<HashMap<String, Instant>> {
|
||||
static MAP: OnceLock<Mutex<HashMap<String, Instant>>> = OnceLock::new();
|
||||
MAP.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Return the elapsed time since the last update for the given key.
|
||||
///
|
||||
/// The first call for a key returns 1 second, mirroring the Python
|
||||
/// implementation.
|
||||
pub fn get_time_since_last_update(key: &str) -> f64 {
|
||||
let now = Instant::now();
|
||||
let mut map = last_update_times().lock().unwrap();
|
||||
let since = match map.get(key) {
|
||||
Some(last) => now.duration_since(*last).as_secs_f64(),
|
||||
None => 1.0,
|
||||
};
|
||||
map.insert(key.to_string(), now);
|
||||
since
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn timer_finished() {
|
||||
let timer = Timer::new(0.0);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
assert!(timer.finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_since_first_call_is_one() {
|
||||
let v = get_time_since_last_update("test-key");
|
||||
assert_eq!(v, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Formatting helpers (units, Python-compatible `str()`/`repr()`).
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Convert Celsius to Fahrenheit.
|
||||
pub fn to_fahrenheit(celsius: f64) -> f64 {
|
||||
celsius * 1.8 + 32.0
|
||||
}
|
||||
|
||||
/// Make a nice human-readable string out of a number (port of `auto_unit`).
|
||||
pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol: &str) -> String {
|
||||
let symbols: &[char] = &['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
let symbols: Vec<char> = match symbols.iter().position(|&s| s.to_string() == min_symbol) {
|
||||
Some(idx) => symbols[idx..].to_vec(),
|
||||
None => symbols.to_vec(),
|
||||
};
|
||||
let prefix = |symbol: char| -> f64 {
|
||||
match symbol {
|
||||
'Y' => 1208925819614629174706176.0,
|
||||
'Z' => 1180591620717411303424.0,
|
||||
'E' => 1152921504606846976.0,
|
||||
'P' => 1125899906842624.0,
|
||||
'T' => 1099511627776.0,
|
||||
'G' => 1073741824.0,
|
||||
'M' => 1048576.0,
|
||||
'K' => 1024.0,
|
||||
_ => 1.0,
|
||||
}
|
||||
};
|
||||
|
||||
if number.is_nan() {
|
||||
return none_symbol.to_string();
|
||||
}
|
||||
if number == 0.0 {
|
||||
// Avoid 0.0
|
||||
return "0".to_string();
|
||||
}
|
||||
if !number.is_finite() {
|
||||
return format!("{number}");
|
||||
}
|
||||
|
||||
// If the value was given as an integer, decimal_precision is 0.
|
||||
let mut decimal_precision = if number.fract() != 0.0 { 2 } else { 0 };
|
||||
|
||||
for &symbol in symbols.iter().rev() {
|
||||
let value = number / prefix(symbol);
|
||||
if value > 1.0 {
|
||||
decimal_precision = 0;
|
||||
if value <= 9.995 {
|
||||
decimal_precision = 2;
|
||||
} else if value < 99.95 {
|
||||
decimal_precision = 1;
|
||||
}
|
||||
if low_precision {
|
||||
if symbol == 'M' || symbol == 'K' {
|
||||
decimal_precision = 0;
|
||||
} else {
|
||||
decimal_precision = decimal_precision.min(1);
|
||||
}
|
||||
} else if symbol == 'K' {
|
||||
decimal_precision = 0;
|
||||
}
|
||||
return format!("{value:.decimal_precision$}{symbol}");
|
||||
}
|
||||
}
|
||||
|
||||
format!("{number:.decimal_precision$}")
|
||||
}
|
||||
|
||||
/// Parse a value like `'12.5 MB'` into a float (port of `string_value_to_float`).
|
||||
pub fn string_value_to_float(s: &str) -> Option<f64> {
|
||||
let cleaned: String = s.chars().filter(|c| *c != ' ').collect();
|
||||
let mut value: Option<f64> = None;
|
||||
let mut unit: Option<String> = None;
|
||||
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;
|
||||
}
|
||||
}
|
||||
rest.push(c);
|
||||
}
|
||||
if !digits.is_empty() {
|
||||
value = digits.parse::<f64>().ok();
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
unit = Some(rest.to_uppercase());
|
||||
}
|
||||
let value = value?;
|
||||
let multiplier = match unit.as_deref() {
|
||||
None => 1.0,
|
||||
Some("B") => 1.0,
|
||||
Some("KB") => 1000.0,
|
||||
Some("MB") => 1_000_000.0,
|
||||
Some("GB") => 1_000_000_000.0,
|
||||
Some("TB") => 1_000_000_000_000.0,
|
||||
Some("PB") => 1_000_000_000_000_000.0,
|
||||
Some(_) => return None,
|
||||
};
|
||||
Some(value * multiplier)
|
||||
}
|
||||
|
||||
/// Python `str()` for a JSON value (used by stdout/csv outputs).
|
||||
pub fn py_str(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Null => "None".to_string(),
|
||||
Value::Bool(true) => "True".to_string(),
|
||||
Value::Bool(false) => "False".to_string(),
|
||||
Value::Number(n) => py_number(n),
|
||||
Value::String(s) => s.clone(),
|
||||
other => py_repr(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn py_number(n: &serde_json::Number) -> String {
|
||||
if let Some(i) = n.as_i64() {
|
||||
return i.to_string();
|
||||
}
|
||||
if let Some(u) = n.as_u64() {
|
||||
return u.to_string();
|
||||
}
|
||||
let f = n.as_f64().unwrap_or(0.0);
|
||||
py_float(f)
|
||||
}
|
||||
|
||||
/// Python `repr()` for a float.
|
||||
pub fn py_float(f: f64) -> String {
|
||||
if f.is_nan() {
|
||||
return "nan".to_string();
|
||||
}
|
||||
if f.is_infinite() {
|
||||
return if f > 0.0 { "inf" } else { "-inf" }.to_string();
|
||||
}
|
||||
if f.fract() == 0.0 && f.abs() < 1e16 {
|
||||
format!("{f:.1}")
|
||||
} else {
|
||||
format!("{f}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `repr()` for a JSON value (single quotes, True/False/None).
|
||||
pub fn py_repr(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Null => "None".to_string(),
|
||||
Value::Bool(b) => if *b { "True" } else { "False" }.to_string(),
|
||||
Value::Number(n) => py_number(n),
|
||||
Value::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
|
||||
Value::Array(items) => {
|
||||
let inner: Vec<String> = items.iter().map(py_repr).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let inner: Vec<String> = map
|
||||
.iter()
|
||||
.map(|(k, v)| format!("'{}': {}", k.replace('\'', "\\'"), py_repr(v)))
|
||||
.collect();
|
||||
format!("{{{}}}", inner.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a f64 from a JSON value the way Python would accept a number.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple mean subsampling (port of `subsample`).
|
||||
pub fn subsample(data: &[f64], sampling: usize) -> Vec<f64> {
|
||||
if data.len() <= sampling || sampling == 0 {
|
||||
return data.to_vec();
|
||||
}
|
||||
let sampling_length = (data.len() as f64 / sampling as f64).round() as usize;
|
||||
(0..sampling)
|
||||
.map(|s| {
|
||||
let start = s * sampling_length;
|
||||
let end = ((s + 1) * sampling_length).min(data.len());
|
||||
let slice = &data[start..end];
|
||||
slice.iter().sum::<f64>() / slice.len() as f64
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return a list of the substrings in the input string, using `sep` as the
|
||||
/// separator char and `esc` as the escape character (port of `split_esc`,
|
||||
/// only the explicit separator flavour is required).
|
||||
pub fn split_esc(input: &str, sep: char, esc: char) -> Vec<String> {
|
||||
let mut ret: Vec<String> = vec![String::new()];
|
||||
let mut esc_seen = false;
|
||||
for ch in input.chars() {
|
||||
if !esc_seen {
|
||||
if ch == esc {
|
||||
esc_seen = true;
|
||||
} else if ch == sep {
|
||||
ret.push(String::new());
|
||||
} else {
|
||||
ret.last_mut().unwrap().push(ch);
|
||||
}
|
||||
} else {
|
||||
ret.last_mut().unwrap().push(ch);
|
||||
esc_seen = false;
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Replace special chars by another (new lines, tabs...) or escape for a
|
||||
/// 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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_auto_unit() {
|
||||
assert_eq!(auto_unit(613421788.0, false, "K", "-"), "585M");
|
||||
assert_eq!(auto_unit(5307033647.0, false, "K", "-"), "4.94G");
|
||||
assert_eq!(auto_unit(5307033647.0, true, "K", "-"), "4.9G");
|
||||
assert_eq!(auto_unit(44968414685.0, false, "K", "-"), "41.9G");
|
||||
assert_eq!(auto_unit(838471403472.0, false, "K", "-"), "781G");
|
||||
assert_eq!(auto_unit(9683209690677.0, false, "K", "-"), "8.81T");
|
||||
assert_eq!(auto_unit(1073741824.0, false, "K", "-"), "1024M");
|
||||
assert_eq!(auto_unit(1181116006.0, false, "K", "-"), "1.10G");
|
||||
assert_eq!(auto_unit(0.0, false, "K", "-"), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_py_repr() {
|
||||
assert_eq!(py_repr(&json!({"a": 1, "b": "x"})), "{'a': 1, 'b': 'x'}");
|
||||
assert_eq!(py_str(&json!(1234.0)), "1234.0");
|
||||
assert_eq!(py_str(&json!(5)), "5");
|
||||
assert_eq!(py_str(&json!(true)), "True");
|
||||
assert_eq!(py_str(&json!(null)), "None");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user