M4: fetch mode, cleanup and documentation

- --fetch with a Jinja-subset renderer and the shipped templates
- clippy clean, cargo fmt, all tests green
- README status and differential testing instructions
This commit is contained in:
glances-rs
2026-10-09 23:59:26 +00:00
parent cb22e53aa6
commit 6819ce4e18
47 changed files with 4145 additions and 935 deletions
Generated
+1
View File
@@ -722,6 +722,7 @@ dependencies = [
"glances-core",
"glances-plugins",
"glances-tui",
"indexmap",
"serde_json",
"tracing",
"tracing-subscriber",
+45 -9
View File
@@ -7,24 +7,60 @@ This project is a drop-in replacement for the **standalone** Glances modes:
- curses-like terminal UI (`glances`)
- stdout modes: `--stdout`, `--stdout-csv`, `--stdout-json`
- `--fetch`, `--issue`, `--modules-list`
- all plugins, including extended ones (containers, GPUs, sensors, ...)
- the same `glances.conf` configuration file
- `--fetch` (with the shipped templates), `--issue`, `--module-list`
- the same `glances.conf` configuration file (limits, thresholds, aliases,
show/hide lists, per-plugin refresh, history, actions)
- compatible stats schemas and CLI flags for the implemented features
The following Python Glances features are intentionally **not** provided:
REST API, WebUI, MCP, client/server mode, discovery/browser, SNMP and exporters.
REST API, WebUI, MCP, client/server mode, discovery/browser, SNMP and
exporters.
## Status
Work in progress. See the milestones in the project plan; the workspace is
organized as:
| Area | Status |
| --- | --- |
| CLI (clap, drop-in flags) | done |
| Configuration (`glances.conf`) | done |
| Plugin model (limits, views, rates, MMM, history, events, actions) | done |
| Terminal UI (ratatui) | done |
| stdout / CSV / JSON / fetch / issue modes | done |
| Core plugins | cpu, percpu, mem, memswap, load, fs, network, diskio, sensors, irq, system, uptime, now, core, version, psutilversion, quicklook, processcount, processlist, programlist |
| Extended plugins | folders, amps, ports, raid, wifi, ip |
| Not ported yet | containers, vms, gpu/npu/mpp, connections, smart, cloud |
| Python-specific flags | `--trace-malloc`, `--api-doc`, `--api-restful-doc` rejected with a clear error |
## Build and run
```console
$ cargo build --release
$ ./target/release/glances # terminal UI
$ ./target/release/glances --stdout-json cpu,mem,load
$ ./target/release/glances --fetch
```
## Differential testing
The `tools/parity.py` script compares the Rust implementation with the Python
reference implementation (the oracle) plugin by plugin: it runs both for a few
refreshes and diffs the flattened key sets and value types.
```console
$ python3 -m venv /tmp/opencode/glances-py
$ /tmp/opencode/glances-py/bin/pip install /path/to/python-glances
$ cargo build
$ python3 tools/parity.py # all implemented plugins
$ python3 tools/parity.py cpu mem # selected plugins
```
## Workspace layout
| Crate | Purpose |
| --- | --- |
| `glances-core` | plugin model, config, limits, history, events, filters, output |
| `glances-plugins` | collectors and plugin registry/engine |
| `glances-core` | plugin model, config, limits, history, events, filters, processes |
| `glances-plugins` | collectors, plugin registry and update engine |
| `glances-tui` | terminal UI (ratatui) |
| `glances-cli` | the `glances` binary |
| `glances-cli` | the `glances` binary, stdout modes and fetch renderer |
## License
+1
View File
@@ -15,6 +15,7 @@ ctrlc = "3.5.2"
glances-core = { version = "0.1.0", path = "../glances-core" }
glances-plugins = { version = "0.1.0", path = "../glances-plugins" }
glances-tui = { version = "0.1.0", path = "../glances-tui" }
indexmap.workspace = true
serde_json.workspace = true
tracing.workspace = true
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+969
View File
@@ -0,0 +1,969 @@
//! Fetch mode (`--fetch`): render a template against the Glances API.
//!
//! The Python implementation uses Jinja2. This module implements the small
//! template subset used by the shipped templates (expressions, filters,
//! `for`/`if` blocks, dotted access, calls).
use anyhow::{Result, bail};
use glances_core::plugin::Plugin;
use glances_core::{Args, py_str};
use glances_plugins::engine::Engine;
use indexmap::IndexMap;
use serde_json::Value;
/// Default template (same as the Python `DEFAULT_FETCH_TEMPLATE`).
const DEFAULT_TEMPLATE: &str = r#"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✨ {{ gl.system['hostname'] }}{{ ' | ' + gl.ip['address'] if gl.ip['address'] else '' }} | Uptime: {{ gl.uptime }}
⚙️ {{ gl.system['hr_name'] }}
💡 LOAD {{ '%0.2f'| format(gl.load['min1']) }}/min1 |\
{{ '%0.2f'| format(gl.load['min5']) }}/min5 |\
{{ '%0.2f'| format(gl.load['min15']) }}/min15
⚡ CPU {{ gl.bar(gl.cpu['total']) }} {{ gl.cpu['total'] }}% of {{ gl.core['log'] }} cores
🧠 MEM {{ gl.bar(gl.mem['percent']) }} {{ gl.mem['percent'] }}% ({{ gl.auto_unit(gl.mem['used']) }} /\
{{ gl.auto_unit(gl.mem['total']) }})
{% for fs in gl.fs.keys() %}\
💾 {% if loop.index == 1 %}DISK{% else %} {% endif %}\
{{ gl.bar(gl.fs[fs]['percent']) }} {{ gl.fs[fs]['percent'] }}% ({{ gl.auto_unit(gl.fs[fs]['used']) }} /\
{{ gl.auto_unit(gl.fs[fs]['size']) }}) for {{ fs }}
{% endfor %}\
{% for net in gl.network.keys() %}\
📡 {% if loop.index == 1 %}NET{% else %} {% endif %}\
↓ {{ gl.auto_unit(gl.network[net]['bytes_recv_rate_per_sec']) }}b/s\
↑ {{ gl.auto_unit(gl.network[net]['bytes_sent_rate_per_sec']) }}b/s for {{ net }}
{% endfor %}\
🔥 TOP PROCESS by CPU
{% for process in gl.top_process() %}\
{{ loop.index }}️⃣ {{ process['name'][:20] }}{{ ' ' * (20 - process['name'][:20] | length) }}\
⚡ {{ process['cpu_percent'] }}% CPU\
{{ ' ' * (8 - (gl.auto_unit(process['cpu_percent']) | length)) }}\
🧠 {{ gl.auto_unit(process['memory_info']['rss']) }}B MEM
{% endfor %}\
🔥 TOP PROCESS by MEM
{% for process in gl.top_process(sorted_by='memory_percent', sorted_by_secondary='cpu_percent') %}\
{{ loop.index }}️⃣ {{ process['name'][:20] }}{{ ' ' * (20 - process['name'][:20] | length) }}\
🧠 {{ gl.auto_unit(process['memory_info']['rss']) }}B MEM\
{{ ' ' * (7 - (gl.auto_unit(process['memory_info']['rss']) | length)) }}\
⚡ {{ process['cpu_percent'] }}% CPU
{% endfor %}\
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"#;
#[derive(Debug, Clone, PartialEq)]
enum JVal {
Null,
Bool(bool),
Num(f64),
Str(String),
List(Vec<JVal>),
Map(IndexMap<String, JVal>),
/// A builtin function of the API object.
Builtin(&'static str),
}
impl JVal {
fn truthy(&self) -> bool {
match self {
JVal::Null => false,
JVal::Bool(b) => *b,
JVal::Num(n) => *n != 0.0,
JVal::Str(s) => !s.is_empty(),
JVal::List(l) => !l.is_empty(),
JVal::Map(m) => !m.is_empty(),
JVal::Builtin(_) => true,
}
}
fn as_num(&self) -> f64 {
match self {
JVal::Num(n) => *n,
JVal::Bool(b) => f64::from(*b as u8),
JVal::Str(s) => s.parse().unwrap_or(0.0),
_ => 0.0,
}
}
fn to_text(&self) -> String {
match self {
JVal::Null => "".to_string(),
JVal::Bool(b) => if *b { "True" } else { "False" }.to_string(),
JVal::Num(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{n}")
}
}
JVal::Str(s) => s.clone(),
JVal::List(_) | JVal::Map(_) => String::new(),
JVal::Builtin(_) => String::new(),
}
}
}
fn json_to_jval(value: &Value) -> JVal {
match value {
Value::Null => JVal::Null,
Value::Bool(b) => JVal::Bool(*b),
Value::Number(n) => JVal::Num(n.as_f64().unwrap_or(0.0)),
Value::String(s) => JVal::Str(s.clone()),
Value::Array(items) => JVal::List(items.iter().map(json_to_jval).collect()),
Value::Object(map) => JVal::Map(
map.iter()
.map(|(k, v)| (k.clone(), json_to_jval(v)))
.collect(),
),
}
}
/// API object exposed to the template as `gl`.
struct Api<'a> {
engine: &'a Engine,
#[allow(dead_code)]
args: &'a Args,
}
impl<'a> Api<'a> {
fn new(engine: &'a Engine, args: &'a Args) -> Self {
Self { engine, args }
}
fn plugin_stats(&self, name: &str) -> JVal {
self.engine
.get_plugin(name)
.map(|p| json_to_jval(p.stats()))
.unwrap_or(JVal::Null)
}
/// Build the `gl` object.
fn object(&self) -> JVal {
let mut map: IndexMap<String, JVal> = IndexMap::new();
map.insert("system".to_string(), self.plugin_stats("system"));
map.insert("ip".to_string(), self.plugin_stats("ip"));
map.insert("uptime".to_string(), self.plugin_stats("uptime"));
map.insert("load".to_string(), self.plugin_stats("load"));
map.insert("cpu".to_string(), self.plugin_stats("cpu"));
map.insert("core".to_string(), self.plugin_stats("core"));
map.insert("mem".to_string(), self.plugin_stats("mem"));
// fs and network are exposed as dicts keyed by mount point/interface.
map.insert(
"fs".to_string(),
self.plugin_stats_as_dict("fs", "mnt_point"),
);
map.insert(
"network".to_string(),
self.plugin_stats_as_dict("network", "interface_name"),
);
map.insert("bar".to_string(), JVal::Builtin("bar"));
map.insert("auto_unit".to_string(), JVal::Builtin("auto_unit"));
map.insert("top_process".to_string(), JVal::Builtin("top_process"));
JVal::Map(map)
}
fn plugin_stats_as_dict(&self, plugin: &str, key: &str) -> JVal {
let mut map = IndexMap::new();
if let Some(plugin) = self.engine.get_plugin(plugin)
&& let Some(items) = plugin.stats().as_array()
{
for item in items {
if let Some(identity) = item.get(key).and_then(Value::as_str) {
map.insert(identity.to_string(), json_to_jval(item));
}
}
}
JVal::Map(map)
}
fn top_process(&self, sorted_by: &str, sorted_by_secondary: &str, limit: usize) -> JVal {
let procs = glances_core::processes::processes().lock().unwrap();
let mut list: Vec<Value> = procs
.get_list(false)
.iter()
.filter(|p| {
let cmdline = p
.get("cmdline")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
!cmdline.contains("glances")
})
.cloned()
.collect();
glances_core::processes::sort_stats(&mut list, sorted_by, sorted_by_secondary, true);
JVal::List(list.iter().take(limit).map(json_to_jval).collect())
}
}
// ---------------------------------------------------------------------------
// Template parser
// ---------------------------------------------------------------------------
#[derive(Debug)]
enum Node {
Text(String),
Expr(String),
For {
var: String,
iterable: String,
body: Vec<Node>,
},
If {
cond: String,
then_body: Vec<Node>,
else_body: Vec<Node>,
},
}
#[derive(Debug)]
enum Token {
Text(String),
Expr(String),
Stmt(String),
}
fn tokenize(template: &str) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
let mut chars = template.chars().peekable();
let mut text = String::new();
while let Some(c) = chars.next() {
// Jinja line continuation.
if c == '\\' && chars.peek() == Some(&'\n') {
chars.next();
continue;
}
if c == '{' {
match chars.peek() {
Some('{') => {
chars.next();
if !text.is_empty() {
tokens.push(Token::Text(std::mem::take(&mut text)));
}
let mut expr = String::new();
while let Some(c2) = chars.next() {
if c2 == '}' && chars.peek() == Some(&'}') {
chars.next();
break;
}
expr.push(c2);
}
tokens.push(Token::Expr(expr.trim().to_string()));
continue;
}
Some('%') => {
chars.next();
if !text.is_empty() {
tokens.push(Token::Text(std::mem::take(&mut text)));
}
let mut stmt = String::new();
while let Some(c2) = chars.next() {
if c2 == '%' && chars.peek() == Some(&'}') {
chars.next();
break;
}
stmt.push(c2);
}
tokens.push(Token::Stmt(stmt.trim().to_string()));
continue;
}
_ => {}
}
}
text.push(c);
}
if !text.is_empty() {
tokens.push(Token::Text(text));
}
Ok(tokens)
}
fn parse_block(tokens: &[Token], index: &mut usize, stop: &[&str]) -> Result<Vec<Node>> {
let mut nodes = Vec::new();
while *index < tokens.len() {
match &tokens[*index] {
Token::Text(text) => {
nodes.push(Node::Text(text.clone()));
*index += 1;
}
Token::Expr(expr) => {
nodes.push(Node::Expr(expr.clone()));
*index += 1;
}
Token::Stmt(stmt) => {
if stop
.iter()
.any(|s| stmt == s || stmt.starts_with(&format!("{s} ")))
{
return Ok(nodes);
}
if let Some(rest) = stmt.strip_prefix("for ") {
let (var, iterable) = rest.split_once(" in ").unwrap_or((rest, ""));
*index += 1;
let body = parse_block(tokens, index, &["endfor"])?;
if *index < tokens.len() {
*index += 1; // consume endfor
}
nodes.push(Node::For {
var: var.trim().to_string(),
iterable: iterable.trim().to_string(),
body,
});
continue;
}
if let Some(cond) = stmt.strip_prefix("if ") {
*index += 1;
let then_body = parse_block(tokens, index, &["else", "endif"])?;
let mut else_body = Vec::new();
if *index < tokens.len() {
let current = &tokens[*index];
let is_else = matches!(current, Token::Stmt(s) if s == "else");
*index += 1;
if is_else {
else_body = parse_block(tokens, index, &["endif"])?;
if *index < tokens.len() {
*index += 1;
}
}
}
nodes.push(Node::If {
cond: cond.trim().to_string(),
then_body,
else_body,
});
continue;
}
// Unknown statement: ignore it.
*index += 1;
}
}
}
Ok(nodes)
}
fn parse_template(template: &str) -> Result<Vec<Node>> {
let tokens = tokenize(template)?;
let mut index = 0;
let nodes = parse_block(&tokens, &mut index, &[])?;
if index < tokens.len() {
bail!("unexpected template statement");
}
Ok(nodes)
}
// ---------------------------------------------------------------------------
// Expression evaluator
// ---------------------------------------------------------------------------
struct ExprParser<'a> {
input: &'a [u8],
pos: usize,
}
impl<'a> ExprParser<'a> {
fn new(input: &'a str) -> Self {
Self {
input: input.as_bytes(),
pos: 0,
}
}
fn skip_ws(&mut self) {
while self.pos < self.input.len() && (self.input[self.pos] as char).is_whitespace() {
self.pos += 1;
}
}
fn peek_word(&mut self) -> String {
self.skip_ws();
let start = self.pos;
while self.pos < self.input.len() {
let c = self.input[self.pos] as char;
if c.is_alphanumeric() || c == '_' {
self.pos += 1;
} else {
break;
}
}
String::from_utf8_lossy(&self.input[start..self.pos]).to_string()
}
fn parse(&mut self, ctx: &EvalContext) -> Result<JVal> {
let value = self.parse_ternary(ctx)?;
Ok(value)
}
fn parse_ternary(&mut self, ctx: &EvalContext) -> Result<JVal> {
let value = self.parse_or(ctx)?;
let save = self.pos;
let word = self.peek_word();
if word == "if" {
let cond = self.parse_or(ctx)?;
let word = self.peek_word();
if word == "else" {
let other = self.parse_ternary(ctx)?;
return Ok(if cond.truthy() { value } else { other });
}
self.pos = save;
} else {
self.pos = save;
}
Ok(value)
}
fn parse_or(&mut self, ctx: &EvalContext) -> Result<JVal> {
let mut value = self.parse_and(ctx)?;
loop {
let save = self.pos;
if self.peek_word() == "or" {
let right = self.parse_and(ctx)?;
value = JVal::Bool(value.truthy() || right.truthy());
} else {
self.pos = save;
return Ok(value);
}
}
}
fn parse_and(&mut self, ctx: &EvalContext) -> Result<JVal> {
let mut value = self.parse_not(ctx)?;
loop {
let save = self.pos;
if self.peek_word() == "and" {
let right = self.parse_not(ctx)?;
value = JVal::Bool(value.truthy() && right.truthy());
} else {
self.pos = save;
return Ok(value);
}
}
}
fn parse_not(&mut self, ctx: &EvalContext) -> Result<JVal> {
let save = self.pos;
if self.peek_word() == "not" {
let value = self.parse_not(ctx)?;
return Ok(JVal::Bool(!value.truthy()));
}
self.pos = save;
self.parse_comparison(ctx)
}
fn parse_comparison(&mut self, ctx: &EvalContext) -> Result<JVal> {
let left = self.parse_additive(ctx)?;
self.skip_ws();
for op in ["==", "!=", ">=", "<=", ">", "<"] {
if self.input[self.pos..].starts_with(op.as_bytes()) {
self.pos += op.len();
let right = self.parse_additive(ctx)?;
let result = match op {
"==" => {
left == right
|| (left.to_text() == right.to_text()
&& !matches!(left, JVal::Map(_) | JVal::List(_)))
}
"!=" => left != right,
">" => left.as_num() > right.as_num(),
"<" => left.as_num() < right.as_num(),
">=" => left.as_num() >= right.as_num(),
"<=" => left.as_num() <= right.as_num(),
_ => false,
};
return Ok(JVal::Bool(result));
}
}
Ok(left)
}
fn parse_additive(&mut self, ctx: &EvalContext) -> Result<JVal> {
let mut value = self.parse_multiplicative(ctx)?;
loop {
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'+' {
self.pos += 1;
let right = self.parse_multiplicative(ctx)?;
value = match (&value, &right) {
(JVal::Num(a), JVal::Num(b)) => JVal::Num(a + b),
_ => JVal::Str(value.to_text() + &right.to_text()),
};
} else if self.pos < self.input.len() && self.input[self.pos] == b'-' {
self.pos += 1;
let right = self.parse_multiplicative(ctx)?;
value = JVal::Num(value.as_num() - right.as_num());
} else {
return Ok(value);
}
}
}
fn parse_multiplicative(&mut self, ctx: &EvalContext) -> Result<JVal> {
let mut value = self.parse_unary(ctx)?;
loop {
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'*' {
self.pos += 1;
let right = self.parse_unary(ctx)?;
value = match (&value, &right) {
(JVal::Str(s), JVal::Num(n)) | (JVal::Num(n), JVal::Str(s)) => {
JVal::Str(s.repeat(n.max(0.0) as usize))
}
_ => JVal::Num(value.as_num() * right.as_num()),
};
} else if self.pos < self.input.len() && self.input[self.pos] == b'/' {
self.pos += 1;
let right = self.parse_unary(ctx)?;
let divisor = right.as_num();
value = JVal::Num(if divisor == 0.0 {
0.0
} else {
value.as_num() / divisor
});
} else {
return Ok(value);
}
}
}
fn parse_unary(&mut self, ctx: &EvalContext) -> Result<JVal> {
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'-' {
self.pos += 1;
let value = self.parse_unary(ctx)?;
return Ok(JVal::Num(-value.as_num()));
}
self.parse_postfix(ctx)
}
fn parse_postfix(&mut self, ctx: &EvalContext) -> Result<JVal> {
let mut value = self.parse_primary(ctx)?;
loop {
self.skip_ws();
if self.pos >= self.input.len() {
return Ok(value);
}
match self.input[self.pos] {
b'.' => {
self.pos += 1;
let name = self.peek_word();
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'(' {
// Method call on a dict/list or builtin function call.
self.pos += 1;
let (args, kwargs) = self.parse_args(ctx)?;
let member = match &value {
JVal::Map(map) => map.get(&name).cloned().unwrap_or(JVal::Null),
_ => JVal::Null,
};
if let JVal::Builtin(builtin) = &member {
value = ctx.call_builtin(builtin, &args, &kwargs);
continue;
}
value = match (&value, name.as_str()) {
(JVal::Map(map), "keys") => {
JVal::List(map.keys().map(|k| JVal::Str(k.clone())).collect())
}
(JVal::Map(map), "values") => {
JVal::List(map.values().cloned().collect())
}
(JVal::Map(map), "items") => JVal::List(
map.iter()
.map(|(k, v)| JVal::List(vec![JVal::Str(k.clone()), v.clone()]))
.collect(),
),
(JVal::Map(map), "get") => {
let key = args.first().map(|v| v.to_text()).unwrap_or_default();
map.get(&key)
.cloned()
.unwrap_or_else(|| args.get(1).cloned().unwrap_or(JVal::Null))
}
_ => JVal::Null,
};
continue;
}
value = match &value {
JVal::Map(map) => map.get(&name).cloned().unwrap_or(JVal::Null),
_ => JVal::Null,
};
}
b'[' => {
self.pos += 1;
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b':' {
// Slice [:n]
self.pos += 1;
let end = self.parse_ternary(ctx)?.as_num() as usize;
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b']' {
self.pos += 1;
}
if let JVal::Str(s) = &value {
let chars: Vec<char> = s.chars().collect();
value = JVal::Str(chars[..end.min(chars.len())].iter().collect());
}
} else {
let key = self.parse_ternary(ctx)?;
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b']' {
self.pos += 1;
}
value = match &value {
JVal::Map(map) => {
map.get(&key.to_text()).cloned().unwrap_or(JVal::Null)
}
JVal::List(items) => {
let index = key.as_num() as usize;
items.get(index).cloned().unwrap_or(JVal::Null)
}
JVal::Str(s) => {
let chars: Vec<char> = s.chars().collect();
let index = key.as_num() as usize;
chars
.get(index)
.map(|c| JVal::Str(c.to_string()))
.unwrap_or(JVal::Null)
}
_ => JVal::Null,
};
}
}
b'(' => {
self.pos += 1;
let (args, kwargs) = self.parse_args(ctx)?;
value = self.call(value, &args, &kwargs, ctx);
}
b'|' => {
self.pos += 1;
let filter = self.peek_word();
let mut args: Vec<JVal> = Vec::new();
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'(' {
self.pos += 1;
loop {
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b')' {
self.pos += 1;
break;
}
let arg = self.parse_ternary(ctx)?;
args.push(arg);
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b',' {
self.pos += 1;
}
}
}
value = self.apply_filter(&filter, value, &args);
}
_ => return Ok(value),
}
}
}
fn call(
&self,
callee: JVal,
args: &[JVal],
kwargs: &IndexMap<String, JVal>,
ctx: &EvalContext,
) -> JVal {
match callee {
JVal::Builtin(name) => ctx.call_builtin(name, args, kwargs),
_ => JVal::Null,
}
}
fn apply_filter(&self, name: &str, value: JVal, args: &[JVal]) -> JVal {
match name {
"length" => JVal::Num(match &value {
JVal::Str(s) => s.chars().count() as f64,
JVal::List(l) => l.len() as f64,
JVal::Map(m) => m.len() as f64,
_ => 0.0,
}),
"format" => {
let mut result = value.to_text();
for arg in args {
// Only the first (already embedded) value is formatted.
let replacement = format_c(arg, &result);
result = replacement;
}
JVal::Str(result)
}
"upper" => JVal::Str(value.to_text().to_uppercase()),
"lower" => JVal::Str(value.to_text().to_lowercase()),
_ => value,
}
}
fn parse_args(&mut self, ctx: &EvalContext) -> Result<(Vec<JVal>, IndexMap<String, JVal>)> {
let mut args: Vec<JVal> = Vec::new();
let mut kwargs: IndexMap<String, JVal> = IndexMap::new();
loop {
self.skip_ws();
if self.pos >= self.input.len() {
break;
}
if self.input[self.pos] == b')' {
self.pos += 1;
break;
}
let save = self.pos;
let word = self.peek_word();
let mut is_kwarg = false;
if !word.is_empty() {
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b'=' {
is_kwarg = true;
self.pos += 1;
}
}
if is_kwarg {
let value = self.parse_ternary(ctx)?;
kwargs.insert(word, value);
} else {
self.pos = save;
let before = self.pos;
let value = self.parse_ternary(ctx)?;
args.push(value);
if self.pos == before {
// Defensive: never loop forever on an unexpected token.
self.pos += 1;
}
}
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b',' {
self.pos += 1;
}
}
Ok((args, kwargs))
}
fn parse_primary(&mut self, ctx: &EvalContext) -> Result<JVal> {
self.skip_ws();
if self.pos >= self.input.len() {
return Ok(JVal::Null);
}
let c = self.input[self.pos] as char;
if c == '(' {
self.pos += 1;
let value = self.parse_ternary(ctx)?;
self.skip_ws();
if self.pos < self.input.len() && self.input[self.pos] == b')' {
self.pos += 1;
}
return Ok(value);
}
if c == '\'' || c == '"' {
let quote = self.input[self.pos];
self.pos += 1;
let start = self.pos;
while self.pos < self.input.len() && self.input[self.pos] != quote {
self.pos += 1;
}
let text = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
self.pos += 1;
return Ok(JVal::Str(text));
}
if c.is_ascii_digit() {
let start = self.pos;
while self.pos < self.input.len()
&& ((self.input[self.pos] as char).is_ascii_digit() || self.input[self.pos] == b'.')
{
self.pos += 1;
}
let text = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
return Ok(JVal::Num(text.parse().unwrap_or(0.0)));
}
let word = self.peek_word();
match word.as_str() {
"" => Ok(JVal::Null),
"true" | "True" => Ok(JVal::Bool(true)),
"false" | "False" => Ok(JVal::Bool(false)),
"none" | "None" | "null" => Ok(JVal::Null),
_ => Ok(ctx.lookup(&word)),
}
}
}
/// Poor man's `%`-format: only `%0.2f`, `%d`, `%s` and `%x.yf` are needed.
fn format_c(value: &JVal, format: &str) -> String {
if let Some(spec) = format.strip_prefix('%') {
let spec = spec
.trim_end_matches('f')
.trim_end_matches('d')
.trim_end_matches('s');
let precision = spec
.split('.')
.nth(1)
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(6);
let width = spec
.split('.')
.next()
.and_then(|w| w.trim_start_matches('0').parse::<usize>().ok())
.unwrap_or(0);
if format.ends_with('d') {
return format!("{:>width$}", value.as_num() as i64);
}
if format.ends_with('s') {
return value.to_text();
}
return format!("{:>width$.precision$}", value.as_num());
}
value.to_text()
}
struct EvalContext<'a> {
api: &'a Api<'a>,
variables: Vec<IndexMap<String, JVal>>,
loop_var: Option<IndexMap<String, JVal>>,
}
impl<'a> EvalContext<'a> {
fn lookup(&self, name: &str) -> JVal {
if name == "loop"
&& let Some(loop_var) = &self.loop_var
{
return JVal::Map(loop_var.clone());
}
if name == "gl" {
return self.api.object();
}
for scope in self.variables.iter().rev() {
if let Some(value) = scope.get(name) {
return value.clone();
}
}
JVal::Null
}
fn call_builtin(&self, name: &str, args: &[JVal], kwargs: &IndexMap<String, JVal>) -> JVal {
match name {
"bar" => {
let value = args.first().map(|v| v.as_num()).unwrap_or(0.0);
JVal::Str(glances_core::units::bar_custom(value, 18, "■", "□", false))
}
"auto_unit" => {
let value = args.first().map(|v| v.as_num()).unwrap_or(0.0);
let low_precision = kwargs
.get("low_precision")
.map(|v| v.truthy())
.unwrap_or(false);
let min_symbol = kwargs
.get("min_symbol")
.map(|v| v.to_text())
.unwrap_or_else(|| "K".to_string());
JVal::Str(glances_core::auto_unit(
value,
low_precision,
&min_symbol,
"-",
))
}
"top_process" => {
let sorted_by = kwargs
.get("sorted_by")
.map(|v| v.to_text())
.unwrap_or_else(|| "cpu_percent".to_string());
let sorted_by_secondary = kwargs
.get("sorted_by_secondary")
.map(|v| v.to_text())
.unwrap_or_else(|| "memory_percent".to_string());
let limit = args.first().map(|v| v.as_num() as usize).unwrap_or(3);
self.api
.top_process(&sorted_by, &sorted_by_secondary, limit)
}
_ => JVal::Null,
}
}
}
fn render_nodes(nodes: &[Node], ctx: &mut EvalContext) -> Result<String> {
let mut out = String::new();
for node in nodes {
match node {
Node::Text(text) => out.push_str(text),
Node::Expr(expr) => {
let mut parser = ExprParser::new(expr);
let value = parser.parse(ctx)?;
out.push_str(&value.to_text());
}
Node::For {
var,
iterable,
body,
} => {
let mut parser = ExprParser::new(iterable);
let collection = parser.parse(ctx)?;
let items: Vec<JVal> = match collection {
JVal::List(items) => items,
JVal::Map(map) => map.keys().map(|k| JVal::Str(k.clone())).collect(),
_ => Vec::new(),
};
let length = items.len();
for (index, item) in items.into_iter().enumerate() {
let mut scope = IndexMap::new();
scope.insert(var.clone(), item);
let mut loop_var = IndexMap::new();
loop_var.insert("index".to_string(), JVal::Num((index + 1) as f64));
loop_var.insert("index0".to_string(), JVal::Num(index as f64));
loop_var.insert("first".to_string(), JVal::Bool(index == 0));
loop_var.insert("last".to_string(), JVal::Bool(index + 1 == length));
loop_var.insert("length".to_string(), JVal::Num(length as f64));
ctx.variables.push(scope);
ctx.loop_var = Some(loop_var);
out.push_str(&render_nodes(body, ctx)?);
ctx.variables.pop();
ctx.loop_var = None;
}
}
Node::If {
cond,
then_body,
else_body,
} => {
let mut parser = ExprParser::new(cond);
let value = parser.parse(ctx)?;
if value.truthy() {
out.push_str(&render_nodes(then_body, ctx)?);
} else {
out.push_str(&render_nodes(else_body, ctx)?);
}
}
}
}
Ok(out)
}
/// Run the fetch mode: render the template and exit.
pub fn run_fetch(engine: &mut Engine, args: &Args) -> Result<()> {
engine.update();
let template = if args.fetch_template.is_empty() {
DEFAULT_TEMPLATE.to_string()
} else {
std::fs::read_to_string(&args.fetch_template).map_err(|e| {
anyhow::anyhow!("cannot read fetch template '{}': {e}", args.fetch_template)
})?
};
let nodes = parse_template(&template)?;
let api = Api::new(engine, args);
let mut ctx = EvalContext {
api: &api,
variables: Vec::new(),
loop_var: None,
};
let output = render_nodes(&nodes, &mut ctx)?;
print!("{output}");
Ok(())
}
#[allow(dead_code)]
fn unused(_: &dyn Plugin, _: &str) -> String {
py_str(&Value::Null)
}
+15 -8
View File
@@ -9,15 +9,22 @@ pub fn log_filename() -> PathBuf {
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".local").join("share")));
if let Some(base) = base {
if base.is_dir() {
let dir = base.join("glances");
let _ = std::fs::create_dir_all(&dir);
return dir.join("glances.log");
}
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".local").join("share"))
});
if let Some(base) = base
&& base.is_dir()
{
let dir = base.join("glances");
let _ = std::fs::create_dir_all(&dir);
return dir.join("glances.log");
}
std::env::temp_dir().join(format!("glances-{}.log", std::env::var("USER").unwrap_or_else(|_| "user".into())))
std::env::temp_dir().join(format!(
"glances-{}.log",
std::env::var("USER").unwrap_or_else(|_| "user".into())
))
}
/// Initialise the logger: everything goes to the log file, warnings and
+44 -18
View File
@@ -1,13 +1,13 @@
//! The `glances` binary.
mod fetch;
mod logging;
mod outputs;
mod tui_placeholder;
use anyhow::Result;
use clap::{CommandFactory, Parser};
use glances_core::config::Config;
use glances_core::Args;
use glances_core::config::Config;
use glances_plugins::engine::Engine;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -21,7 +21,10 @@ fn main() {
let args = Args::parse();
if args.version {
println!("{}", args.version_message(&logging::log_filename().display().to_string()));
println!(
"{}",
args.version_message(&logging::log_filename().display().to_string())
);
return;
}
if let Some(shell) = &args.print_completion {
@@ -85,7 +88,7 @@ fn run(mut args: Args) -> Result<()> {
}
if args.stdout_fetch {
return tui_placeholder::run_fetch(&mut engine, &args);
return fetch::run_fetch(&mut engine, &args);
}
if args.quiet {
@@ -105,11 +108,28 @@ fn display_modules_list(engine: &Engine) {
fn print_completion(shell: &str) {
let mut command = Args::command();
match shell {
"bash" => clap_complete::generate(clap_complete::Shell::Bash, &mut command, "glances", &mut std::io::stdout()),
"zsh" => clap_complete::generate(clap_complete::Shell::Zsh, &mut command, "glances", &mut std::io::stdout()),
"fish" => clap_complete::generate(clap_complete::Shell::Fish, &mut command, "glances", &mut std::io::stdout()),
"bash" => clap_complete::generate(
clap_complete::Shell::Bash,
&mut command,
"glances",
&mut std::io::stdout(),
),
"zsh" => clap_complete::generate(
clap_complete::Shell::Zsh,
&mut command,
"glances",
&mut std::io::stdout(),
),
"fish" => clap_complete::generate(
clap_complete::Shell::Fish,
&mut command,
"glances",
&mut std::io::stdout(),
),
other => {
eprintln!("glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)");
eprintln!(
"glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)"
);
std::process::exit(2);
}
}
@@ -134,13 +154,19 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
let mut csv = outputs::CsvRenderer::new();
let stdout_items = args.stdout.as_deref().map(outputs::build_stdout_list);
let json_plugins = args.stdout_json.as_deref().map(|value| {
value.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::<Vec<_>>()
value
.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
});
let csv_plugins = args.stdout_csv.as_deref().map(|value| {
value
.split(',')
.map(|p| match p.split_once('.') {
Some((plugin, attribute)) => (plugin.trim().to_string(), Some(attribute.to_string())),
Some((plugin, attribute)) => {
(plugin.trim().to_string(), Some(attribute.to_string()))
}
None => (p.trim().to_string(), None),
})
.filter(|(p, _)| !p.is_empty())
@@ -164,10 +190,10 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
std::io::stdout().flush().ok();
count += 1;
if let Some(max) = iterations {
if count >= max {
break;
}
if let Some(max) = iterations
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
@@ -182,10 +208,10 @@ fn run_quiet(engine: &mut Engine, args: &Args) -> Result<()> {
let start = Instant::now();
engine.update();
count += 1;
if let Some(max) = args.stop_after {
if count >= max {
break;
}
if let Some(max) = args.stop_after
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
+56 -15
View File
@@ -17,7 +17,11 @@ pub fn build_stdout_list(value: &str) -> Vec<StdoutItem> {
.map(|item| {
let parts: Vec<&str> = item.split('.').collect();
match parts.len() {
1 => StdoutItem { plugin: parts[0].to_string(), key: None, attribute: None },
1 => StdoutItem {
plugin: parts[0].to_string(),
key: None,
attribute: None,
},
2 => StdoutItem {
plugin: parts[0].to_string(),
key: None,
@@ -48,9 +52,18 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
Some(attribute) => match &stat {
Value::Object(map) => {
if let Some(value) = map.get(attribute) {
out.push_str(&format!("{}.{}: {}\n", item.plugin, attribute, glances_core::py_str(value)));
out.push_str(&format!(
"{}.{}: {}\n",
item.plugin,
attribute,
glances_core::py_str(value)
));
} else {
tracing::error!("Can not display stat {}.{} (KeyError)", item.plugin, attribute);
tracing::error!(
"Can not display stat {}.{} (KeyError)",
item.plugin,
attribute
);
}
}
Value::Array(list) => {
@@ -66,10 +79,10 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
let Some(entry_key) = entry_key else {
continue;
};
if let Some(wanted) = &item.key {
if wanted != &entry_key {
continue;
}
if let Some(wanted) = &item.key
&& wanted != &entry_key
{
continue;
}
if let Some(value) = entry.get(attribute) {
out.push_str(&format!(
@@ -85,7 +98,11 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
_ => {}
},
None => {
out.push_str(&format!("{}: {}\n", item.plugin, glances_core::py_repr(&stat)));
out.push_str(&format!(
"{}: {}\n",
item.plugin,
glances_core::py_repr(&stat)
));
}
}
}
@@ -116,7 +133,10 @@ pub struct CsvRenderer {
impl CsvRenderer {
pub fn new() -> Self {
Self { header: true, ..Default::default() }
Self {
header: true,
..Default::default()
}
}
pub fn render(&mut self, engine: &Engine, items: &[(String, Option<String>)]) -> String {
@@ -172,7 +192,8 @@ impl CsvRenderer {
}
self.list_keys.insert(plugin.to_string(), keys_order);
if let Some(first) = list.iter().find_map(|entry| entry.as_object()) {
self.header_field_names.insert(plugin.to_string(), first.keys().cloned().collect());
self.header_field_names
.insert(plugin.to_string(), first.keys().cloned().collect());
}
}
_ => {
@@ -186,7 +207,10 @@ impl CsvRenderer {
let mut line = String::new();
if let Some(attribute) = attribute {
let text = match stat {
Value::Object(map) => map.get(attribute).map(glances_core::py_str).unwrap_or_else(|| "N/A".to_string()),
Value::Object(map) => map
.get(attribute)
.map(glances_core::py_str)
.unwrap_or_else(|| "N/A".to_string()),
_ => "N/A".to_string(),
};
line.push_str(&format!("{text},"));
@@ -213,9 +237,19 @@ impl CsvRenderer {
current.insert(glances_core::py_str(identity), entry.clone());
}
let empty = Map::new();
for identity in self.list_keys.get(plugin).map(|k| k.as_slice()).unwrap_or(&[]) {
for identity in self
.list_keys
.get(plugin)
.map(|k| k.as_slice())
.unwrap_or(&[])
{
let entry = current.get(identity).unwrap_or(&empty);
for field in self.header_field_names.get(plugin).map(|f| f.as_slice()).unwrap_or(&[]) {
for field in self
.header_field_names
.get(plugin)
.map(|f| f.as_slice())
.unwrap_or(&[])
{
let value = entry.get(field.as_str());
let text = match value {
Some(value) => glances_core::py_str(value),
@@ -241,8 +275,15 @@ pub fn render_issue(engine: &Engine) -> String {
let Some(plugin) = engine.get_plugin(&name) else {
continue;
};
let enabled = if plugin.is_enabled() { "enabled" } else { "disabled" };
out.push_str(&format!("{name} ({enabled}): {}\n", glances_core::py_repr(plugin.stats())));
let enabled = if plugin.is_enabled() {
"enabled"
} else {
"disabled"
};
out.push_str(&format!(
"{name} ({enabled}): {}\n",
glances_core::py_repr(plugin.stats())
));
}
out
}
-10
View File
@@ -1,10 +0,0 @@
//! Temporary placeholder for the terminal UI and the fetch mode.
//! The real TUI lives in the `glances-tui` crate (next milestone).
use anyhow::Result;
use glances_core::Args;
use glances_plugins::engine::Engine;
pub fn run_fetch(_engine: &mut Engine, _args: &Args) -> Result<()> {
anyhow::bail!("fetch mode is not implemented yet")
}
+35 -8
View File
@@ -35,12 +35,20 @@ fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result<String,
let Some(close) = stripped.find("}}}") else {
return Err("unclosed mustache tag".to_string());
};
(stripped[..close].trim().to_string(), true, tag_start + 1 + close + 3)
(
stripped[..close].trim().to_string(),
true,
tag_start + 1 + close + 3,
)
} else {
let Some(close) = after.find("}}") else {
return Err("unclosed mustache tag".to_string());
};
(after[..close].trim().to_string(), false, tag_start + close + 2)
(
after[..close].trim().to_string(),
false,
tag_start + close + 2,
)
};
if let Some(name) = tag.strip_prefix('#') {
@@ -199,7 +207,8 @@ impl Actions {
}
pub fn set(&mut self, stat_name: &str, criticality: &str) {
self.status.insert(stat_name.to_string(), criticality.to_string());
self.status
.insert(stat_name.to_string(), criticality.to_string());
}
pub fn get(&self, stat_name: &str) -> Option<&str> {
@@ -254,8 +263,14 @@ mod tests {
#[test]
fn test_render_simple() {
let data = json!({"name": "nginx", "pid": 42});
assert_eq!(render_template("kill {{pid}} {{name}}", &data).unwrap(), "kill 42 nginx");
assert_eq!(render_template("raw {{{name}}}", &data).unwrap(), "raw nginx");
assert_eq!(
render_template("kill {{pid}} {{name}}", &data).unwrap(),
"kill 42 nginx"
);
assert_eq!(
render_template("raw {{{name}}}", &data).unwrap(),
"raw nginx"
);
}
#[test]
@@ -268,13 +283,25 @@ mod tests {
#[test]
fn test_render_section() {
let data = json!({"items": [1, 2]});
assert_eq!(render_template("{{#items}}x{{/items}}", &data).unwrap(), "xx");
assert_eq!(render_template("{{^missing}}y{{/missing}}", &data).unwrap(), "y");
assert_eq!(
render_template("{{#items}}x{{/items}}", &data).unwrap(),
"xx"
);
assert_eq!(
render_template("{{^missing}}y{{/missing}}", &data).unwrap(),
"y"
);
}
#[test]
fn test_actions_no_trigger_before_start_timer() {
let mut actions = Actions::new(false, 100.0);
assert!(!actions.run("cpu_user", "critical", &["/bin/true".to_string()], false, None));
assert!(!actions.run(
"cpu_user",
"critical",
&["/bin/true".to_string()],
false,
None
));
}
}
+14 -7
View File
@@ -379,8 +379,14 @@ impl Args {
(self.server, "server mode (-s/--server)"),
(self.browser, "the server browser (--browser)"),
(self.export.is_some(), "export modules (--export)"),
(self.stdout_api_doc, "the Python API documentation (--api-doc)"),
(self.stdout_api_restful_doc, "the RESTful API documentation (--api-restful-doc)"),
(
self.stdout_api_doc,
"the Python API documentation (--api-doc)",
),
(
self.stdout_api_restful_doc,
"the RESTful API documentation (--api-restful-doc)",
),
(self.trace_malloc, "Python memory tracing (--trace-malloc)"),
];
checks.iter().find(|(used, _)| *used).map(|(_, name)| *name)
@@ -398,10 +404,7 @@ impl Args {
pub fn version_message(&self, log_file: &str) -> String {
format!(
"Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}",
"4.5.7",
GLANCES_API_VERSION,
GLANCES_VERSION,
log_file
"4.5.7", GLANCES_API_VERSION, GLANCES_VERSION, log_file
)
}
}
@@ -492,7 +495,11 @@ mod tests {
assert!(parse(&["glances", "-w"]).unsupported().is_some());
assert!(parse(&["glances", "-s"]).unsupported().is_some());
assert!(parse(&["glances", "-c", "host"]).unsupported().is_some());
assert!(parse(&["glances", "--export", "csv"]).unsupported().is_some());
assert!(
parse(&["glances", "--export", "csv"])
.unsupported()
.is_some()
);
assert!(parse(&["glances"]).unsupported().is_none());
}
+57 -24
View File
@@ -34,8 +34,11 @@ impl Config {
/// Build an empty configuration (used by tests and by callers that
/// already read a file).
pub fn empty(disable_config_exec: bool) -> Self {
let mut config = Config::default();
config.disable_config_exec = disable_config_exec;
let mut config = Config {
sections: IndexMap::new(),
loaded_file: None,
disable_config_exec,
};
config.set_defaults();
config
}
@@ -64,7 +67,11 @@ impl Config {
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config")));
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".config"))
});
if let Some(base) = user_base {
paths.push(base.join("glances").join(CONFIG_FILENAME));
}
@@ -74,12 +81,12 @@ impl Config {
// Default location: <prefix>/share/doc/glances (and the checkout
// layout next to the executable, useful while developing).
if let Ok(exe) = std::env::current_exe() {
if let Some(bin_dir) = exe.parent() {
for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME));
paths.push(prefix.join("conf").join(CONFIG_FILENAME));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(bin_dir) = exe.parent()
{
for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME));
paths.push(prefix.join("conf").join(CONFIG_FILENAME));
}
}
@@ -96,7 +103,10 @@ impl Config {
tracing::info!("Read configuration file '{}'", config_file.display());
}
Err(err) => {
tracing::error!("Can not read configuration file '{}': {err}", config_file.display());
tracing::error!(
"Can not read configuration file '{}': {err}",
config_file.display()
);
std::process::exit(1);
}
}
@@ -123,10 +133,19 @@ impl Config {
self.set_default_cwc("cpu", Some("steal"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]);
// Context switches bottleneck identification #1212
let ctx_switches_bottleneck =
500000.0 * 0.10 * std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64;
let careful = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20));
let warning = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10));
let ctx_switches_bottleneck = 500000.0
* 0.10
* std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1) as f64;
let careful = format!(
"{}",
ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)
);
let warning = format!(
"{}",
ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)
);
let critical = format!("{ctx_switches_bottleneck}");
self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]);
@@ -159,7 +178,12 @@ impl Config {
self.set_default_cwc("processlist", Some("mem"), ["50", "70", "90"]);
}
fn set_default_cwc<const N: usize>(&mut self, section: &str, option_header: Option<&str>, cwc: [&str; N]) {
fn set_default_cwc<const N: usize>(
&mut self,
section: &str,
option_header: Option<&str>,
cwc: [&str; N],
) {
let header = match option_header {
Some(header) => format!("{header}_"),
None => String::new(),
@@ -171,7 +195,9 @@ impl Config {
fn set_default(&mut self, section: &str, option: &str, default: &str) {
let entry = self.sections.entry(section.to_string()).or_default();
entry.entry(option.to_string()).or_insert_with(|| default.to_string());
entry
.entry(option.to_string())
.or_insert_with(|| default.to_string());
}
pub fn loaded_config_file(&self) -> Option<&Path> {
@@ -215,7 +241,9 @@ impl Config {
if let Some(value) = ret.as_mut() {
for command in find_backticks(value) {
if self.disable_config_exec {
tracing::warn!("Config exec disabled: skipping command `{command}` in [{section}] {option}");
tracing::warn!(
"Config exec disabled: skipping command `{command}` in [{section}] {option}"
);
} else {
tracing::warn!("Executing config command `{command}` for [{section}] {option}");
let output = system_exec(&command);
@@ -294,11 +322,11 @@ fn parse_ini(content: &str) -> IndexMap<String, IndexMap<String, String>> {
}
// Continuation line (indented).
if line.starts_with(' ') || line.starts_with('\t') {
if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref()) {
if let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key)) {
value.push('\n');
value.push_str(trimmed);
}
if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref())
&& let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key))
{
value.push('\n');
value.push_str(trimmed);
}
continue;
}
@@ -341,7 +369,9 @@ fn system_exec(command: &str) -> String {
return String::new();
};
match Command::new(program).args(args).output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).trim_end().to_string(),
Ok(output) => String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_string(),
Err(e) => format!("ERROR: {e}"),
}
}
@@ -387,6 +417,9 @@ mod tests {
let config = Config::from_str("[x]\nprefix=`echo hello`\n", false);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello");
let config = Config::from_str("[x]\nprefix=`echo hello`\n", true);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "`echo hello`");
assert_eq!(
config.get_value("x", "prefix", None).unwrap(),
"`echo hello`"
);
}
}
+52 -10
View File
@@ -13,7 +13,11 @@ const TREE: [(&str, &[&str], u32); 9] = [
("High CPU user mode", &["cpu_user"], 2),
("High CPU kernel usage", &["cpu_system"], 2),
("High CPU I/O waiting", &["cpu_iowait"], 2),
("Large CPU stolen time. System running the hypervisor is too busy.", &["cpu_steal"], 2),
(
"Large CPU stolen time. System running the hypervisor is too busy.",
&["cpu_steal"],
2,
),
("High CPU niced value", &["cpu_niced"], 2),
("System overloaded in the last 5 minutes", &["load"], 2),
("High swap (paging) usage", &["memswap"], 2),
@@ -28,14 +32,22 @@ pub fn build_global_message() -> String {
let mut best_min = 0u32;
let mut best_msg = TREE[0].0;
for (msg, names, min) in TREE {
let weight: u32 = names.iter().filter_map(|n| current.get(*n)).map(|t| t.value() as u32).sum();
let weight: u32 = names
.iter()
.filter_map(|n| current.get(*n))
.map(|t| t.value() as u32)
.sum();
if weight > best_weight {
best_weight = weight;
best_min = min;
best_msg = msg;
}
}
if best_weight >= best_min { best_msg.to_string() } else { TREE[0].0.to_string() }
if best_weight >= best_min {
best_msg.to_string()
} else {
TREE[0].0.to_string()
}
}
#[derive(Debug, Clone)]
@@ -106,11 +118,16 @@ impl Event {
let mut sorted: Vec<Value> = proc_list.to_vec();
sort_stats(&mut sorted, sort_key, "memory_percent", true);
for process in sorted.iter().take(6) {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
let name = process
.get("name")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string();
*self.top_dict.entry(name).or_insert(0) += 1;
}
let mut counts: Vec<(String, i64)> = self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect();
counts.sort_by(|a, b| b.1.cmp(&a.1));
let mut counts: Vec<(String, i64)> =
self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect();
counts.sort_by_key(|entry| std::cmp::Reverse(entry.1));
self.top = counts.into_iter().take(3).map(|(name, _)| name).collect();
} else {
self.top_dict.clear();
@@ -136,7 +153,12 @@ impl Default for EventsList {
impl EventsList {
pub fn new(max_events: usize, min_duration: f64, min_interval: f64) -> Self {
Self { events: Vec::new(), max_events, min_duration, min_interval }
Self {
events: Vec::new(),
max_events,
min_duration,
min_interval,
}
}
pub fn get(&self) -> Vec<Value> {
@@ -184,14 +206,27 @@ impl EventsList {
}
/// Add a new item to the events list.
pub fn add(&mut self, event_state: &str, event_type: &str, event_value: f64, proc_desc: &str) -> usize {
pub fn add(
&mut self,
event_state: &str,
event_type: &str,
event_value: f64,
proc_desc: &str,
) -> usize {
let event_time = chrono::Utc::now().timestamp() as f64;
let global_message = build_global_message();
let proc_list: Vec<Value> = processes().lock().unwrap().get_list(false).to_vec();
match self.event_exist(event_time, event_type) {
None => {
self.create_event(event_time, event_state, event_type, event_value, proc_desc, &global_message);
self.create_event(
event_time,
event_state,
event_type,
event_value,
proc_desc,
&global_message,
);
}
Some(index) => {
self.update_event(
@@ -266,7 +301,14 @@ impl EventsList {
Self::set_process_sort(event_type);
let sort_key = Self::get_event_sort_key(event_type).to_string();
let event = &mut self.events[event_index];
event.update(event_state, event_value, &sort_key, proc_list, proc_desc, global_message);
event.update(
event_state,
event_value,
&sort_key,
proc_list,
proc_desc,
global_message,
);
}
}
+17 -4
View File
@@ -74,9 +74,14 @@ impl Filter {
return false;
}
if self.filter_key.is_none() {
return self.filter_matches(process, "name", filter_re) || self.filter_matches(process, "cmdline", filter_re);
return self.filter_matches(process, "name", filter_re)
|| self.filter_matches(process, "cmdline", filter_re);
}
self.filter_matches(process, self.filter_key.as_deref().unwrap_or("name"), filter_re)
self.filter_matches(
process,
self.filter_key.as_deref().unwrap_or("name"),
filter_re,
)
}
fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool {
@@ -89,13 +94,21 @@ impl Filter {
if key == "cmdline" && !items.is_empty() {
items[0].as_str().unwrap_or_default().to_string()
} else {
items.iter().map(|i| i.as_str().unwrap_or_default()).collect::<Vec<_>>().join(" ")
items
.iter()
.map(|i| i.as_str().unwrap_or_default())
.collect::<Vec<_>>()
.join(" ")
}
}
Value::Null => return false,
other => crate::units::py_str(other),
};
filter_re.is_match(&text) && filter_re.find(&text).map(|m| m.len() == text.len()).unwrap_or(false)
filter_re.is_match(&text)
&& filter_re
.find(&text)
.map(|m| m.len() == text.len())
.unwrap_or(false)
}
}
+32 -6
View File
@@ -15,7 +15,11 @@ pub struct Attribute {
}
impl Attribute {
pub fn new(name: impl Into<String>, description: impl Into<String>, history_max_size: Option<usize>) -> Self {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
history_max_size: Option<usize>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
@@ -44,7 +48,15 @@ impl Attribute {
if nb == 0 {
self.history.iter().cloned().collect()
} else {
self.history.iter().rev().take(nb).collect::<Vec<_>>().into_iter().rev().cloned().collect()
self.history
.iter()
.rev()
.take(nb)
.collect::<Vec<_>>()
.into_iter()
.rev()
.cloned()
.collect()
}
}
@@ -63,7 +75,13 @@ pub struct History {
}
impl History {
pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option<usize>) {
pub fn add(
&mut self,
key: &str,
value: f64,
description: &str,
history_max_size: Option<usize>,
) {
let attribute = self
.attributes
.entry(key.to_string())
@@ -78,16 +96,24 @@ impl History {
}
pub fn get(&self, nb: usize) -> IndexMap<String, Vec<(DateTime<Utc>, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_raw(nb))).collect()
self.attributes
.iter()
.map(|(key, attr)| (key.clone(), attr.history_raw(nb)))
.collect()
}
pub fn get_json(&self, nb: usize) -> IndexMap<String, Vec<(String, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_json(nb))).collect()
self.attributes
.iter()
.map(|(key, attr)| (key.clone(), attr.history_json(nb)))
.collect()
}
/// Return the values of one item (used for TUI trends).
pub fn item_values(&self, item: &str) -> Option<Vec<f64>> {
self.attributes.get(item).map(|a| a.history.iter().map(|(_, v)| *v).collect())
self.attributes
.get(item)
.map(|a| a.history.iter().map(|(_, v)| *v).collect())
}
}
+2
View File
@@ -1,5 +1,7 @@
//! glances-core: shared foundations of the glances-rs rewrite.
//!
#![allow(clippy::type_complexity)]
//! This crate contains the pieces that are common to every plugin and output:
//! argument parsing, configuration file handling, the plugin model (limits,
//! views, history, rates...), formatting helpers, the event/threshold/action
+246 -88
View File
@@ -161,7 +161,10 @@ pub struct Field {
impl Field {
pub fn new(description: &str) -> Self {
Self { description: description.to_string(), ..Default::default() }
Self {
description: description.to_string(),
..Default::default()
}
}
pub fn unit(mut self, unit: &str) -> Self {
@@ -208,7 +211,9 @@ impl Field {
pub fn unit_type(&self) -> &'static str {
match self.unit.as_deref() {
Some("percent" | "percents") => "float",
Some("number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes") => "int",
Some(
"number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes",
) => "int",
_ => "float",
}
}
@@ -233,7 +238,10 @@ pub struct HistoryItem {
impl HistoryItem {
pub fn new(name: &str, description: &str) -> Self {
Self { name: name.to_string(), description: description.to_string() }
Self {
name: name.to_string(),
description: description.to_string(),
}
}
}
@@ -357,11 +365,16 @@ impl PluginModel {
/// Load limits from the configuration file.
pub fn load_limits(&mut self, config: &Config) {
self.limits.clear();
self.limits.insert("history_size".to_string(), Limit::Num(HISTORY_MAX_SIZE as f64));
self.limits.insert(
"history_size".to_string(),
Limit::Num(HISTORY_MAX_SIZE as f64),
);
if config.has_section("global") {
let history_size = config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64);
self.limits.insert("history_size".to_string(), Limit::Num(history_size));
let history_size =
config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64);
self.limits
.insert("history_size".to_string(), Limit::Num(history_size));
}
if config.has_section(&self.name) {
@@ -372,7 +385,8 @@ impl PluginModel {
self.limits.insert(limit.clone(), Limit::Num(number));
}
Err(_) => {
let items: Vec<String> = value.split(',').map(|s| s.trim().to_string()).collect();
let items: Vec<String> =
value.split(',').map(|s| s.trim().to_string()).collect();
self.limits.insert(limit.clone(), Limit::List(items));
}
}
@@ -389,7 +403,8 @@ impl PluginModel {
}
pub fn set_refresh(&mut self, value: f64) {
self.limits.insert(format!("{}_refresh", self.name), Limit::Num(value));
self.limits
.insert(format!("{}_refresh", self.name), Limit::Num(value));
}
pub fn get_refresh(&self) -> f64 {
@@ -424,12 +439,19 @@ impl PluginModel {
}
pub fn is_limit(&self, criticality: &str, stat_name: &str) -> bool {
let key = format!("{}_{criticality}", self.get_stat_name(Some(stat_name), None).to_lowercase());
let key = format!(
"{}_{criticality}",
self.get_stat_name(Some(stat_name), None).to_lowercase()
);
self.limits.contains_key(&key)
}
/// Return the (commands, repeat) action pair for the alert.
pub fn get_limit_action(&self, criticality: &str, stat_name: &str) -> (Option<Vec<String>>, bool) {
pub fn get_limit_action(
&self,
criticality: &str,
stat_name: &str,
) -> (Option<Vec<String>>, bool) {
let candidates = [
(format!("{stat_name}_{criticality}_action"), false),
(format!("{stat_name}_{criticality}_action_repeat"), true),
@@ -446,10 +468,18 @@ impl PluginModel {
pub fn get_limit_log(&self, stat_name: &str, default_action: bool) -> bool {
if let Some(limit) = self.limits.get(&format!("{stat_name}_log")) {
return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false);
return limit
.as_list()
.and_then(|l| l.first())
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
}
if let Some(limit) = self.limits.get(&format!("{}_log", self.name)) {
return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false);
return limit
.as_list()
.and_then(|l| l.first())
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
}
default_action
}
@@ -477,7 +507,10 @@ impl PluginModel {
key = format!("{key}_{header}");
}
match self.limits.get(&format!("{key}_{value}")) {
Some(Limit::List(items)) => items.first().map(|v| v.to_lowercase() == "true").unwrap_or(false),
Some(Limit::List(items)) => items
.first()
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false),
Some(Limit::Num(n)) => *n != 0.0,
None => false,
}
@@ -487,7 +520,11 @@ impl PluginModel {
pub fn is_show(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value);
self.get_conf_value("show", header).iter().any(|pattern| {
fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false)
fullmatch(pattern, value)
|| alias
.as_deref()
.map(|a| fullmatch(pattern, a))
.unwrap_or(false)
})
}
@@ -495,7 +532,11 @@ impl PluginModel {
pub fn is_hide(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value);
self.get_conf_value("hide", header).iter().any(|pattern| {
fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false)
fullmatch(pattern, value)
|| alias
.as_deref()
.map(|a| fullmatch(pattern, a))
.unwrap_or(false)
})
}
@@ -537,17 +578,17 @@ impl PluginModel {
pub fn get_stat_name(&self, header: Option<&str>, action_key: Option<&str>) -> String {
let mut ret = self.name.clone();
if let Some(key) = action_key {
if !key.is_empty() {
ret.push('_');
ret.push_str(key);
}
if let Some(key) = action_key
&& !key.is_empty()
{
ret.push('_');
ret.push_str(key);
}
if let Some(header) = header {
if !header.is_empty() {
ret.push('_');
ret.push_str(header);
}
if let Some(header) = header
&& !header.is_empty()
{
ret.push('_');
ret.push_str(header);
}
ret
}
@@ -560,15 +601,32 @@ impl PluginModel {
.map(|(name, _)| name.clone())
.collect();
for field_name in mmm_names {
let unit = self.fields.get(&field_name).and_then(|f| f.unit.clone()).unwrap_or_default();
let unit = self
.fields
.get(&field_name)
.and_then(|f| f.unit.clone())
.unwrap_or_default();
self.mmm.insert(
field_name.clone(),
MmmState { values: VecDeque::new(), min: None, max: None },
MmmState {
values: VecDeque::new(),
min: None,
max: None,
},
);
for (suffix, description) in [
("_min", format!("Minimum {field_name} observed since Glances startup.")),
("_max", format!("Maximum {field_name} observed since Glances startup.")),
("_mean", format!("Mean (average) {field_name} computed from the history.")),
(
"_min",
format!("Minimum {field_name} observed since Glances startup."),
),
(
"_max",
format!("Maximum {field_name} observed since Glances startup."),
),
(
"_mean",
format!("Mean (average) {field_name} computed from the history."),
),
] {
let generated = format!("{field_name}{suffix}");
let unit = unit.clone();
@@ -604,7 +662,10 @@ impl PluginModel {
let mean = state.values.iter().sum::<f64>() / state.values.len() as f64;
map.insert(format!("{field_name}_min"), json!(min));
map.insert(format!("{field_name}_max"), json!(max));
map.insert(format!("{field_name}_mean"), json!((mean * 100.0).round() / 100.0));
map.insert(
format!("{field_name}_mean"),
json!((mean * 100.0).round() / 100.0),
);
}
}
@@ -623,7 +684,8 @@ impl PluginModel {
.map(|list| {
list.iter()
.filter_map(|item| {
item.get(key).map(|v| (crate::units::py_str(v), item.clone()))
item.get(key)
.map(|v| (crate::units::py_str(v), item.clone()))
})
.collect()
})
@@ -633,7 +695,7 @@ impl PluginModel {
for item in items.iter_mut() {
let previous_item = key
.and_then(|k| item.get(k))
.map(|v| crate::units::py_str(v))
.map(crate::units::py_str)
.and_then(|k| previous_by_key.get(&k).cloned())
.unwrap_or(Value::Object(Map::new()));
self.compute_rate_on_stat(item, Some(&previous_item));
@@ -660,13 +722,20 @@ impl PluginModel {
let Value::Object(map) = stat else {
return;
};
let rate_fields: Vec<String> =
self.fields.iter().filter(|(_, f)| f.rate).map(|(name, _)| name.clone()).collect();
let rate_fields: Vec<String> = self
.fields
.iter()
.filter(|(_, f)| f.rate)
.map(|(name, _)| name.clone())
.collect();
for field in rate_fields {
if !map.contains_key(&field) {
continue;
}
map.insert("time_since_update".to_string(), json!(self.time_since_last_update));
map.insert(
"time_since_update".to_string(),
json!(self.time_since_last_update),
);
let current_value = map.get(&field).cloned().unwrap_or(Value::Null);
map.insert(format!("{field}_gauge"), current_value.clone());
let current = as_f64(&current_value);
@@ -703,7 +772,10 @@ impl PluginModel {
None => self.views.get(field),
Some(item) => self.views.get(item).and_then(|v| v.get(field)),
};
item_views.and_then(|v| v.get(option)).cloned().unwrap_or_else(|| json!("DEFAULT"))
item_views
.and_then(|v| v.get(option))
.cloned()
.unwrap_or_else(|| json!("DEFAULT"))
}
pub fn view_decoration(&self, item: Option<&str>, field: &str) -> Decoration {
@@ -714,7 +786,9 @@ impl PluginModel {
}
pub fn view_bool(&self, item: Option<&str>, field: &str, option: &str) -> bool {
self.get_view(item, field, option).as_bool().unwrap_or(false)
self.get_view(item, field, option)
.as_bool()
.unwrap_or(false)
}
/// Override the decoration of a view (used by plugins which add specific
@@ -733,18 +807,37 @@ impl PluginModel {
let Some(definition) = self.fields.get(field).cloned() else {
return Decoration::Default;
};
if definition.rate && self.stats.is_object() {
if self.stats.get("time_since_update").and_then(as_f64) == Some(0.0) {
return Decoration::Default;
}
if definition.rate
&& self.stats.is_object()
&& self.stats.get("time_since_update").and_then(as_f64) == Some(0.0)
{
return Decoration::Default;
}
if definition.log {
let (current, minimum, maximum) = numeric_alert_args(value);
return self.get_alert(current, minimum, maximum, true, false, Some(field), None, true);
return self.get_alert(
current,
minimum,
maximum,
true,
false,
Some(field),
None,
true,
);
}
if definition.alert {
let (current, minimum, maximum) = numeric_alert_args(value);
return self.get_alert(current, minimum, maximum, true, false, Some(field), None, false);
return self.get_alert(
current,
minimum,
maximum,
true,
false,
Some(field),
None,
false,
);
}
Decoration::Default
}
@@ -756,14 +849,18 @@ impl PluginModel {
let hidden = if !self.hide_zero {
false
} else if let Some(key) = key {
if let Some(existing) = self.views.get(key).and_then(|v| v.get(field)).and_then(|v| v.get("hidden")) {
if let Some(existing) = self
.views
.get(key)
.and_then(|v| v.get(field))
.and_then(|v| v.get("hidden"))
{
let mut hidden = existing.as_bool().unwrap_or(false);
if self.hide_zero_fields.iter().any(|f| f == field) {
if let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v)) {
if raw > self.hide_threshold_bytes {
hidden = false;
}
}
if self.hide_zero_fields.iter().any(|f| f == field)
&& let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v))
&& raw > self.hide_threshold_bytes
{
hidden = false;
}
hidden
} else {
@@ -860,29 +957,31 @@ impl PluginModel {
let value = current * 100.0 / maximum;
let stat_name = self.get_stat_name(header, action_key).to_lowercase();
let mut ret = if is_max { Decoration::Max } else { Decoration::Ok };
let mut ret = if is_max {
Decoration::Max
} else {
Decoration::Ok
};
let critical = self.get_limit("critical", &stat_name);
let warning = self.get_limit("warning", &stat_name);
let careful = self.get_limit("careful", &stat_name);
if let Some(critical) = critical {
if value >= critical {
ret = Decoration::Critical;
}
if let Some(critical) = critical
&& value >= critical
{
ret = Decoration::Critical;
}
if ret == Decoration::Ok {
if let Some(warning) = warning {
if value >= warning {
ret = Decoration::Warning;
}
}
if ret == Decoration::Ok
&& let Some(warning) = warning
&& value >= warning
{
ret = Decoration::Warning;
}
if ret == Decoration::Ok {
if let Some(careful) = careful {
if value >= careful {
ret = Decoration::Careful;
}
}
if ret == Decoration::Ok
&& let Some(careful) = careful
&& value >= careful
{
ret = Decoration::Careful;
}
if ret == Decoration::Ok && careful.is_none() && warning.is_none() && critical.is_none() {
ret = Decoration::Default;
@@ -903,12 +1002,20 @@ impl PluginModel {
Decoration::Max => "MAX",
_ => "OK",
};
events().lock().unwrap().add(log_ret, &stat_name.to_uppercase(), value, "");
events()
.lock()
.unwrap()
.add(log_ret, &stat_name.to_uppercase(), value, "");
}
thresholds().lock().unwrap().add(&stat_name, ret.name());
self.manage_action(&stat_name, ret.name().to_lowercase().as_str(), header, action_key);
self.manage_action(
&stat_name,
ret.name().to_lowercase().as_str(),
header,
action_key,
);
match (ret, log_str) {
(Decoration::Ok, true) => Decoration::OkLog,
@@ -919,11 +1026,22 @@ impl PluginModel {
}
}
pub fn get_alert_log(&mut self, current: f64, maximum: f64, header: Option<&str>) -> Decoration {
pub fn get_alert_log(
&mut self,
current: f64,
maximum: f64,
header: Option<&str>,
) -> Decoration {
self.get_alert(current, 0.0, maximum, false, false, header, None, true)
}
fn manage_action(&mut self, stat_name: &str, trigger: &str, header: Option<&str>, action_key: Option<&str>) {
fn manage_action(
&mut self,
stat_name: &str,
trigger: &str,
header: Option<&str>,
action_key: Option<&str>,
) {
let (command, repeat) = self.get_limit_action(trigger, stat_name);
match command {
None => {
@@ -935,12 +1053,11 @@ impl PluginModel {
let mut mustache_dict = match &stats_action {
Value::Array(items) if action_key.is_some() => {
let action_key = action_key.unwrap();
let key_field = self.item_key.clone();
let key_field = self.item_key;
items
.iter()
.find(|item| {
key_field
.as_deref()
.and_then(|k| item.get(k))
.map(crate::units::py_str)
.as_deref()
@@ -953,15 +1070,29 @@ impl PluginModel {
_ => Value::Object(Map::new()),
};
if let Value::Object(map) = &mut mustache_dict {
map.insert("critical".to_string(), json!(self.get_limit("critical", stat_name)));
map.insert("warning".to_string(), json!(self.get_limit("warning", stat_name)));
map.insert("careful".to_string(), json!(self.get_limit("careful", stat_name)));
map.insert(
"critical".to_string(),
json!(self.get_limit("critical", stat_name)),
);
map.insert(
"warning".to_string(),
json!(self.get_limit("warning", stat_name)),
);
map.insert(
"careful".to_string(),
json!(self.get_limit("careful", stat_name)),
);
map.insert(
"time".to_string(),
json!(chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.6f").to_string()),
json!(
chrono::Local::now()
.format("%Y-%m-%dT%H:%M:%S%.6f")
.to_string()
),
);
}
self.actions.run(stat_name, trigger, &commands, repeat, Some(&mustache_dict));
self.actions
.run(stat_name, trigger, &commands, repeat, Some(&mustache_dict));
}
}
}
@@ -994,7 +1125,14 @@ impl PluginModel {
}
/// Add a `key: value` pair to the TUI (port of `curse_add_stat`).
pub fn curse_add_stat(&self, key: &str, width: Option<usize>, header: &str, separator: &str, trailer: &str) -> Vec<StatLine> {
pub fn curse_add_stat(
&self,
key: &str,
width: Option<usize>,
header: &str,
separator: &str,
trailer: &str,
) -> Vec<StatLine> {
let Some(value) = self.stats.get(key) else {
return Vec::new();
};
@@ -1006,7 +1144,9 @@ impl PluginModel {
let unit_short = definition.map(|f| f.short_unit()).unwrap_or("");
let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float");
let value = if definition.map(|f| f.rate).unwrap_or(false) {
self.stats.get(&format!("{key}_rate_per_sec")).unwrap_or(&Value::Null)
self.stats
.get(format!("{key}_rate_per_sec"))
.unwrap_or(&Value::Null)
} else {
value
};
@@ -1020,7 +1160,9 @@ impl PluginModel {
Some(v) => {
if unit_type == "float" {
format!("{v:.1}{unit_short}")
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
} else if let Some(min_symbol) =
definition.and_then(|f| f.min_symbol.clone())
{
format!("{}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else {
format!("{}{unit_short}", v as i64)
@@ -1030,13 +1172,18 @@ impl PluginModel {
(item, value_text)
}
Some(width) => {
let item = format!("{header}{key_name:<width$}{separator}", width = width.saturating_sub(7));
let item = format!(
"{header}{key_name:<width$}{separator}",
width = width.saturating_sub(7)
);
let value_text = match value {
None => format!("{:>5}", "-"),
Some(v) => {
if unit_type == "float" {
format!("{v:>5.1}{unit_short}")
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
} else if let Some(min_symbol) =
definition.and_then(|f| f.min_symbol.clone())
{
format!("{:>5}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else {
format!("{:>5}{unit_short}", v as i64)
@@ -1048,7 +1195,11 @@ impl PluginModel {
};
let item = format!("{msg_item}{trailer}");
let decoration = if value.is_some() { self.view_decoration(None, key) } else { Decoration::Default };
let decoration = if value.is_some() {
self.view_decoration(None, key)
} else {
Decoration::Default
};
let optional = self.view_bool(None, key, "optional");
vec![
@@ -1088,12 +1239,14 @@ impl PluginModel {
}
let history_max_size = self.history_size();
let items = self.items_history_list.clone();
let key = self.item_key.clone();
let key = self.item_key;
for item in items {
match &export {
Value::Array(list) => {
for entry in list {
let Some(item_key) = key.as_deref().and_then(|k| entry.get(k)).map(crate::units::py_str) else {
let Some(item_key) =
key.and_then(|k| entry.get(k)).map(crate::units::py_str)
else {
continue;
};
if let Some(value) = entry.get(&item.name).and_then(as_f64) {
@@ -1108,7 +1261,12 @@ impl PluginModel {
}
Value::Object(_) => {
if let Some(value) = export.get(&item.name).and_then(as_f64) {
self.history.add(&item.name, value, &item.description, Some(history_max_size));
self.history.add(
&item.name,
value,
&item.description,
Some(history_max_size),
);
}
}
_ => {}
+218 -75
View File
@@ -15,8 +15,15 @@ use sysinfo::{ProcessesToUpdate, System, Users};
pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"];
/// Available process sort keys.
pub const SORT_PROCESSES_STATS: [&str; 7] =
["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
pub const SORT_PROCESSES_STATS: [&str; 7] = [
"cpu_percent",
"memory_percent",
"username",
"cpu_times",
"io_counters",
"name",
"cpu_num",
];
/// Sort keys as displayed to humans.
pub fn sort_for_human(key: &str) -> &str {
@@ -155,7 +162,9 @@ impl Processes {
}
pub fn pid_max(&self) -> Option<i64> {
std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::<i64>().ok())
std::fs::read_to_string("/proc/sys/kernel/pid_max")
.ok()
.and_then(|v| v.trim().parse::<i64>().ok())
}
pub fn processes_count(&self, max_processes: Option<i64>) -> i64 {
@@ -199,20 +208,28 @@ impl Processes {
let stat = read_proc_stat(pid);
let name = process.name().to_string_lossy().to_string();
let cmdline: Vec<String> =
process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect();
let cmdline: Vec<String> = process
.cmd()
.iter()
.map(|c| c.to_string_lossy().to_string())
.collect();
let username = process
.user_id()
.and_then(|uid| self.users.get_user_by_id(uid))
.map(|u| u.name().to_string())
.unwrap_or_default();
let status = stat.as_ref().map(|s| s.state.clone()).unwrap_or_else(|| "?".to_string());
let status = stat
.as_ref()
.map(|s| s.state.clone())
.unwrap_or_else(|| "?".to_string());
let nice = stat.as_ref().map(|s| s.nice).unwrap_or(0);
let num_threads = stat.as_ref().map(|s| s.num_threads).unwrap_or(0);
let cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0);
let (user_time, system_time) =
stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0));
let (user_time, system_time) = stat
.as_ref()
.map(|s| (s.utime, s.stime))
.unwrap_or((0.0, 0.0));
let rss = process.memory() as f64;
let vms = process.virtual_memory() as f64;
@@ -269,7 +286,12 @@ impl Processes {
}
// Sort processes by the current sort key.
sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse);
sort_stats(
&mut processlist,
&self.sort_key,
"memory_percent",
self.sort_reverse,
);
self.update_processcount(&processlist);
@@ -284,23 +306,25 @@ impl Processes {
{
self.extended_process_pid = Some(pid);
}
if let Some(target) = self.extended_process_pid {
if target == pid {
let extended = self.set_extended_stats(pid, proc);
if let Value::Object(map) = extended {
if let Some(obj) = proc.as_object_mut() {
for (k, v) in map {
obj.insert(k, v);
}
}
if let Some(target) = self.extended_process_pid
&& target == pid
{
let extended = self.set_extended_stats(pid, proc);
if let Value::Object(map) = extended
&& let Some(obj) = proc.as_object_mut()
{
for (k, v) in map {
obj.insert(k, v);
}
}
}
}
// Remove non running processes from the IO cache.
let running: std::collections::HashSet<i64> =
processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect();
let running: std::collections::HashSet<i64> = processlist
.iter()
.filter_map(|p| p.get("pid").and_then(Value::as_i64))
.collect();
self.io_old.retain(|pid, _| running.contains(pid));
// Export list.
@@ -343,8 +367,12 @@ impl Processes {
let mean_counter_key = format!("{stat_prefix}_mean_counter");
let prev_min = prev.and_then(|p| p.get(&min_key)).and_then(Value::as_f64);
let prev_max = prev.and_then(|p| p.get(&max_key)).and_then(Value::as_f64);
let prev_sum = prev.and_then(|p| p.get(&mean_sum_key)).and_then(Value::as_f64);
let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64);
let prev_sum = prev
.and_then(|p| p.get(&mean_sum_key))
.and_then(Value::as_f64);
let prev_count = prev
.and_then(|p| p.get(&mean_counter_key))
.and_then(Value::as_i64);
let min = prev_min.map(|m| m.min(current)).unwrap_or(current);
let max = prev_max.map(|m| m.max(current)).unwrap_or(current);
let sum = prev_sum.map(|s| s + current).unwrap_or(current);
@@ -356,8 +384,14 @@ impl Processes {
ret.insert(format!("{stat_prefix}_mean"), json!(sum / count as f64));
};
let cpu_percent = proc.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
let cpu_percent = proc
.get("cpu_percent")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let memory_percent = proc
.get("memory_percent")
.and_then(Value::as_f64)
.unwrap_or(0.0);
prefix(&mut ret, "cpu", cpu_percent, previous.as_ref());
prefix(&mut ret, "memory", memory_percent, previous.as_ref());
ret.insert("extended_stats".to_string(), json!(true));
@@ -387,19 +421,31 @@ impl Processes {
fn update_list(&self, processlist: &[Value]) -> Vec<Value> {
if !self.filter_focus.is_empty() {
return processlist.iter().filter(|p| self.filter_focus.is_filtered(p)).cloned().collect();
return processlist
.iter()
.filter(|p| self.filter_focus.is_filtered(p))
.cloned()
.collect();
}
if self.filter.filter().is_none() {
return processlist.to_vec();
}
processlist.iter().filter(|p| self.filter.is_filtered(p)).cloned().collect()
processlist
.iter()
.filter(|p| self.filter.is_filtered(p))
.cloned()
.collect()
}
fn update_export_list(&self, processlist: &[Value]) -> Vec<Value> {
if self.filter_export.is_empty() {
return Vec::new();
}
processlist.iter().filter(|p| self.filter_export.is_filtered(p)).cloned().collect()
processlist
.iter()
.filter(|p| self.filter_export.is_filtered(p))
.cloned()
.collect()
}
/// Get the process list (sorted or not).
@@ -422,16 +468,23 @@ impl Processes {
}
pub fn get_stats(&self, pid: i64) -> Option<&Value> {
self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid))
self.processlist
.iter()
.find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid))
}
/// Decrease the nice level of a process (higher priority).
pub fn nice_decrease(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32;
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current - 1) } == 0 {
tracing::info!("Set nice level of process {pid} to {} (higher the priority)", current - 1);
tracing::info!(
"Set nice level of process {pid} to {} (higher the priority)",
current - 1
);
} else {
tracing::warn!("Can not decrease (higher the priority) the nice level of process {pid} (access denied)");
tracing::warn!(
"Can not decrease (higher the priority) the nice level of process {pid} (access denied)"
);
}
}
@@ -439,15 +492,23 @@ impl Processes {
pub fn nice_increase(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32;
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current + 1) } == 0 {
tracing::info!("Set nice level of process {pid} to {} (lower the priority)", current + 1);
tracing::info!(
"Set nice level of process {pid} to {} (lower the priority)",
current + 1
);
} else {
tracing::warn!("Can not increase (lower the priority) the nice level of process {pid} (access denied)");
tracing::warn!(
"Can not increase (lower the priority) the nice level of process {pid} (access denied)"
);
}
}
/// Kill a process.
pub fn kill(&self, pid: i64) {
assert!(pid != std::process::id() as i64, "Glances can kill itself...");
assert!(
pid != std::process::id() as i64,
"Glances can kill itself..."
);
tracing::debug!("Send kill signal to process: {pid}");
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGKILL);
@@ -472,23 +533,48 @@ pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &st
"io_counters" => {
// [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
let counters = process.get("io_counters").and_then(Value::as_array);
let read = counters.and_then(|c| c.first()).and_then(Value::as_f64).unwrap_or(0.0);
let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0);
let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0);
let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0);
let read = counters
.and_then(|c| c.first())
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write = counters
.and_then(|c| c.get(1))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let read_old = counters
.and_then(|c| c.get(2))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write_old = counters
.and_then(|c| c.get(3))
.and_then(Value::as_f64)
.unwrap_or(0.0);
read - read_old + write - write_old
}
"cpu_times" => {
let times = process.get("cpu_times");
let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0);
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0);
let user = times
.and_then(|t| t.get("user"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let system = times
.and_then(|t| t.get("system"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
user + system
}
_ => process.get(key).and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY),
_ => process
.get(key)
.and_then(Value::as_f64)
.unwrap_or(f64::NEG_INFINITY),
}
};
let string_key = |process: &Value, key: &str| -> String {
process.get(key).and_then(Value::as_str).unwrap_or("~").to_string()
process
.get(key)
.and_then(Value::as_str)
.unwrap_or("~")
.to_string()
};
stats.sort_by(|a, b| {
@@ -523,22 +609,53 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
let no_io = [0u64; 5];
let mut programs: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
for process in processlist {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0);
let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({}));
let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({}));
let name = process
.get("name")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string();
let num_threads = process
.get("num_threads")
.and_then(Value::as_f64)
.unwrap_or(0.0)
.max(0.0);
let cpu_percent = process
.get("cpu_percent")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let memory_percent = process
.get("memory_percent")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let cpu_times = process
.get("cpu_times")
.cloned()
.unwrap_or_else(|| json!({}));
let memory_info = process
.get("memory_info")
.cloned()
.unwrap_or_else(|| json!({}));
let io_counters: Vec<u64> = process
.get("io_counters")
.and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec());
let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0);
let username = process.get("username").and_then(Value::as_str).unwrap_or("_").to_string();
let username = process
.get("username")
.and_then(Value::as_str)
.unwrap_or("_")
.to_string();
let nice = process.get("nice").and_then(Value::as_i64).unwrap_or(0);
let status = process.get("status").and_then(Value::as_str).unwrap_or("?").to_string();
let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null);
let status = process
.get("status")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string();
let time_since_update = process
.get("time_since_update")
.cloned()
.unwrap_or(Value::Null);
match programs.get_mut(&name) {
None => {
@@ -575,20 +692,26 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
add(program, "memory_percent", memory_percent);
for key in ["user", "system", "iowait"] {
let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
if value != 0.0 || cpu_times.get(key).is_some() {
if let Some(times) = program.get_mut("cpu_times").and_then(Value::as_object_mut) {
let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
times.insert(key.to_string(), json!(current + value));
}
if (value != 0.0 || cpu_times.get(key).is_some())
&& let Some(times) =
program.get_mut("cpu_times").and_then(Value::as_object_mut)
{
let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
times.insert(key.to_string(), json!(current + value));
}
}
if let Some(program_memory) = program.get_mut("memory_info").and_then(Value::as_object_mut) {
if let Some(addition) = memory_info.as_object() {
for (key, value) in addition {
let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0);
let value = value.as_f64().unwrap_or(0.0);
program_memory.insert(key.clone(), json!(current + value));
}
if let Some(program_memory) = program
.get_mut("memory_info")
.and_then(Value::as_object_mut)
&& let Some(addition) = memory_info.as_object()
{
for (key, value) in addition {
let current = program_memory
.get(key)
.and_then(Value::as_f64)
.unwrap_or(0.0);
let value = value.as_f64().unwrap_or(0.0);
program_memory.insert(key.clone(), json!(current + value));
}
}
// io_counters: sum slots 0..4, OR the tag.
@@ -598,10 +721,15 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec());
for index in 0..4 {
summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0);
for (index, value) in summed.iter_mut().take(4).enumerate() {
*value = existing.get(index).copied().unwrap_or(0)
+ io_counters.get(index).copied().unwrap_or(0);
}
summed[4] = existing.get(4).copied().unwrap_or(0).max(io_counters.get(4).copied().unwrap_or(0));
summed[4] = existing
.get(4)
.copied()
.unwrap_or(0)
.max(io_counters.get(4).copied().unwrap_or(0));
program.insert("io_counters".to_string(), json!(summed));
if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) {
children.push(json!(pid));
@@ -625,7 +753,11 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.into_values()
.map(|mut program| {
if let Some(program) = program.as_object_mut() {
let nprocs = program.get("childrens").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0);
let nprocs = program
.get("childrens")
.and_then(Value::as_array)
.map(|a| a.len())
.unwrap_or(0);
program.insert("nprocs".to_string(), json!(nprocs));
}
program
@@ -657,7 +789,12 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
let fields: Vec<&str> = content[close + 1..].split_whitespace().collect();
// fields[0] = state (stat field 3), so stat field N (1-based) is fields[N-3].
let field = |n: usize| fields.get(n).copied().unwrap_or("");
let state = field(0).chars().next().unwrap_or('?').to_ascii_uppercase().to_string();
let state = field(0)
.chars()
.next()
.unwrap_or('?')
.to_ascii_uppercase()
.to_string();
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
let ticks = if ticks <= 0.0 { 100.0 } else { ticks };
let mut stat = ProcStat {
@@ -680,13 +817,16 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
}
}
}
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) {
if let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) {
let gids: Vec<i64> = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect();
stat.gid_real = gids.first().copied().unwrap_or(0);
stat.gid_effective = gids.get(1).copied().unwrap_or(0);
stat.gid_saved = gids.get(2).copied().unwrap_or(0);
}
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status"))
&& let Some(line) = status.lines().find(|l| l.starts_with("Gid:"))
{
let gids: Vec<i64> = line[4..]
.split_whitespace()
.filter_map(|v| v.parse().ok())
.collect();
stat.gid_real = gids.first().copied().unwrap_or(0);
stat.gid_effective = gids.get(1).copied().unwrap_or(0);
stat.gid_saved = gids.get(2).copied().unwrap_or(0);
}
Some(stat)
}
@@ -712,7 +852,10 @@ mod tests {
#[test]
fn test_sort_stats() {
let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})];
let mut stats = vec![
json!({"name": "a", "cpu_percent": 1.0}),
json!({"name": "b", "cpu_percent": 3.0}),
];
sort_stats(&mut stats, "cpu_percent", "memory_percent", true);
assert_eq!(stats[0]["name"], "b");
}
+18 -9
View File
@@ -24,16 +24,21 @@ pub fn sanitize_value(value: &serde_json::Value) -> serde_json::Value {
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(sanitize_value).collect())
}
serde_json::Value::Object(map) => {
serde_json::Value::Object(map.iter().map(|(k, v)| (k.clone(), sanitize_value(v))).collect())
}
serde_json::Value::Object(map) => serde_json::Value::Object(
map.iter()
.map(|(k, v)| (k.clone(), sanitize_value(v)))
.collect(),
),
other => other.clone(),
}
}
/// Split a command string into an argument list. Spaces are separators except
/// within single or double quotes (the surrounding quotes are removed).
pub fn split_args(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> Result<Vec<String>, String> {
pub fn split_args(
cmd: &str,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> Result<Vec<String>, String> {
let mut args: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_single = false;
@@ -182,16 +187,20 @@ fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, St
return stderr;
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(target) = stdout_redirect {
if let Ok(mut file) = std::fs::File::create(&target) {
let _ = file.write_all(stdout.as_bytes());
}
if let Some(target) = stdout_redirect
&& let Ok(mut file) = std::fs::File::create(&target)
{
let _ = file.write_all(stdout.as_bytes());
}
stdout
}
/// Run a command line, interpreting (or not) the shell operators.
pub fn secure_popen(cmd: &str, allow_operators: bool, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
pub fn secure_popen(
cmd: &str,
allow_operators: bool,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> String {
if !allow_operators {
return run_argv(cmd, render);
}
+1
View File
@@ -13,6 +13,7 @@ pub enum Threshold {
}
impl Threshold {
#[allow(clippy::should_implement_trait)]
pub fn from_str(name: &str) -> Option<Self> {
match name {
"OK" => Some(Threshold::Ok),
+12 -3
View File
@@ -14,7 +14,10 @@ pub struct Timer {
impl Timer {
pub fn new(duration: f64) -> Self {
let mut timer = Self { duration, target: Instant::now() };
let mut timer = Self {
duration,
target: Instant::now(),
};
timer.start();
timer
}
@@ -32,7 +35,11 @@ impl Timer {
/// Elapsed seconds since the timer was started.
pub fn get(&self) -> f64 {
self.duration - self.target.saturating_duration_since(Instant::now()).as_secs_f64()
self.duration
- self
.target
.saturating_duration_since(Instant::now())
.as_secs_f64()
}
pub fn set(&mut self, duration: f64) {
@@ -62,7 +69,9 @@ impl Default for Counter {
impl Counter {
pub fn new() -> Self {
Self { start: Instant::now() }
Self {
start: Instant::now(),
}
}
pub fn reset(&mut self) {
+33 -11
View File
@@ -69,7 +69,22 @@ pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol
/// Render a progress bar the way the Python `Bar` class does.
pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: bool) -> String {
let size = if display_value { size.saturating_sub(6) } else { size };
bar_custom(percent, size, bar_char, " ", display_value)
}
/// Render a progress bar with custom characters.
pub fn bar_custom(
percent: f64,
size: usize,
bar_char: &str,
empty_char: &str,
display_value: bool,
) -> String {
let size = if display_value {
size.saturating_sub(6)
} else {
size
};
let value = percent.clamp(0.0, 100.0);
let scaled = size as f64 * value / 100.0;
let whole = scaled.floor() as usize;
@@ -81,14 +96,18 @@ pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: boo
if (frac * 8.0) as usize >= 5 {
bar.push_str(bar_char);
} else {
bar.push(' ');
bar.push_str(empty_char);
}
whole += 1;
}
bar.push_str(&" ".repeat(size.saturating_sub(whole)));
bar.push_str(&empty_char.repeat(size.saturating_sub(whole)));
if display_value {
if percent >= 100.0 {
bar = format!("{bar} {}{:3.0}%", if percent > 100.0 { ">" } else { " " }, 100.0);
bar = format!(
"{bar} {}{:3.0}%",
if percent > 100.0 { ">" } else { " " },
100.0
);
} else {
bar = format!("{bar}{percent:5.1}%");
}
@@ -104,11 +123,9 @@ pub fn string_value_to_float(s: &str) -> Option<f64> {
let mut digits = String::new();
let mut rest = String::new();
for c in cleaned.chars() {
if c.is_ascii_digit() || c == '.' {
if rest.is_empty() {
digits.push(c);
continue;
}
if (c.is_ascii_digit() || c == '.') && rest.is_empty() {
digits.push(c);
continue;
}
rest.push(c);
}
@@ -195,7 +212,9 @@ pub fn py_repr(value: &Value) -> String {
pub fn as_f64(value: &Value) -> Option<f64> {
match value {
Value::Number(n) => n.as_f64(),
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => {
None
}
}
}
@@ -242,7 +261,10 @@ pub fn split_esc(input: &str, sep: char, esc: char) -> Vec<String> {
/// shell by adding a `$` prefix in front of them. (Only the replacement
/// flavour is needed by the plugins.)
pub fn replace_special_chars(input: &str, by: char) -> String {
input.replace("\r\n", &by.to_string()).replace('\n', &by.to_string()).replace('\t', &by.to_string())
input
.replace("\r\n", &by.to_string())
.replace('\n', &by.to_string())
.replace('\t', &by.to_string())
}
#[cfg(test)]
+86 -32
View File
@@ -94,7 +94,13 @@ fn clock_ticks() -> f64 {
fn parse_cpu_line(fields: &[&str]) -> CpuTimes {
let ticks = clock_ticks();
let value = |index: usize| -> f64 { fields.get(index).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) / ticks };
let value = |index: usize| -> f64 {
fields
.get(index)
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
/ ticks
};
CpuTimes {
user: value(0),
nice: value(1),
@@ -150,7 +156,11 @@ pub fn read_meminfo() -> HashMap<String, u64> {
continue;
};
let unit = parts.next().unwrap_or("");
let bytes = if unit.starts_with("kB") { value * 1024 } else { value };
let bytes = if unit.starts_with("kB") {
value * 1024
} else {
value
};
values.insert(key.trim().to_string(), bytes);
}
values
@@ -163,10 +173,10 @@ pub fn read_vmstat() -> HashMap<String, u64> {
return values;
};
for line in content.lines() {
if let Some((key, value)) = line.split_once(' ') {
if let Ok(value) = value.trim().parse::<u64>() {
values.insert(key.to_string(), value);
}
if let Some((key, value)) = line.split_once(' ')
&& let Ok(value) = value.trim().parse::<u64>()
{
values.insert(key.to_string(), value);
}
}
values
@@ -212,7 +222,9 @@ pub fn uptime_seconds() -> Option<f64> {
/// Number of logical CPUs.
pub fn logical_core_count() -> usize {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// Number of physical cores, parsed from `/proc/cpuinfo`.
@@ -262,20 +274,21 @@ pub fn cpu_name() -> String {
let mut implementer: Option<String> = None;
let mut ret = fallback;
for line in content.lines() {
if line.starts_with("model name") || line.starts_with("Model") || line.starts_with("cpu model") {
if let Some((_, value)) = line.split_once(':') {
return value.trim().to_string();
}
if (line.starts_with("model name")
|| line.starts_with("Model")
|| line.starts_with("cpu model"))
&& let Some((_, value)) = line.split_once(':')
{
return value.trim().to_string();
}
if line.starts_with("CPU implementer") {
implementer = line.split_once(':').map(|(_, v)| v.trim().to_string());
}
if line.starts_with("CPU part") {
if let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string()) {
if let Some(implementer) = &implementer {
ret = format!("{implementer} part {part}");
}
}
if line.starts_with("CPU part")
&& let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string())
&& let Some(implementer) = &implementer
{
ret = format!("{implementer} part {part}");
}
}
ret
@@ -316,7 +329,10 @@ pub fn read_net_dev() -> IndexMap<String, NetDev> {
let Some((interface, values)) = line.split_once(':') else {
continue;
};
let fields: Vec<u64> = values.split_whitespace().filter_map(|v| v.parse().ok()).collect();
let fields: Vec<u64> = values
.split_whitespace()
.filter_map(|v| v.parse().ok())
.collect();
if fields.len() < 16 {
continue;
}
@@ -429,7 +445,8 @@ pub fn read_diskstats() -> IndexMap<String, DiskStat> {
continue;
}
let name = fields[2].to_string();
let value = |index: usize| -> u64 { fields.get(index).and_then(|v| v.parse().ok()).unwrap_or(0) };
let value =
|index: usize| -> u64 { fields.get(index).and_then(|v| v.parse().ok()).unwrap_or(0) };
result.insert(
name,
DiskStat {
@@ -520,12 +537,25 @@ pub fn disk_usage(path: &str) -> Option<FsUsage> {
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 {
return None;
}
let block_size = if stat.f_frsize > 0 { stat.f_frsize } else { stat.f_bsize } as u64;
let block_size = if stat.f_frsize > 0 {
stat.f_frsize
} else {
stat.f_bsize
} as u64;
let total = stat.f_blocks as u64 * block_size;
let free = stat.f_bavail as u64 * block_size;
let used = (stat.f_blocks as u64).saturating_sub(stat.f_bfree as u64) * block_size;
let percent = if used + free > 0 { (used as f64 * 100.0) / (used + free) as f64 } else { 0.0 };
Some(FsUsage { total, used, free, percent })
let percent = if used + free > 0 {
(used as f64 * 100.0) / (used + free) as f64
} else {
0.0
};
Some(FsUsage {
total,
used,
free,
percent,
})
}
/// Linux distribution name/version (`/etc/os-release`).
@@ -546,7 +576,8 @@ pub fn read_os_release() -> (String, String) {
pub fn hostname() -> String {
let mut buffer = [0u8; 256];
let result = unsafe { libc::gethostname(buffer.as_mut_ptr() as *mut libc::c_char, buffer.len()) };
let result =
unsafe { libc::gethostname(buffer.as_mut_ptr() as *mut libc::c_char, buffer.len()) };
if result == 0 {
let end = buffer.iter().position(|c| *c == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..end]).to_string()
@@ -571,10 +602,17 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
addr.sin_family = libc::AF_INET as u16;
addr.sin_port = 53u16.to_be();
addr.sin_addr.s_addr = u32::from_be_bytes([192, 0, 2, 1]).to_be();
if libc::connect(sock, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in>() as u32) == 0 {
if libc::connect(
sock,
&addr as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in>() as u32,
) == 0
{
let mut local: libc::sockaddr_in = std::mem::zeroed();
let mut len = std::mem::size_of::<libc::sockaddr_in>() as u32;
if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len) == 0 {
if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len)
== 0
{
let bytes = local.sin_addr.s_addr.to_ne_bytes();
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
if !candidate.starts_with("127.") && candidate != "0.0.0.0" {
@@ -599,11 +637,15 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET {
let sin = addr as *const libc::sockaddr_in;
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes();
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
let candidate =
format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
if &candidate == ip && !mask.is_null() {
let sin_mask = mask as *const libc::sockaddr_in;
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes();
netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3]));
netmask = Some(format!(
"{}.{}.{}.{}",
mbytes[0], mbytes[1], mbytes[2], mbytes[3]
));
break;
}
}
@@ -624,18 +666,24 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
while !current.is_null() {
let addr = (*current).ifa_addr;
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET {
let name = std::ffi::CStr::from_ptr((*current).ifa_name).to_string_lossy().to_string();
let name = std::ffi::CStr::from_ptr((*current).ifa_name)
.to_string_lossy()
.to_string();
let flags = (*current).ifa_flags as u32;
if name != "lo" && flags & libc::IFF_UP as u32 != 0 {
let sin = addr as *const libc::sockaddr_in;
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes();
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
let candidate =
format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
let mask = (*current).ifa_netmask;
let mut netmask = None;
if !mask.is_null() {
let sin_mask = mask as *const libc::sockaddr_in;
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes();
netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3]));
netmask = Some(format!(
"{}.{}.{}.{}",
mbytes[0], mbytes[1], mbytes[2], mbytes[3]
));
}
found = (Some(candidate), netmask);
break;
@@ -661,7 +709,11 @@ pub fn get_default_gateway() -> Option<String> {
let gateway = fields[2];
if gateway.len() == 8 {
let bytes: Vec<String> = (0..4)
.map(|i| u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16).unwrap_or(0).to_string())
.map(|i| {
u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16)
.unwrap_or(0)
.to_string()
})
.collect();
return Some(bytes.join("."));
}
@@ -684,7 +736,9 @@ pub fn ip_to_cidr(ip: Option<&str>) -> u32 {
/// Read a sysfs hwmon input value.
pub fn read_number_file(path: &str) -> Option<f64> {
std::fs::read_to_string(path).ok().and_then(|v| v.trim().parse::<f64>().ok())
std::fs::read_to_string(path)
.ok()
.and_then(|v| v.trim().parse::<f64>().ok())
}
/// Get the current date formatted with a C strftime format (same semantics
+8 -6
View File
@@ -20,6 +20,7 @@ pub struct CpuPercentState {
prev_cpu: Option<CpuTimes>,
prev_percpu: Vec<CpuTimes>,
cpu_percent: f64,
cpu_timer: std::time::Instant,
percpu_percent: Vec<Value>,
}
@@ -30,6 +31,7 @@ impl CpuPercentState {
prev_cpu: None,
prev_percpu: Vec::new(),
cpu_percent: 0.0,
cpu_timer: std::time::Instant::now(),
percpu_percent: Vec::new(),
};
state.cpu_percent = state.compute_cpu();
@@ -39,12 +41,12 @@ impl CpuPercentState {
/// Update and/or return the CPU usage (psutil `cpu_percent`).
pub fn get_cpu(&mut self) -> f64 {
// The Python implementation caches this value for `cached_timer_cpu`
// seconds. Since the engine refreshes at least every `args.time`
// seconds, the value is recomputed on every call there; keep the cache
// for direct calls.
let _ = self.cached_timer_cpu;
self.cpu_percent = self.compute_cpu();
// The value is cached for `cached_timer_cpu` seconds like in the
// Python implementation.
if self.cpu_timer.elapsed().as_secs_f64() >= self.cached_timer_cpu {
self.cpu_percent = self.compute_cpu();
self.cpu_timer = std::time::Instant::now();
}
self.cpu_percent
}
+124 -36
View File
@@ -27,37 +27,102 @@ impl Engine {
// Registration order mirrors the alphabetical plugin loading of the
// Python implementation (in particular processcount runs before
// processlist and programlist).
plugins.insert("amps".to_string(), Box::new(AmpsPlugin::new(&config, &args)));
plugins.insert("core".to_string(), Box::new(CorePlugin::new(&config, &args)));
plugins.insert(
"amps".to_string(),
Box::new(AmpsPlugin::new(&config, &args)),
);
plugins.insert(
"core".to_string(),
Box::new(CorePlugin::new(&config, &args)),
);
plugins.insert("cpu".to_string(), Box::new(CpuPlugin::new(&config, &args)));
plugins.insert("diskio".to_string(), Box::new(DiskioPlugin::new(&config, &args)));
plugins.insert("folders".to_string(), Box::new(FoldersPlugin::new(&config, &args)));
plugins.insert(
"diskio".to_string(),
Box::new(DiskioPlugin::new(&config, &args)),
);
plugins.insert(
"folders".to_string(),
Box::new(FoldersPlugin::new(&config, &args)),
);
plugins.insert("fs".to_string(), Box::new(FsPlugin::new(&config, &args)));
plugins.insert("ip".to_string(), Box::new(IpPlugin::new(&config, &args)));
plugins.insert("irq".to_string(), Box::new(IrqPlugin::new(&config, &args)));
plugins.insert("load".to_string(), Box::new(LoadPlugin::new(&config, &args)));
plugins.insert(
"load".to_string(),
Box::new(LoadPlugin::new(&config, &args)),
);
plugins.insert("mem".to_string(), Box::new(MemPlugin::new(&config, &args)));
plugins.insert("memswap".to_string(), Box::new(MemSwapPlugin::new(&config, &args)));
plugins.insert("network".to_string(), Box::new(NetworkPlugin::new(&config, &args)));
plugins.insert(
"memswap".to_string(),
Box::new(MemSwapPlugin::new(&config, &args)),
);
plugins.insert(
"network".to_string(),
Box::new(NetworkPlugin::new(&config, &args)),
);
plugins.insert("now".to_string(), Box::new(NowPlugin::new(&config, &args)));
plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::new(&config, &args)));
plugins.insert("ports".to_string(), Box::new(PortsPlugin::new(&config, &args)));
plugins.insert("processcount".to_string(), Box::new(ProcessCountPlugin::new(&config, &args)));
plugins.insert("processlist".to_string(), Box::new(ProcessListPlugin::new(&config, &args)));
plugins.insert("programlist".to_string(), Box::new(ProgramListPlugin::new(&config, &args)));
plugins.insert("psutilversion".to_string(), Box::new(PsutilVersionPlugin::new(&config, &args)));
plugins.insert("quicklook".to_string(), Box::new(QuicklookPlugin::new(&config, &args)));
plugins.insert("raid".to_string(), Box::new(RaidPlugin::new(&config, &args)));
plugins.insert("sensors".to_string(), Box::new(SensorsPlugin::new(&config, &args)));
plugins.insert("system".to_string(), Box::new(SystemPlugin::new(&config, &args)));
plugins.insert("uptime".to_string(), Box::new(UptimePlugin::new(&config, &args)));
plugins.insert("version".to_string(), Box::new(VersionPlugin::new(&config, &args)));
plugins.insert("wifi".to_string(), Box::new(WifiPlugin::new(&config, &args)));
plugins.insert(
"percpu".to_string(),
Box::new(PerCpuPlugin::new(&config, &args)),
);
plugins.insert(
"ports".to_string(),
Box::new(PortsPlugin::new(&config, &args)),
);
plugins.insert(
"processcount".to_string(),
Box::new(ProcessCountPlugin::new(&config, &args)),
);
plugins.insert(
"processlist".to_string(),
Box::new(ProcessListPlugin::new(&config, &args)),
);
plugins.insert(
"programlist".to_string(),
Box::new(ProgramListPlugin::new(&config, &args)),
);
plugins.insert(
"psutilversion".to_string(),
Box::new(PsutilVersionPlugin::new(&config, &args)),
);
plugins.insert(
"quicklook".to_string(),
Box::new(QuicklookPlugin::new(&config, &args)),
);
plugins.insert(
"raid".to_string(),
Box::new(RaidPlugin::new(&config, &args)),
);
plugins.insert(
"sensors".to_string(),
Box::new(SensorsPlugin::new(&config, &args)),
);
plugins.insert(
"system".to_string(),
Box::new(SystemPlugin::new(&config, &args)),
);
plugins.insert(
"uptime".to_string(),
Box::new(UptimePlugin::new(&config, &args)),
);
plugins.insert(
"version".to_string(),
Box::new(VersionPlugin::new(&config, &args)),
);
plugins.insert(
"wifi".to_string(),
Box::new(WifiPlugin::new(&config, &args)),
);
// The refresh time is set on the model, which was built with the
// command line value already.
let _ = refresh_time;
let mut engine = Self { args, config, runtime, plugins };
let mut engine = Self {
args,
config,
runtime,
plugins,
};
engine.apply_plugin_options();
engine.apply_process_options();
engine.sync_enabled();
@@ -77,11 +142,11 @@ impl Engine {
fn apply_plugin_options(&mut self) {
// Disable plugins from the configuration file.
for section in self.config.sections() {
if self.config.get_bool_value(section, "disable", false) {
if let Some(plugin) = self.plugins.get_mut(section) {
debug!("{} disabled by the configuration file", section);
plugin.set_enabled(false);
}
if self.config.get_bool_value(section, "disable", false)
&& let Some(plugin) = self.plugins.get_mut(section)
{
debug!("{} disabled by the configuration file", section);
plugin.set_enabled(false);
}
}
@@ -102,10 +167,18 @@ impl Engine {
// --disable-plugin / --enable-plugin.
let mut explicit_enable: Vec<String> = Vec::new();
if let Some(enable) = &self.args.enable_plugin {
explicit_enable = enable.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
explicit_enable = enable
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
if let Some(disable) = &self.args.disable_plugin {
let tokens: Vec<String> = disable.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
let tokens: Vec<String> = disable
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if tokens.iter().any(|t| t == "all") {
for name in self.plugins.keys().cloned().collect::<Vec<_>>() {
self.plugins.get_mut(&name).unwrap().set_enabled(false);
@@ -134,12 +207,18 @@ impl Engine {
}
// Processlist needs processcount to update the shared process list.
let processcount_enabled = self.plugins.get("processcount").map(|p| p.is_enabled()).unwrap_or(false);
let processcount_enabled = self
.plugins
.get("processcount")
.map(|p| p.is_enabled())
.unwrap_or(false);
if !processcount_enabled {
for name in ["processlist", "programlist"] {
if let Some(plugin) = self.plugins.get_mut(name) {
if plugin.is_enabled() {
tracing::warn!("Processcount is disabled, so processlist (updated by processcount) is also disabled");
tracing::warn!(
"Processcount is disabled, so processlist (updated by processcount) is also disabled"
);
}
plugin.set_enabled(false);
}
@@ -210,7 +289,10 @@ impl Engine {
/// Update every enabled plugin.
pub fn update(&mut self) {
let names: Vec<String> = self.plugins.keys().cloned().collect();
let enabled: Vec<String> = names.into_iter().filter(|n| self.plugins[n].is_enabled()).collect();
let enabled: Vec<String> = names
.into_iter()
.filter(|n| self.plugins[n].is_enabled())
.collect();
self.update_plugins(&enabled);
}
@@ -222,7 +304,13 @@ impl Engine {
}
fn update_plugin(&mut self, name: &str) {
let Engine { plugins, args, config, runtime, .. } = self;
let Engine {
plugins,
args,
config,
runtime,
..
} = self;
let Some(plugin) = plugins.get_mut(name) else {
return;
};
@@ -289,10 +377,10 @@ impl Engine {
};
let mut result = serde_json::Map::new();
for name in names {
if let Some(plugin) = self.plugins.get(&name) {
if plugin.is_enabled() {
result.insert(name, plugin.stats().clone());
}
if let Some(plugin) = self.plugins.get(&name)
&& plugin.is_enabled()
{
result.insert(name, plugin.stats().clone());
}
}
result
+2
View File
@@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
//! glances-plugins: collectors, plugin registry and update engine.
pub mod collectors;
+50 -14
View File
@@ -62,8 +62,12 @@ impl AmpsPlugin {
.get_value(section, "regex", None)
.and_then(|pattern| regex::Regex::new(&pattern).ok());
let refresh = config.get_float_value(section, "refresh", 3.0);
let countmin = config.get_value(section, "countmin", None).and_then(|v| v.parse::<f64>().ok());
let countmax = config.get_value(section, "countmax", None).and_then(|v| v.parse::<f64>().ok());
let countmin = config
.get_value(section, "countmin", None)
.and_then(|v| v.parse::<f64>().ok());
let countmax = config
.get_value(section, "countmax", None)
.and_then(|v| v.parse::<f64>().ok());
let one_line = config.get_bool_value(section, "one_line", false);
amps.push(AmpConfig {
name: amp_display_name(section),
@@ -142,7 +146,12 @@ impl Plugin for AmpsPlugin {
let cmdline = process
.get("cmdline")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(" "))
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
regex.is_match(name) || regex.is_match(&cmdline)
})
@@ -191,7 +200,8 @@ impl Plugin for AmpsPlugin {
} else {
glances_core::Decoration::Ok
};
self.model.set_view_decoration(Some(&name), "count", decoration);
self.model
.set_view_decoration(Some(&name), "count", decoration);
}
}
@@ -201,7 +211,14 @@ impl Plugin for AmpsPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(12);
@@ -212,7 +229,13 @@ impl Plugin for AmpsPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>9}", "Count"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>9}", "Count"),
glances_core::Decoration::Default,
false,
false,
false,
));
let items = self.model.sorted_stats();
let Some(items) = items.as_array() else {
return ret;
@@ -221,15 +244,28 @@ impl Plugin for AmpsPlugin {
let name = i.get("name").and_then(Value::as_str).unwrap_or("");
let count = i.get("count").and_then(Value::as_i64).unwrap_or(0);
let result = i.get("result").and_then(Value::as_str).unwrap_or("");
let display_name = if result.is_empty() { name.to_string() } else { format!("{name}: {result}") };
let display_name = if result.is_empty() {
name.to_string()
} else {
format!("{name}: {result}")
};
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", display_name.chars().take(name_max_width).collect::<String>(), width = name_max_width),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(
self.model.curse_add_line(
format!(
"{:<width$}",
display_name
.chars()
.take(name_max_width)
.collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
false,
),
);
ret.push(self.model.curse_add_line(
format!("{count:>9}"),
self.model.view_decoration(Some(name), "count"),
+75 -18
View File
@@ -48,7 +48,11 @@ impl CpuPlugin {
args.disable_config_exec,
);
let nb_log_core = crate::collectors::logical_core_count();
Self { model, previous_times: None, nb_log_core }
Self {
model,
previous_times: None,
nb_log_core,
}
}
fn times_percent(&mut self) -> CpuTimes {
@@ -105,11 +109,23 @@ impl Plugin for CpuPlugin {
let mut soft_interrupts = 0u64;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("ctxt ") {
ctx_switches = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0);
ctx_switches = rest
.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("intr ") {
interrupts = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0);
interrupts = rest
.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("softirq ") {
soft_interrupts = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0);
soft_interrupts = rest
.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
}
stats.insert("ctx_switches".to_string(), json!(ctx_switches));
@@ -125,13 +141,27 @@ impl Plugin for CpuPlugin {
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
if self.model.stats.get("time_since_update").and_then(Value::as_f64) != Some(0.0) {
if let Some(ctx_switches) = self.model.stats.get("ctx_switches").and_then(Value::as_f64) {
let maximum = 100.0 * self.nb_log_core as f64;
let decoration =
self.model.get_alert(ctx_switches, 0.0, maximum, true, false, Some("ctx_switches"), None, false);
self.model.set_view_decoration(None, "ctx_switches", decoration);
}
if self
.model
.stats
.get("time_since_update")
.and_then(Value::as_f64)
!= Some(0.0)
&& let Some(ctx_switches) = self.model.stats.get("ctx_switches").and_then(Value::as_f64)
{
let maximum = 100.0 * self.nb_log_core as f64;
let decoration = self.model.get_alert(
ctx_switches,
0.0,
maximum,
true,
false,
Some("ctx_switches"),
None,
false,
);
self.model
.set_view_decoration(None, "ctx_switches", decoration);
}
}
@@ -148,7 +178,13 @@ impl Plugin for CpuPlugin {
let idle_tag = s.get("user").is_none();
// Line 1: total + idle + ctx_switches.
ret.push(self.model.curse_add_line(format!("{:8}", "CPU"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:8}", "CPU"),
glances_core::Decoration::Title,
false,
false,
false,
));
if let Some(total) = s.get("total").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{total:>4.1}%"),
@@ -160,7 +196,13 @@ impl Plugin for CpuPlugin {
}
if s.get("idle").is_some() && !idle_tag {
let optional = self.model.view_bool(None, "idle", "optional");
ret.push(self.model.curse_add_line(" idle ", glances_core::Decoration::Default, optional, false, false));
ret.push(self.model.curse_add_line(
" idle ",
glances_core::Decoration::Default,
optional,
false,
false,
));
if let Some(idle) = s.get("idle").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{idle:>4.1}%"),
@@ -171,7 +213,10 @@ impl Plugin for CpuPlugin {
));
}
}
ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("ctx_switches", Some(15), " ", "", ""),
);
// Line 2.
ret.push(self.model.curse_new_line());
@@ -181,7 +226,10 @@ impl Plugin for CpuPlugin {
ret.extend(self.model.curse_add_stat("idle", Some(15), "", "", ""));
}
ret.extend(self.model.curse_add_stat("irq", Some(14), " ", "", ""));
ret.extend(self.model.curse_add_stat("interrupts", Some(15), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("interrupts", Some(15), " ", "", ""),
);
// Line 3.
ret.push(self.model.curse_new_line());
@@ -192,9 +240,15 @@ impl Plugin for CpuPlugin {
}
ret.extend(self.model.curse_add_stat("nice", Some(14), " ", "", ""));
if s.get("soft_interrupts").is_some() {
ret.extend(self.model.curse_add_stat("soft_interrupts", Some(15), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("soft_interrupts", Some(15), " ", "", ""),
);
} else {
ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("ctx_switches", Some(15), " ", "", ""),
);
}
// Line 4.
@@ -208,7 +262,10 @@ impl Plugin for CpuPlugin {
if s.get("guest").is_some() {
ret.extend(self.model.curse_add_stat("guest", Some(14), " ", "", ""));
} else {
ret.extend(self.model.curse_add_stat("syscalls", Some(15), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("syscalls", Some(15), " ", "", ""),
);
}
ret
}
+285 -56
View File
@@ -39,9 +39,12 @@ impl DiskioPlugin {
);
model.set_item_key("disk_name");
model.hide_zero = config.get_bool_value("diskio", "hide_zero", false);
model.hide_threshold_bytes = config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64;
model.hide_zero_fields =
vec!["read_bytes_rate_per_sec".to_string(), "write_bytes_rate_per_sec".to_string()];
model.hide_threshold_bytes =
config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64;
model.hide_zero_fields = vec![
"read_bytes_rate_per_sec".to_string(),
"write_bytes_rate_per_sec".to_string(),
];
Self { model }
}
}
@@ -75,8 +78,14 @@ impl Plugin for DiskioPlugin {
let mut item = serde_json::Map::new();
item.insert("read_count".to_string(), json!(disk_stat.read_count));
item.insert("write_count".to_string(), json!(disk_stat.write_count));
item.insert("read_bytes".to_string(), json!(disk_stat.sectors_read * SECTOR_SIZE));
item.insert("write_bytes".to_string(), json!(disk_stat.sectors_written * SECTOR_SIZE));
item.insert(
"read_bytes".to_string(),
json!(disk_stat.sectors_read * SECTOR_SIZE),
);
item.insert(
"write_bytes".to_string(),
json!(disk_stat.sectors_written * SECTOR_SIZE),
);
item.insert("read_time".to_string(), json!(disk_stat.read_time));
item.insert("write_time".to_string(), json!(disk_stat.write_time));
item.insert("key".to_string(), json!("disk_name"));
@@ -95,20 +104,34 @@ impl Plugin for DiskioPlugin {
let Some(item) = item.as_object_mut() else {
continue;
};
let read_time = item.get("read_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let read_count = item.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let write_time = item.get("write_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let write_count = item.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let read_latency = if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 {
(read_time / read_count).floor() as i64
} else {
0
};
let write_latency = if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 {
(write_time / write_count).floor() as i64
} else {
0
};
let read_time = item
.get("read_time_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let read_count = item
.get("read_count_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write_time = item
.get("write_time_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write_count = item
.get("write_count_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let read_latency =
if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 {
(read_time / read_count).floor() as i64
} else {
0
};
let write_latency =
if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 {
(write_time / write_count).floor() as i64
} else {
0
};
item.insert("read_latency".to_string(), json!(read_latency));
item.insert("write_latency".to_string(), json!(write_latency));
}
@@ -124,36 +147,104 @@ impl Plugin for DiskioPlugin {
.iter()
.filter_map(|item| {
let name = item.get("disk_name")?.as_str()?.to_string();
let read_rate = item.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let write_rate = item.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let read_latency = item.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0);
let write_latency = item.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0);
let read_rate = item
.get("read_bytes_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write_rate = item
.get("write_bytes_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let read_latency = item
.get("read_latency")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let write_latency = item
.get("write_latency")
.and_then(Value::as_f64)
.unwrap_or(0.0);
Some((name, read_rate, write_rate, read_latency, write_latency))
})
.collect(),
_ => Vec::new(),
};
for (disk_name, read_rate, write_rate, read_latency, write_latency) in items {
let alert_rx = self.model.get_alert(read_rate, 0.0, 100.0, true, false, Some("rx"), Some(&disk_name), false);
let alert_tx = self.model.get_alert(write_rate, 0.0, 100.0, true, false, Some("tx"), Some(&disk_name), false);
let alert_rx = self.model.get_alert(
read_rate,
0.0,
100.0,
true,
false,
Some("rx"),
Some(&disk_name),
false,
);
let alert_tx = self.model.get_alert(
write_rate,
0.0,
100.0,
true,
false,
Some("tx"),
Some(&disk_name),
false,
);
for field in ["read_bytes", "read_bytes_rate_per_sec"] {
self.model.set_view_decoration(Some(&disk_name), field, alert_rx);
self.model
.set_view_decoration(Some(&disk_name), field, alert_rx);
}
for field in ["write_bytes", "write_bytes_rate_per_sec"] {
self.model.set_view_decoration(Some(&disk_name), field, alert_tx);
self.model
.set_view_decoration(Some(&disk_name), field, alert_tx);
}
let mut alert_latency_rx =
self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), Some(&disk_name), false);
let mut alert_latency_tx =
self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), Some(&disk_name), false);
let mut alert_latency_rx = self.model.get_alert(
read_latency,
0.0,
100.0,
true,
false,
Some("rx_latency"),
Some(&disk_name),
false,
);
let mut alert_latency_tx = self.model.get_alert(
write_latency,
0.0,
100.0,
true,
false,
Some("tx_latency"),
Some(&disk_name),
false,
);
if alert_latency_rx == Decoration::Default {
alert_latency_rx = self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), None, false);
alert_latency_rx = self.model.get_alert(
read_latency,
0.0,
100.0,
true,
false,
Some("rx_latency"),
None,
false,
);
}
if alert_latency_tx == Decoration::Default {
alert_latency_tx = self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), None, false);
alert_latency_tx = self.model.get_alert(
write_latency,
0.0,
100.0,
true,
false,
Some("tx_latency"),
None,
false,
);
}
self.model.set_view_decoration(Some(&disk_name), "read_latency", alert_latency_rx);
self.model.set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx);
self.model
.set_view_decoration(Some(&disk_name), "read_latency", alert_latency_rx);
self.model
.set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx);
}
}
@@ -163,7 +254,14 @@ impl Plugin for DiskioPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(13);
@@ -175,21 +273,61 @@ impl Plugin for DiskioPlugin {
false,
));
if ctx.args.diskio_iops {
ret.push(self.model.curse_add_line(format!("{:>8}", "IOR/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "IOW/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8}", "IOR/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "IOW/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else if ctx.args.diskio_latency {
ret.push(self.model.curse_add_line(format!("{:>8}", "ms/opR"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "ms/opW"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8}", "ms/opR"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "ms/opW"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:>8}", "R/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "W/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8}", "R/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "W/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
let stats = self.model.sorted_stats();
let Some(items) = stats.as_array() else {
return ret;
};
for i in items {
let disk_real_name = i.get("disk_name").and_then(Value::as_str).unwrap_or("").to_string();
let disk_real_name = i
.get("disk_name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
if self
.model
.hide_zero_fields
@@ -204,7 +342,10 @@ impl Plugin for DiskioPlugin {
.map(|s| s.to_string())
.unwrap_or_else(|| disk_real_name.clone());
if disk_name.chars().count() > name_max_width {
disk_name = format!("{}_", disk_name.chars().take(name_max_width).collect::<String>());
disk_name = format!(
"{}_",
disk_name.chars().take(name_max_width).collect::<String>()
);
}
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
@@ -215,20 +356,108 @@ impl Plugin for DiskioPlugin {
false,
));
if ctx.args.diskio_iops {
let rx = self.model.auto_unit(i.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
let tx = self.model.auto_unit(i.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_count"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_count"), false, false, false));
let rx = self.model.auto_unit(
i.get("read_count_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0),
false,
"K",
);
let tx = self.model.auto_unit(
i.get("write_count_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0),
false,
"K",
);
ret.push(
self.model.curse_add_line(
format!("{rx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "read_count"),
false,
false,
false,
),
);
ret.push(
self.model.curse_add_line(
format!("{tx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "write_count"),
false,
false,
false,
),
);
} else if ctx.args.diskio_latency {
let rx = self.model.auto_unit(i.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K");
let tx = self.model.auto_unit(i.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_latency"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_latency"), false, false, false));
let rx = self.model.auto_unit(
i.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0),
true,
"K",
);
let tx = self.model.auto_unit(
i.get("write_latency")
.and_then(Value::as_f64)
.unwrap_or(0.0),
true,
"K",
);
ret.push(
self.model.curse_add_line(
format!("{rx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "read_latency"),
false,
false,
false,
),
);
ret.push(
self.model.curse_add_line(
format!("{tx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "write_latency"),
false,
false,
false,
),
);
} else {
let rx = self.model.auto_unit(i.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
let tx = self.model.auto_unit(i.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_bytes"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_bytes"), false, false, false));
let rx = self.model.auto_unit(
i.get("read_bytes_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0),
false,
"K",
);
let tx = self.model.auto_unit(
i.get("write_bytes_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0),
false,
"K",
);
ret.push(
self.model.curse_add_line(
format!("{rx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "read_bytes"),
false,
false,
false,
),
);
ret.push(
self.model.curse_add_line(
format!("{tx:>7}"),
self.model
.view_decoration(Some(&disk_real_name), "write_bytes"),
false,
false,
false,
),
);
}
}
ret
+43 -23
View File
@@ -14,7 +14,6 @@ pub struct FoldersPlugin {
#[derive(Debug)]
struct FolderConfig {
indice: String,
path: String,
refresh: f64,
careful: Option<f64>,
@@ -59,12 +58,14 @@ impl FoldersPlugin {
if path.is_empty() {
continue;
}
let refresh = config.get_float_value("folders", &format!("{key}refresh"), default_refresh);
let refresh =
config.get_float_value("folders", &format!("{key}refresh"), default_refresh);
let threshold = |name: &str| -> Option<f64> {
config.get_value("folders", &format!("{key}{name}"), None).and_then(|v| v.parse().ok())
config
.get_value("folders", &format!("{key}{name}"), None)
.and_then(|v| v.parse().ok())
};
folders.push(FolderConfig {
indice: index.to_string(),
path,
refresh,
careful: threshold("careful"),
@@ -123,21 +124,22 @@ impl Plugin for FoldersPlugin {
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let items: Vec<(String, i64, u64, Option<f64>, Option<f64>, Option<f64>)> = match &self.model.stats {
Value::Array(list) => list
.iter()
.filter_map(|item| {
let path = item.get("path")?.as_str()?.to_string();
let errno = item.get("errno")?.as_i64()?;
let size = item.get("size")?.as_u64()?;
let careful = item.get("careful").and_then(Value::as_f64);
let warning = item.get("warning").and_then(Value::as_f64);
let critical = item.get("critical").and_then(Value::as_f64);
Some((path, errno, size, careful, warning, critical))
})
.collect(),
_ => Vec::new(),
};
let items: Vec<(String, i64, u64, Option<f64>, Option<f64>, Option<f64>)> =
match &self.model.stats {
Value::Array(list) => list
.iter()
.filter_map(|item| {
let path = item.get("path")?.as_str()?.to_string();
let errno = item.get("errno")?.as_i64()?;
let size = item.get("size")?.as_u64()?;
let careful = item.get("careful").and_then(Value::as_f64);
let warning = item.get("warning").and_then(Value::as_f64);
let critical = item.get("critical").and_then(Value::as_f64);
Some((path, errno, size, careful, warning, critical))
})
.collect(),
_ => Vec::new(),
};
for (path, errno, size, careful, warning, critical) in items {
if errno != 0 {
continue;
@@ -151,7 +153,8 @@ impl Plugin for FoldersPlugin {
} else {
glances_core::Decoration::Default
};
self.model.set_view_decoration(Some(&path), "size", decoration);
self.model
.set_view_decoration(Some(&path), "size", decoration);
}
}
@@ -161,7 +164,14 @@ impl Plugin for FoldersPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(12);
@@ -172,7 +182,13 @@ impl Plugin for FoldersPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>9}", "Size"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>9}", "Size"),
glances_core::Decoration::Default,
false,
false,
false,
));
let items = self.model.sorted_stats();
let Some(items) = items.as_array() else {
return ret;
@@ -183,7 +199,11 @@ impl Plugin for FoldersPlugin {
let errno = i.get("errno").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", path.chars().take(name_max_width).collect::<String>(), width = name_max_width),
format!(
"{:<width$}",
path.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
+80 -15
View File
@@ -59,11 +59,16 @@ impl Plugin for FsPlugin {
let mut stats = Vec::new();
for mount in crate::collectors::read_mounts() {
let is_physical = mount.device.starts_with("/dev/");
let is_allowed = allowed_fs_types.iter().any(|fs_type| mount.fs_type.contains(fs_type.as_str()));
let is_allowed = allowed_fs_types
.iter()
.any(|fs_type| mount.fs_type.contains(fs_type.as_str()));
if !is_physical && !is_allowed {
continue;
}
if !self.model.is_display_any(&[&mount.mount_point, &mount.device], "") {
if !self
.model
.is_display_any(&[&mount.mount_point, &mount.device], "")
{
continue;
}
let Some(usage) = crate::collectors::disk_usage(&mount.mount_point) else {
@@ -78,7 +83,10 @@ impl Plugin for FsPlugin {
item.insert("size".to_string(), json!(usage.total));
item.insert("used".to_string(), json!(usage.used));
item.insert("free".to_string(), json!(usage.free));
item.insert("percent".to_string(), json!((usage.percent * 10.0).round() / 10.0));
item.insert(
"percent".to_string(),
json!((usage.percent * 10.0).round() / 10.0),
);
item.insert("key".to_string(), json!("mnt_point"));
if let Some(alias) = self.model.has_alias(&mnt_point) {
item.insert("alias".to_string(), json!(alias));
@@ -96,7 +104,11 @@ impl Plugin for FsPlugin {
.iter()
.filter_map(|item| {
let mnt = item.get("mnt_point")?.as_str()?.to_string();
let options = item.get("options").and_then(Value::as_str).unwrap_or("").to_string();
let options = item
.get("options")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let size = item.get("size")?.as_f64()?;
let free = item.get("free")?.as_f64()?;
Some((mnt, size, free, options))
@@ -109,8 +121,18 @@ impl Plugin for FsPlugin {
if options.split(',').any(|o| o == "ro") {
continue;
}
let decoration = self.model.get_alert(size - free, 0.0, size, true, false, Some(&mnt_point), None, false);
self.model.set_view_decoration(Some(&mnt_point), "used", decoration);
let decoration = self.model.get_alert(
size - free,
0.0,
size,
true,
false,
Some(&mnt_point),
None,
false,
);
self.model
.set_view_decoration(Some(&mnt_point), "used", decoration);
}
}
@@ -120,7 +142,14 @@ impl Plugin for FsPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(13);
@@ -132,30 +161,66 @@ impl Plugin for FsPlugin {
false,
));
if ctx.runtime.fs_free_space {
ret.push(self.model.curse_add_line(format!("{:>8}", "Free"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8}", "Free"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:>8}", "Used"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8}", "Used"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!("{:>7}", "Total"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Total"),
glances_core::Decoration::Default,
false,
false,
false,
));
let Some(items) = self.model.stats.as_array() else {
return ret;
};
let mut sorted = items.clone();
sorted.sort_by_key(|i| i.get("mnt_point").and_then(Value::as_str).unwrap_or("").to_string());
sorted.sort_by_key(|i| {
i.get("mnt_point")
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
});
for i in &sorted {
let mnt_point_raw = i.get("mnt_point").and_then(Value::as_str).unwrap_or("").to_string();
let device = i.get("device_name").and_then(Value::as_str).unwrap_or("").to_string();
let mnt_point_raw = i
.get("mnt_point")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let device = i
.get("device_name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let mut mnt_point = i
.get("alias")
.and_then(Value::as_str)
.map(|s| s.to_string())
.unwrap_or_else(|| mnt_point_raw.clone());
let device_basename = device.rsplit('/').next().unwrap_or("").to_string();
if mnt_point.chars().count() + device_basename.chars().count() <= name_max_width.saturating_sub(3) {
if mnt_point.chars().count() + device_basename.chars().count()
<= name_max_width.saturating_sub(3)
{
mnt_point = format!("{mnt_point} ({device_basename})");
} else if mnt_point.chars().count() > name_max_width {
mnt_point = format!("{}_", mnt_point.chars().take(name_max_width).collect::<String>());
mnt_point = format!(
"{}_",
mnt_point.chars().take(name_max_width).collect::<String>()
);
}
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
+31 -13
View File
@@ -80,12 +80,15 @@ impl IpPlugin {
if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) {
request = request.header("Authorization", &format!("Basic {}:{}", username, password));
}
if let Ok(mut response) = request.call() {
if let Ok(value) = response.body_mut().read_json::<Value>() {
self.public_address =
value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string();
self.public_info = Some(value);
}
if let Ok(mut response) = request.call()
&& let Ok(value) = response.body_mut().read_json::<Value>()
{
self.public_address = value
.get(&self.public_field)
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
self.public_info = Some(value);
}
}
@@ -130,7 +133,10 @@ impl Plugin for IpPlugin {
if let Some(mask) = &mask {
stats.insert("mask".to_string(), json!(mask));
}
stats.insert("mask_cidr".to_string(), json!(crate::collectors::ip_to_cidr(mask.as_deref())));
stats.insert(
"mask_cidr".to_string(),
json!(crate::collectors::ip_to_cidr(mask.as_deref())),
);
self.refresh_public();
if !self.public_address.is_empty() {
let public = if ctx.args.hide_public_info {
@@ -158,14 +164,26 @@ impl Plugin for IpPlugin {
return ret;
};
let mut text = address.to_string();
if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) {
if mask_cidr > 0 {
text += &format!("/{mask_cidr}");
}
if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64)
&& mask_cidr > 0
{
text += &format!("/{mask_cidr}");
}
ret.push(self.model.curse_add_line(text, glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
text,
glances_core::Decoration::Default,
false,
false,
false,
));
if let Some(gateway) = s.get("gateway").and_then(Value::as_str) {
ret.push(self.model.curse_add_line(format!(" gw {gateway}"), glances_core::Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!(" gw {gateway}"),
glances_core::Decoration::Default,
true,
false,
false,
));
}
ret
}
+39 -7
View File
@@ -28,7 +28,10 @@ impl IrqPlugin {
args.disable_config_exec,
);
model.set_item_key("irq_line");
Self { model, lasts: IndexMap::new() }
Self {
model,
lasts: IndexMap::new(),
}
}
}
@@ -57,7 +60,10 @@ impl Plugin for IrqPlugin {
return Ok(Value::Array(stats));
};
let mut lines = content.lines();
let cpu_number = lines.next().map(|header| header.split_whitespace().count()).unwrap_or(0);
let cpu_number = lines
.next()
.map(|header| header.split_whitespace().count())
.unwrap_or(0);
for line in lines {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 2 {
@@ -69,7 +75,10 @@ impl Plugin for IrqPlugin {
} else {
first.to_string()
};
let current: u64 = fields[1..1 + cpu_number].iter().filter_map(|v| v.parse::<u64>().ok()).sum();
let current: u64 = fields[1..1 + cpu_number]
.iter()
.filter_map(|v| v.parse::<u64>().ok())
.sum();
let irq_rate = match self.lasts.get(&irq_line) {
Some(last) if *last > 0 && time_since_update > 0.0 => {
((current.saturating_sub(*last)) as f64 / time_since_update) as i64
@@ -101,7 +110,14 @@ impl Plugin for IrqPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(7);
@@ -112,7 +128,13 @@ impl Plugin for IrqPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>9}", "Rate/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>9}", "Rate/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
let Some(items) = self.model.stats.as_array() else {
return ret;
};
@@ -121,13 +143,23 @@ impl Plugin for IrqPlugin {
let rate = i.get("irq_rate").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", line.chars().take(name_max_width).collect::<String>(), width = name_max_width),
format!(
"{:<width$}",
line.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{rate:>9}"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(format!("{rate:>9}"), glances_core::Decoration::Default, false, false, false));
}
ret
}
+42 -10
View File
@@ -64,13 +64,21 @@ impl Plugin for LoadPlugin {
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let max = self.model.stats.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0) * 100.0;
let max = self
.model
.stats
.get("cpucore")
.and_then(Value::as_f64)
.unwrap_or(0.0)
* 100.0;
if let Some(min15) = self.model.stats.get("min15").and_then(Value::as_f64) {
let decoration = self.model.get_alert_log(min15, max, None);
self.model.set_view_decoration(None, "min15", decoration);
}
if let Some(min5) = self.model.stats.get("min5").and_then(Value::as_f64) {
let decoration = self.model.get_alert(min5, 0.0, max, true, false, None, None, false);
let decoration = self
.model
.get_alert(min5, 0.0, max, true, false, None, None, false);
self.model.set_view_decoration(None, "min5", decoration);
}
}
@@ -85,23 +93,47 @@ impl Plugin for LoadPlugin {
if !s.is_object() || s.as_object().map(|o| o.is_empty()).unwrap_or(true) {
return ret;
}
ret.push(self.model.curse_add_line(format!("{:4}", "LOAD"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
format!(" {:1}", self.model.trend_msg(self.model.get_trend("min1", 30), 1.0)),
format!("{:4}", "LOAD"),
glances_core::Decoration::Title,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!(
" {:1}",
self.model.trend_msg(self.model.get_trend("min1", 30), 1.0)
),
glances_core::Decoration::Default,
false,
false,
false,
));
if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64) {
if cpucore > 0.0 {
ret.push(self.model.curse_add_line(format!("{:3}core", cpucore as i64), glances_core::Decoration::Default, false, false, false));
}
if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64)
&& cpucore > 0.0
{
ret.push(self.model.curse_add_line(
format!("{:3}core", cpucore as i64),
glances_core::Decoration::Default,
false,
false,
false,
));
}
for load_time in ["1", "5", "15"] {
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(format!("{:7}", format!("{load_time} min")), glances_core::Decoration::Default, false, false, false));
let stat = s.get(&format!("min{load_time}")).and_then(Value::as_f64).unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:7}", format!("{load_time} min")),
glances_core::Decoration::Default,
false,
false,
false,
));
let stat = s
.get(format!("min{load_time}"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let value = if ctx.runtime.disable_irix {
let cores = s.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0);
if cores != 0.0 {
+35 -6
View File
@@ -69,7 +69,11 @@ impl Plugin for MemPlugin {
let buffers = get("Buffers");
// procps-ng "free" sums reclaimable slab to cached.
let cached = get("Cached") + get("SReclaimable");
let shared = if meminfo.contains_key("Shmem") { get("Shmem") } else { get("MemShared") };
let shared = if meminfo.contains_key("Shmem") {
get("Shmem")
} else {
get("MemShared")
};
let active = get("Active");
let inactive = get("Inactive");
let mut available = get("MemAvailable");
@@ -104,8 +108,18 @@ impl Plugin for MemPlugin {
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0);
let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0);
let total = self
.model
.stats
.get("total")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let used = self
.model
.stats
.get("used")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration);
}
@@ -121,8 +135,20 @@ impl Plugin for MemPlugin {
return ret;
}
// First line: total% + trend + active.
ret.push(self.model.curse_add_line(format!("{}", "MEM"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(format!(" {:2}", self.model.trend_msg(self.model.get_trend("percent", 30), 1.0)), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
"MEM".to_string(),
glances_core::Decoration::Title,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!(" {:2}", self.model.trend_msg(self.model.get_trend("percent", 30), 1.0)),
glances_core::Decoration::Default,
false,
false,
false,
));
if let Some(percent) = s.get("percent").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{:>6.1}%", percent),
@@ -136,7 +162,10 @@ impl Plugin for MemPlugin {
// Second line: total + inactive.
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("total", Some(15), "", "", ""));
ret.extend(self.model.curse_add_stat("inactive", Some(16), " ", "", ""));
ret.extend(
self.model
.curse_add_stat("inactive", Some(16), " ", "", ""),
);
// Third line: available + buffers.
ret.push(self.model.curse_new_line());
if self.available {
+35 -6
View File
@@ -53,7 +53,11 @@ impl Plugin for MemSwapPlugin {
let total = meminfo.get("SwapTotal").copied().unwrap_or(0);
let free = meminfo.get("SwapFree").copied().unwrap_or(0);
let used = total.saturating_sub(free);
let percent = if total > 0 { (used as f64 * 100.0 / total as f64 * 10.0).round() / 10.0 } else { 0.0 };
let percent = if total > 0 {
(used as f64 * 100.0 / total as f64 * 10.0).round() / 10.0
} else {
0.0
};
let vmstat = crate::collectors::read_vmstat();
// pswpin/pswpout are expressed in 4 KiB pages.
let sin = vmstat.get("pswpin").copied().unwrap_or(0) * 4 * 1024;
@@ -73,8 +77,18 @@ impl Plugin for MemSwapPlugin {
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0);
let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0);
let total = self
.model
.stats
.get("total")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let used = self
.model
.stats
.get("used")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration);
}
@@ -89,10 +103,25 @@ impl Plugin for MemSwapPlugin {
if !s.is_object() {
return ret;
}
ret.push(self.model.curse_add_line("SWAP", glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(" ", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>6.1}%", s.get("percent").and_then(Value::as_f64).unwrap_or(0.0)),
"SWAP",
glances_core::Decoration::Title,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
" ",
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!(
"{:>6.1}%",
s.get("percent").and_then(Value::as_f64).unwrap_or(0.0)
),
self.model.view_decoration(None, "percent"),
false,
false,
+123 -24
View File
@@ -39,11 +39,19 @@ impl NetworkPlugin {
);
model.set_item_key("interface_name");
model.hide_zero = config.get_bool_value("network", "hide_zero", false);
model.hide_zero_fields =
vec!["bytes_recv_rate_per_sec".to_string(), "bytes_sent_rate_per_sec".to_string()];
let hide_no_up = config.has_section("network") && config.get_bool_value("network", "hide_no_up", false);
let hide_no_ip = config.has_section("network") && config.get_bool_value("network", "hide_no_ip", false);
Self { model, hide_no_up, hide_no_ip }
model.hide_zero_fields = vec![
"bytes_recv_rate_per_sec".to_string(),
"bytes_sent_rate_per_sec".to_string(),
];
let hide_no_up =
config.has_section("network") && config.get_bool_value("network", "hide_no_up", false);
let hide_no_ip =
config.has_section("network") && config.get_bool_value("network", "hide_no_ip", false);
Self {
model,
hide_no_up,
hide_no_ip,
}
}
}
@@ -90,7 +98,10 @@ impl Plugin for NetworkPlugin {
item.insert("key".to_string(), json!("interface_name"));
item.insert("interface_name".to_string(), json!(interface_name));
item.insert("alias".to_string(), json!(alias));
item.insert("bytes_all".to_string(), json!(counters.bytes_sent + counters.bytes_recv));
item.insert(
"bytes_all".to_string(),
json!(counters.bytes_sent + counters.bytes_recv),
);
stats.push(Value::Object(item));
}
Ok(Value::Array(stats))
@@ -104,8 +115,14 @@ impl Plugin for NetworkPlugin {
.iter()
.filter_map(|item| {
let name = item.get("interface_name")?.as_str()?.to_string();
let rx = item.get("bytes_recv_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let tx = item.get("bytes_sent_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0);
let rx = item
.get("bytes_recv_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let tx = item
.get("bytes_sent_rate_per_sec")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let speed = item.get("speed").and_then(Value::as_f64).unwrap_or(0.0);
let real_name = name.split(':').next().unwrap_or("").to_string();
Some((name, rx, tx, speed, Some(real_name)))
@@ -117,21 +134,57 @@ impl Plugin for NetworkPlugin {
let bps_rx = rx as i64 * 8;
let bps_tx = tx as i64 * 8;
let action_key = action_key.as_deref();
let mut alert_rx =
self.model.get_alert(bps_rx as f64, 0.0, 100.0, true, false, Some("rx"), action_key, false);
let mut alert_tx =
self.model.get_alert(bps_tx as f64, 0.0, 100.0, true, false, Some("tx"), action_key, false);
let mut alert_rx = self.model.get_alert(
bps_rx as f64,
0.0,
100.0,
true,
false,
Some("rx"),
action_key,
false,
);
let mut alert_tx = self.model.get_alert(
bps_tx as f64,
0.0,
100.0,
true,
false,
Some("tx"),
action_key,
false,
);
if alert_rx == glances_core::Decoration::Default && speed != 0.0 {
alert_rx = self.model.get_alert(bps_rx as f64, 0.0, speed, true, false, Some("rx"), None, false);
alert_rx = self.model.get_alert(
bps_rx as f64,
0.0,
speed,
true,
false,
Some("rx"),
None,
false,
);
}
if alert_tx == glances_core::Decoration::Default && speed != 0.0 {
alert_tx = self.model.get_alert(bps_tx as f64, 0.0, speed, true, false, Some("tx"), None, false);
alert_tx = self.model.get_alert(
bps_tx as f64,
0.0,
speed,
true,
false,
Some("tx"),
None,
false,
);
}
for field in ["bytes_recv", "bytes_recv_rate_per_sec"] {
self.model.set_view_decoration(Some(&interface_name), field, alert_rx);
self.model
.set_view_decoration(Some(&interface_name), field, alert_rx);
}
for field in ["bytes_sent", "bytes_sent_rate_per_sec"] {
self.model.set_view_decoration(Some(&interface_name), field, alert_tx);
self.model
.set_view_decoration(Some(&interface_name), field, alert_tx);
}
}
}
@@ -156,16 +209,52 @@ impl Plugin for NetworkPlugin {
));
if ctx.runtime.network_cumul {
if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{:>14}", "Rx+Tx"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>14}", "Rx+Tx"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:>7}", "Rx"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Rx"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Tx"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
} else if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{:>14}", "Rx+Tx/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>14}", "Rx+Tx/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:>7}", "Rx/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Rx/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Tx/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
let stats = self.model.sorted_stats();
@@ -173,7 +262,11 @@ impl Plugin for NetworkPlugin {
return ret;
};
for i in items {
let item_key = i.get("interface_name").and_then(Value::as_str).unwrap_or("").to_string();
let item_key = i
.get("interface_name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
// Hide stats never different from zero.
if self
.model
@@ -227,7 +320,13 @@ impl Plugin for NetworkPlugin {
false,
));
if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{ax:>14}"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{ax:>14}"),
glances_core::Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(
format!("{rx:>7}"),
+19 -4
View File
@@ -43,11 +43,15 @@ impl Plugin for NowPlugin {
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let iso = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string();
let iso = chrono::Local::now()
.format("%Y-%m-%dT%H:%M:%S%:z")
.to_string();
let strftime = if !ctx.args.strftime_format.is_empty() {
ctx.args.strftime_format.clone()
} else {
ctx.config.get_value("global", "strftime_format", Some("")).unwrap_or_default()
ctx.config
.get_value("global", "strftime_format", Some(""))
.unwrap_or_default()
};
let custom = if !strftime.is_empty() {
crate::collectors::strftime(&strftime)
@@ -72,7 +76,18 @@ impl Plugin for NowPlugin {
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let custom = self.model.stats.get("custom").and_then(Value::as_str).unwrap_or("");
vec![self.model.curse_add_line(format!("{custom:<23}"), glances_core::Decoration::Default, false, false, false)]
let custom = self
.model
.stats
.get("custom")
.and_then(Value::as_str)
.unwrap_or("");
vec![self.model.curse_add_line(
format!("{custom:<23}"),
glances_core::Decoration::Default,
false,
false,
false,
)]
}
}
+69 -12
View File
@@ -41,7 +41,10 @@ impl PerCpuPlugin {
);
model.set_item_key("cpu_number");
let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(1) as usize;
Self { model, max_cpu_display }
Self {
model,
max_cpu_display,
}
}
}
@@ -75,13 +78,27 @@ impl Plugin for PerCpuPlugin {
}
let items = &items;
let quicklook_disabled = !ctx.runtime.is_plugin_enabled("quicklook");
let mut header = vec!["user", "system", "iowait", "idle", "irq", "nice", "steal", "guest"];
let mut header = vec![
"user", "system", "iowait", "idle", "irq", "nice", "steal", "guest",
];
if quicklook_disabled {
ret.push(self.model.curse_add_line(format!("{:5}", "CPU"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:5}", "CPU"),
glances_core::Decoration::Title,
false,
false,
false,
));
header.insert(0, "total");
}
for stat in &header {
ret.push(self.model.curse_add_line(format!("{stat:>7}"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{stat:>7}"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
// Sort by total and keep the top max_cpu_display cores.
let mut sorted: Vec<Value> = items.clone();
@@ -99,26 +116,54 @@ impl Plugin for PerCpuPlugin {
ret.push(self.model.curse_new_line());
if quicklook_disabled {
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0);
let msg = if cpu_id < 10 { format!("CPU{cpu_id:1} ") } else { format!("{cpu_id:4} ") };
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
let msg = if cpu_id < 10 {
format!("CPU{cpu_id:1} ")
} else {
format!("{cpu_id:4} ")
};
ret.push(self.model.curse_add_line(
msg,
glances_core::Decoration::Default,
false,
false,
false,
));
}
for stat in &header {
let value = cpu.get(*stat).and_then(Value::as_f64);
let decoration = match value {
Some(value) => self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat), None, false),
Some(value) => self.model.get_alert(
value,
0.0,
100.0,
true,
false,
Some(stat),
None,
false,
),
None => glances_core::Decoration::Default,
};
let msg = match value {
Some(value) => format!("{value:6.1}%"),
None => format!("{:>6}%", "?"),
};
ret.push(self.model.curse_add_line(msg, decoration, false, false, false));
ret.push(
self.model
.curse_add_line(msg, decoration, false, false, false),
);
}
}
if items.len() > self.max_cpu_display {
ret.push(self.model.curse_new_line());
if quicklook_disabled {
ret.push(self.model.curse_add_line("CPU* ", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
"CPU* ",
glances_core::Decoration::Default,
false,
false,
false,
));
}
for stat in &header {
let values: Vec<f64> = sorted
@@ -126,9 +171,21 @@ impl Plugin for PerCpuPlugin {
.skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get(*stat).and_then(Value::as_f64))
.collect();
let mean = if values.is_empty() { 0.0 } else { values.iter().sum::<f64>() / values.len() as f64 };
let decoration = self.model.get_alert(mean, 0.0, 100.0, true, false, Some(stat), None, false);
ret.push(self.model.curse_add_line(format!("{mean:6.1}%"), decoration, false, false, false));
let mean = if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
};
let decoration =
self.model
.get_alert(mean, 0.0, 100.0, true, false, Some(stat), None, false);
ret.push(self.model.curse_add_line(
format!("{mean:6.1}%"),
decoration,
false,
false,
false,
));
}
}
ret
+66 -27
View File
@@ -57,21 +57,21 @@ impl PortsPlugin {
let mut entries = Vec::new();
if config.has_section("ports") {
// Default gateway entry.
if config.get_bool_value("ports", "port_default_gateway", false) {
if let Some(gateway) = crate::collectors::get_default_gateway() {
entries.push(PortEntry {
indice: "port_0".to_string(),
host: gateway,
port: None,
url: None,
description: "Default gateway".to_string(),
refresh,
timeout: default_timeout,
rtt_warning: None,
timer: Instant::now() - Duration::from_secs(3600),
status: None,
});
}
if config.get_bool_value("ports", "port_default_gateway", false)
&& let Some(gateway) = crate::collectors::get_default_gateway()
{
entries.push(PortEntry {
indice: "port_0".to_string(),
host: gateway,
port: None,
url: None,
description: "Default gateway".to_string(),
refresh,
timeout: default_timeout,
rtt_warning: None,
timer: Instant::now() - Duration::from_secs(3600),
status: None,
});
}
for index in 1..=255 {
let key = format!("port_{index}_");
@@ -81,14 +81,17 @@ impl PortsPlugin {
if host.is_empty() {
continue;
}
let port = config.get_value("ports", &format!("{key}port"), None).and_then(|v| v.parse().ok());
let port = config
.get_value("ports", &format!("{key}port"), None)
.and_then(|v| v.parse().ok());
let description = config
.get_value("ports", &format!("{key}description"), None)
.unwrap_or_else(|| match port {
Some(port) => format!("{host}:{port}"),
None => host.clone(),
});
let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
let timeout =
config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
let rtt_warning = config
.get_value("ports", &format!("{key}rtt_warning"), None)
.and_then(|v| v.parse::<f64>().ok())
@@ -114,8 +117,11 @@ impl PortsPlugin {
if url.is_empty() {
continue;
}
let description = config.get_value("ports", &format!("{key}description"), None).unwrap_or_else(|| url.clone());
let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
let description = config
.get_value("ports", &format!("{key}description"), None)
.unwrap_or_else(|| url.clone());
let timeout =
config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
let rtt_warning = config
.get_value("ports", &format!("{key}rtt_warning"), None)
.and_then(|v| v.parse::<f64>().ok())
@@ -134,14 +140,21 @@ impl PortsPlugin {
});
}
}
Self { model, entries, default_timeout }
Self {
model,
entries,
default_timeout,
}
}
fn check(entry: &PortEntry) -> Option<f64> {
let start = Instant::now();
if let Some(url) = &entry.url {
let timeout = Duration::from_secs_f64(entry.timeout.max(0.1));
let request = ureq::get(url).config().timeout_global(Some(timeout)).build();
let request = ureq::get(url)
.config()
.timeout_global(Some(timeout))
.build();
match request.call() {
Ok(_) => Some(start.elapsed().as_secs_f64()),
Err(_) => None,
@@ -158,7 +171,13 @@ impl PortsPlugin {
} else {
// ICMP check with the ping command.
let output = std::process::Command::new("ping")
.args(["-c", "1", "-W", &format!("{}", entry.timeout.max(1.0) as i64), &entry.host])
.args([
"-c",
"1",
"-W",
&format!("{}", entry.timeout.max(1.0) as i64),
&entry.host,
])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
@@ -237,10 +256,13 @@ impl Plugin for PortsPlugin {
for (indice, status, warning) in items {
let decoration = match status {
None => glances_core::Decoration::Critical,
Some(status) if warning.is_some_and(|w| status > w) => glances_core::Decoration::Warning,
Some(status) if warning.is_some_and(|w| status > w) => {
glances_core::Decoration::Warning
}
Some(_) => glances_core::Decoration::Ok,
};
self.model.set_view_decoration(Some(&indice), "status", decoration);
self.model
.set_view_decoration(Some(&indice), "status", decoration);
}
}
@@ -250,7 +272,14 @@ impl Plugin for PortsPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(12);
@@ -261,7 +290,13 @@ impl Plugin for PortsPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>9}", "RTT"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>9}", "RTT"),
glances_core::Decoration::Default,
false,
false,
false,
));
let items = self.model.sorted_stats();
let Some(items) = items.as_array() else {
return ret;
@@ -272,7 +307,11 @@ impl Plugin for PortsPlugin {
let status = i.get("status").and_then(Value::as_f64);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", description.chars().take(name_max_width).collect::<String>(), width = name_max_width),
format!(
"{:<width$}",
description.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
@@ -77,50 +77,127 @@ impl Plugin for ProcessCountPlugin {
return ret;
}
if ctx.args.disable_process {
ret.push(self.model.curse_add_line("PROCESSES DISABLED (press 'z' to display)", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
"PROCESSES DISABLED (press 'z' to display)",
glances_core::Decoration::Default,
false,
false,
false,
));
return ret;
}
// Filter line.
{
let procs = glances_core::processes::processes().lock().unwrap();
if let Some(filter) = procs.filter.filter() {
ret.push(self.model.curse_add_line("Processes filter:", glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
"Processes filter:",
glances_core::Decoration::Title,
false,
false,
false,
));
let mut msg = format!(" {filter} ");
if let Some(key) = procs.filter.filter_key() {
msg += &format!("on column {key} ");
}
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Filter, false, false, false));
ret.push(self.model.curse_add_line("('ENTER' to edit, 'E' to reset)", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
msg,
glances_core::Decoration::Filter,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
"('ENTER' to edit, 'E' to reset)",
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_new_line());
}
}
ret.push(self.model.curse_add_line("TASKS", glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
"TASKS",
glances_core::Decoration::Title,
false,
false,
false,
));
let total = s.get("total").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_add_line(format!("{total:>4}"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{total:>4}"),
glances_core::Decoration::Default,
false,
false,
false,
));
if let Some(thread) = s.get("thread").and_then(Value::as_i64) {
ret.push(self.model.curse_add_line(format!(" ({thread} thr),"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!(" ({thread} thr),"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
let mut other = total;
if let Some(running) = s.get("running").and_then(Value::as_i64) {
other -= running;
ret.push(self.model.curse_add_line(format!(" {running} run,"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!(" {running} run,"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
if let Some(sleeping) = s.get("sleeping").and_then(Value::as_i64) {
other -= sleeping;
ret.push(self.model.curse_add_line(format!(" {sleeping} slp,"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!(" {sleeping} slp,"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!(" {other} oth "), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!(" {other} oth "),
glances_core::Decoration::Default,
false,
false,
false,
));
let procs = glances_core::processes::processes().lock().unwrap();
let mut msg = if ctx.args.programs { "Programs" } else { "Threads" }.to_string();
let mut msg = if ctx.args.programs {
"Programs"
} else {
"Threads"
}
.to_string();
let sort_human = glances_core::processes::sort_for_human(&procs.sort_key);
if procs.auto_sort {
msg += " sorted automatically";
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
msg,
glances_core::Decoration::Default,
false,
false,
false,
));
msg = format!(" by {sort_human}");
} else {
msg += &format!(" sorted by {sort_human}");
}
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
msg,
glances_core::Decoration::Default,
false,
false,
false,
));
ret
}
}
+320 -74
View File
@@ -43,33 +43,48 @@ impl ProcessListPlugin {
model.set_item_key("pid");
// Load the configuration.
if config.has_option("processlist", "sort_key") {
if let Some(sort_key) = config.get_value("processlist", "sort_key", None) {
let mut procs = processes().lock().unwrap();
procs.set_sort_key(&sort_key, false);
}
if config.has_option("processlist", "sort_key")
&& let Some(sort_key) = config.get_value("processlist", "sort_key", None)
{
let mut procs = processes().lock().unwrap();
procs.set_sort_key(&sort_key, false);
}
if config.has_option("processlist", "focus") {
if let Some(focus) = config.get_value("processlist", "focus", None) {
processes().lock().unwrap().filter_focus.set_filter(Some(&focus));
}
if config.has_option("processlist", "focus")
&& let Some(focus) = config.get_value("processlist", "focus", None)
{
processes()
.lock()
.unwrap()
.filter_focus
.set_filter(Some(&focus));
}
if config.has_option("processlist", "disable_stats") {
if let Some(disabled) = config.get_value("processlist", "disable_stats", None) {
let stats: Vec<String> = disabled
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && !glances_core::processes::MANDATORY_PROCESS_STATS.contains(&s.as_str()))
.collect();
processes().lock().unwrap().disable_stats = stats;
}
if config.has_option("processlist", "disable_stats")
&& let Some(disabled) = config.get_value("processlist", "disable_stats", None)
{
let stats: Vec<String> = disabled
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| {
!s.is_empty()
&& !glances_core::processes::MANDATORY_PROCESS_STATS.contains(&s.as_str())
})
.collect();
processes().lock().unwrap().disable_stats = stats;
}
if let Some(sort_key) = args.sort_processes_key {
processes().lock().unwrap().set_sort_key(sort_key.as_str(), false);
processes()
.lock()
.unwrap()
.set_sort_key(sort_key.as_str(), false);
}
let nb_log_core = crate::collectors::logical_core_count();
let disable_virtual_memory = config.get_bool_value("processlist", "disable_virtual_memory", false);
Self { model, nb_log_core, disable_virtual_memory }
let disable_virtual_memory =
config.get_bool_value("processlist", "disable_virtual_memory", false);
Self {
model,
nb_log_core,
disable_virtual_memory,
}
}
}
@@ -129,7 +144,11 @@ impl Plugin for ProcessListPlugin {
let procs = glances_core::processes::processes().lock().unwrap();
(
procs.max_values.get("cpu_percent").copied().unwrap_or(0.0),
procs.max_values.get("memory_percent").copied().unwrap_or(0.0),
procs
.max_values
.get("memory_percent")
.copied()
.unwrap_or(0.0),
procs.sort_key.clone(),
)
};
@@ -140,33 +159,120 @@ impl Plugin for ProcessListPlugin {
// Header.
if ctx.runtime.disable_irix {
ret.push(self.model.curse_add_line(format!("{:<6} ", "CPUi"), Decoration::Sort, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:<6} ", "CPUi"),
Decoration::Sort,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:<6} ", "CPU%"), Decoration::Sort, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:<6} ", "CPU%"),
Decoration::Sort,
false,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!("{:<5} ", "MEM%"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:<5} ", "MEM%"),
Decoration::Default,
false,
false,
false,
));
if wide {
if !self.disable_virtual_memory {
ret.push(self.model.curse_add_line(format!("{:<5} ", "VIRT"), Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!("{:<5} ", "VIRT"),
Decoration::Default,
true,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!("{:<5} ", "RES"), Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!("{:<5} ", "RES"),
Decoration::Default,
true,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!("{:>5} ", "PID"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:<10} ", "USER"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>5} ", "PID"),
Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:<10} ", "USER"),
Decoration::Default,
false,
false,
false,
));
if wide {
ret.push(self.model.curse_add_line(format!("{:>8} ", "TIME+"), Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!("{:>8} ", "TIME+"),
Decoration::Default,
true,
false,
false,
));
}
ret.push(self.model.curse_add_line(format!("{:<3} ", "THR"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>3} ", "NI"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>1} ", "S"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:<3} ", "THR"),
Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>3} ", "NI"),
Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>1} ", "S"),
Decoration::Default,
false,
false,
false,
));
if wide {
ret.push(self.model.curse_add_line(format!("{:>4} ", "R/s"), Decoration::Default, true, true, false));
ret.push(self.model.curse_add_line(format!("{:<4} ", "W/s"), Decoration::Default, true, true, false));
ret.push(self.model.curse_add_line(
format!("{:>4} ", "R/s"),
Decoration::Default,
true,
true,
false,
));
ret.push(self.model.curse_add_line(
format!("{:<4} ", "W/s"),
Decoration::Default,
true,
true,
false,
));
}
if wide {
ret.push(self.model.curse_add_line(format!("{:>3} ", "CPU"), Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!("{:>3} ", "CPU"),
Decoration::Default,
true,
false,
false,
));
}
ret.push(self.model.curse_add_line("Command", Decoration::Sort, false, false, false));
ret.push(
self.model
.curse_add_line("Command", Decoration::Sort, false, false, false),
);
let _ = sort_key;
let max_pid_size = {
@@ -181,7 +287,11 @@ impl Plugin for ProcessListPlugin {
let selected = position as i64 == cursor;
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
if selected && !disable_cursor { ">" } else { " " },
if selected && !disable_cursor {
">"
} else {
" "
},
Decoration::Selected,
false,
false,
@@ -205,10 +315,19 @@ impl Plugin for ProcessListPlugin {
None,
false,
);
ret.push(self.model.curse_add_line(format!("{cpu_value:<6.1}"), cpu_decoration, false, false, false));
ret.push(self.model.curse_add_line(
format!("{cpu_value:<6.1}"),
cpu_decoration,
false,
false,
false,
));
// MEM%.
let memory_percent = p.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
let memory_percent = p
.get("memory_percent")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let mem_decoration = self.model.get_alert(
memory_percent,
0.0,
@@ -219,13 +338,22 @@ impl Plugin for ProcessListPlugin {
None,
false,
);
ret.push(self.model.curse_add_line(format!("{memory_percent:<5.1} "), mem_decoration, false, false, false));
ret.push(self.model.curse_add_line(
format!("{memory_percent:<5.1} "),
mem_decoration,
false,
false,
false,
));
// VIRT and RES.
let memory_info = p.get("memory_info");
if wide {
if !self.disable_virtual_memory {
let vms = memory_info.and_then(|m| m.get("vms")).and_then(Value::as_f64).unwrap_or(0.0);
let vms = memory_info
.and_then(|m| m.get("vms"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(vms, false, "K")),
Decoration::Default,
@@ -234,7 +362,10 @@ impl Plugin for ProcessListPlugin {
false,
));
}
let rss = memory_info.and_then(|m| m.get("rss")).and_then(Value::as_f64).unwrap_or(0.0);
let rss = memory_info
.and_then(|m| m.get("rss"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(rss, false, "K")),
Decoration::Default,
@@ -246,18 +377,36 @@ impl Plugin for ProcessListPlugin {
// PID.
let pid = p.get("pid").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_add_line(format!("{pid:>max_pid_size$} "), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{pid:>max_pid_size$} "),
Decoration::Default,
false,
false,
false,
));
// USER.
let username = p.get("username").and_then(Value::as_str).unwrap_or("?");
let username: String = username.chars().take(9).collect();
ret.push(self.model.curse_add_line(format!("{username:<10} "), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{username:<10} "),
Decoration::Default,
false,
false,
false,
));
// TIME+.
if wide {
let times = p.get("cpu_times");
let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0);
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0);
let user = times
.and_then(|t| t.get("user"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let system = times
.and_then(|t| t.get("system"))
.and_then(Value::as_f64)
.unwrap_or(0.0);
let total = (user + system) as i64;
let hours = total / 3600;
let minutes = (total % 3600) / 60;
@@ -265,29 +414,62 @@ impl Plugin for ProcessListPlugin {
let (msg, decoration) = if hours > 99 {
(format!("{hours:<7}h"), Decoration::Default)
} else if hours > 0 {
(format!("{hours}h{minutes}:{seconds:02}"), Decoration::CpuTime)
(
format!("{hours}h{minutes}:{seconds:02}"),
Decoration::CpuTime,
)
} else {
(format!("{minutes}:{seconds:02}"), Decoration::Default)
};
ret.push(self.model.curse_add_line(format!("{msg:>8} "), decoration, true, false, false));
ret.push(self.model.curse_add_line(
format!("{msg:>8} "),
decoration,
true,
false,
false,
));
}
// THR.
if let Some(threads) = p.get("num_threads").and_then(Value::as_i64) {
ret.push(self.model.curse_add_line(format!("{threads:<3} "), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{threads:<3} "),
Decoration::Default,
false,
false,
false,
));
} else {
ret.push(self.model.curse_add_line(format!("{:<3} ", "?"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:<3} ", "?"),
Decoration::Default,
false,
false,
false,
));
}
// NI.
let nice_value = p.get("nice").and_then(Value::as_i64).unwrap_or(0);
let nice_decoration = self.nice_alert(nice_value);
ret.push(self.model.curse_add_line(format!("{nice_value:>3} "), nice_decoration, false, false, false));
ret.push(self.model.curse_add_line(
format!("{nice_value:>3} "),
nice_decoration,
false,
false,
false,
));
// Status.
let status = p.get("status").and_then(Value::as_str).unwrap_or("?");
let status_decoration = self.status_alert(status);
ret.push(self.model.curse_add_line(format!("{status:>1} "), status_decoration, false, false, false));
ret.push(self.model.curse_add_line(
format!("{status:>1} "),
status_decoration,
false,
false,
false,
));
// IO rates.
if wide {
@@ -301,12 +483,28 @@ impl Plugin for ProcessListPlugin {
let current = counters.get(slot)?.as_f64()?;
let old = counters.get(if slot == 0 { 2 } else { 3 })?.as_f64()?;
let io = (current - old) / time_since_update;
Some(if io == 0.0 { "0".to_string() } else { self.model.auto_unit(io, true, "K") })
Some(if io == 0.0 {
"0".to_string()
} else {
self.model.auto_unit(io, true, "K")
})
};
for (slot, width) in [(0usize, 4usize), (1, 4)] {
match rate(slot) {
Some(value) => ret.push(self.model.curse_add_line(format!("{value:>width$} "), Decoration::Default, true, true, false)),
None => ret.push(self.model.curse_add_line(format!("{:>width$} ", "?"), Decoration::Default, true, true, false)),
Some(value) => ret.push(self.model.curse_add_line(
format!("{value:>width$} "),
Decoration::Default,
true,
true,
false,
)),
None => ret.push(self.model.curse_add_line(
format!("{:>width$} ", "?"),
Decoration::Default,
true,
true,
false,
)),
}
}
}
@@ -318,7 +516,10 @@ impl Plugin for ProcessListPlugin {
Some(value) if value >= 0 => format!("{value:>3} "),
_ => format!("{:>3} ", "-"),
};
ret.push(self.model.curse_add_line(msg, Decoration::Default, true, false, false));
ret.push(
self.model
.curse_add_line(msg, Decoration::Default, true, false, false),
);
}
// Command.
@@ -326,9 +527,17 @@ impl Plugin for ProcessListPlugin {
let cmdline: Vec<String> = p
.get("cmdline")
.and_then(Value::as_array)
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.map(|a| {
a.iter()
.map(|v| v.as_str().unwrap_or("").to_string())
.collect()
})
.unwrap_or_default();
let process_decoration = if selected && !disable_cursor { Decoration::ProcessSelected } else { Decoration::Process };
let process_decoration = if selected && !disable_cursor {
Decoration::ProcessSelected
} else {
Decoration::Process
};
if !cmdline.is_empty() {
let first = &cmdline[0];
let (path, cmd) = if first.starts_with(name) {
@@ -341,17 +550,50 @@ impl Plugin for ProcessListPlugin {
};
let arguments = cmdline[1..].join(" ");
if !path.is_empty() && std::path::Path::new(&path).is_dir() && !short_name {
ret.push(self.model.curse_add_line(format!("{path}/"), Decoration::Default, false, false, true));
ret.push(self.model.curse_add_line(cmd, process_decoration, false, false, true));
ret.push(self.model.curse_add_line(
format!("{path}/"),
Decoration::Default,
false,
false,
true,
));
ret.push(self.model.curse_add_line(
cmd,
process_decoration,
false,
false,
true,
));
} else {
ret.push(self.model.curse_add_line(cmd, process_decoration, false, false, true));
ret.push(self.model.curse_add_line(
cmd,
process_decoration,
false,
false,
true,
));
}
if !arguments.is_empty() {
let msg = format!(" {}", glances_core::units::replace_special_chars(&arguments, ' '));
ret.push(self.model.curse_add_line(msg, Decoration::Default, false, false, true));
let msg = format!(
" {}",
glances_core::units::replace_special_chars(&arguments, ' ')
);
ret.push(self.model.curse_add_line(
msg,
Decoration::Default,
false,
false,
true,
));
}
} else {
ret.push(self.model.curse_add_line(name.to_string(), process_decoration, false, false, true));
ret.push(self.model.curse_add_line(
name.to_string(),
process_decoration,
false,
false,
true,
));
}
}
ret
@@ -367,10 +609,10 @@ impl ProcessListPlugin {
("nice_careful", glances_core::Decoration::Careful),
("nice_ok", glances_core::Decoration::Ok),
] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") {
if items.iter().any(|item| item == &value) {
return decoration;
}
if let Some(items) = self.model.get_limit_list(criticality, "processlist")
&& items.iter().any(|item| item == &value)
{
return decoration;
}
}
glances_core::Decoration::Default
@@ -383,13 +625,17 @@ impl ProcessListPlugin {
("status_careful", glances_core::Decoration::Careful),
("status_ok", glances_core::Decoration::Ok),
] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") {
if items.iter().any(|item| item == value) {
return decoration;
}
if let Some(items) = self.model.get_limit_list(criticality, "processlist")
&& items.iter().any(|item| item == value)
{
return decoration;
}
}
if value == "R" { glances_core::Decoration::Ok } else { glances_core::Decoration::Default }
if value == "R" {
glances_core::Decoration::Ok
} else {
glances_core::Decoration::Default
}
}
}
+139 -33
View File
@@ -51,13 +51,23 @@ impl QuicklookPlugin {
args.disable_config_exec,
);
let stats_list = model.get_conf_value("list", "");
let mut stats_list: Vec<String> =
if stats_list.is_empty() { DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect() } else { stats_list };
if stats_list.iter().any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str())) {
let mut stats_list: Vec<String> = if stats_list.is_empty() {
DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect()
} else {
stats_list
};
if stats_list
.iter()
.any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str()))
{
stats_list = DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect();
}
let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(0) as usize;
Self { model, stats_list, max_cpu_display }
Self {
model,
stats_list,
max_cpu_display,
}
}
fn mhz_to_hz(value: Option<f64>) -> Option<f64> {
@@ -88,7 +98,10 @@ impl Plugin for QuicklookPlugin {
(info, cpu, percpu)
};
stats.insert("cpu_name".to_string(), json!(info.cpu_name));
stats.insert("cpu_hz_current".to_string(), json!(Self::mhz_to_hz(info.cpu_hz_current)));
stats.insert(
"cpu_hz_current".to_string(),
json!(Self::mhz_to_hz(info.cpu_hz_current)),
);
stats.insert("cpu_hz".to_string(), json!(Self::mhz_to_hz(info.cpu_hz)));
stats.insert("cpu".to_string(), json!(cpu_value));
stats.insert("percpu".to_string(), Value::Array(percpu_values));
@@ -98,14 +111,19 @@ impl Plugin for QuicklookPlugin {
let available = meminfo.get("MemAvailable").copied().unwrap_or(0) as f64;
stats.insert(
"mem".to_string(),
json!(if total > 0.0 { ((total - available) / total * 100.0 * 10.0).round() / 10.0 } else { 0.0 }),
json!(if total > 0.0 {
((total - available) / total * 100.0 * 10.0).round() / 10.0
} else {
0.0
}),
);
let swap_total = meminfo.get("SwapTotal").copied().unwrap_or(0);
let swap_free = meminfo.get("SwapFree").copied().unwrap_or(0);
stats.insert(
"swap".to_string(),
json!(if swap_total > 0 {
(((swap_total - swap_free) as f64 * 100.0 / swap_total as f64) * 10.0).round() / 10.0
(((swap_total - swap_free) as f64 * 100.0 / swap_total as f64) * 10.0).round()
/ 10.0
} else {
0.0
}),
@@ -113,9 +131,16 @@ impl Plugin for QuicklookPlugin {
let log_core = crate::collectors::logical_core_count();
stats.insert("cpu_log_core".to_string(), json!(log_core));
stats.insert("cpu_phys_core".to_string(), json!(crate::collectors::physical_core_count()));
stats.insert(
"cpu_phys_core".to_string(),
json!(crate::collectors::physical_core_count()),
);
let load = crate::collectors::read_loadavg().map(|(_, _, min15)| {
if log_core > 0 { min15 / log_core as f64 * 100.0 } else { 0.0 }
if log_core > 0 {
min15 / log_core as f64 * 100.0
} else {
0.0
}
});
stats.insert("load".to_string(), json!(load));
stats.insert("gpu_mem".to_string(), Value::Null);
@@ -130,8 +155,16 @@ impl Plugin for QuicklookPlugin {
let stats_list = self.stats_list.clone();
for stat_key in &stats_list {
if let Some(value) = self.model.stats.get(stat_key).and_then(Value::as_f64) {
let decoration =
self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat_key), None, false);
let decoration = self.model.get_alert(
value,
0.0,
100.0,
true,
false,
Some(stat_key),
None,
false,
);
self.model.set_view_decoration(None, stat_key, decoration);
}
}
@@ -142,22 +175,36 @@ impl Plugin for QuicklookPlugin {
let mut percpu_decoration = serde_json::Map::new();
if let Some(percpu) = self.model.stats.get("percpu").and_then(Value::as_array) {
for cpu in percpu {
let number = cpu.get("cpu_number").map(glances_core::py_str).unwrap_or_default();
let number = cpu
.get("cpu_number")
.map(glances_core::py_str)
.unwrap_or_default();
let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0);
percpu_decoration.insert(number, json!(decoration_for_percent(&self.model, total).name()));
percpu_decoration.insert(
number,
json!(decoration_for_percent(&self.model, total).name()),
);
}
if percpu.len() > self.max_cpu_display && self.max_cpu_display > 0 {
let mut hidden: Vec<f64> =
percpu.iter().filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)).collect();
let mut hidden: Vec<f64> = percpu
.iter()
.filter_map(|cpu| cpu.get("total").and_then(Value::as_f64))
.collect();
hidden.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let hidden = &hidden[self.max_cpu_display.min(hidden.len())..];
if !hidden.is_empty() {
let mean = hidden.iter().sum::<f64>() / hidden.len() as f64;
percpu_decoration.insert("other".to_string(), json!(decoration_for_percent(&self.model, mean).name()));
percpu_decoration.insert(
"other".to_string(),
json!(decoration_for_percent(&self.model, mean).name()),
);
}
}
}
self.model.views.insert("percpu_decoration".to_string(), Value::Object(percpu_decoration));
self.model.views.insert(
"percpu_decoration".to_string(),
Value::Object(percpu_decoration),
);
}
fn display_curse(&self) -> bool {
@@ -171,26 +218,45 @@ impl Plugin for QuicklookPlugin {
return ret;
}
// System information: CPU name + frequency.
let freq = match (s.get("cpu_hz_current").and_then(Value::as_f64), s.get("cpu_hz").and_then(Value::as_f64)) {
let freq = match (
s.get("cpu_hz_current").and_then(Value::as_f64),
s.get("cpu_hz").and_then(Value::as_f64),
) {
(Some(current), Some(max)) => format!(" {:.2}/{:.2}GHz", current / 1e9, max / 1e9),
(Some(current), None) => format!(" {:.2}GHz", current / 1e9),
_ => String::new(),
};
let cpu_name = s.get("cpu_name").and_then(Value::as_str).unwrap_or("");
let name = if (ctx.max_width as i64 - freq.len() as i64 + 7) > 0 {
format!("{:<width$}", cpu_name, width = ctx.max_width - freq.len() + 7)
format!(
"{:<width$}",
cpu_name,
width = ctx.max_width - freq.len() + 7
)
} else if freq.is_empty() {
String::new()
} else {
"Frequency".to_string()
};
ret.push(self.model.curse_add_line(name.clone(), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(freq.clone(), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
name.clone(),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
freq.clone(),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_new_line());
let bar_size = std::cmp::max(
name.len().saturating_add(freq.len()).saturating_sub(7),
(ctx.max_width as usize).saturating_sub(9),
ctx.max_width.saturating_sub(9),
)
.max(10);
let bar_char = self
@@ -201,14 +267,32 @@ impl Plugin for QuicklookPlugin {
.unwrap_or_else(|| "|".to_string());
let mut lines: Vec<Vec<glances_core::StatLine>> = Vec::new();
let mut push_line = |msg: String, bar_key: &str, percent: f64, decoration: glances_core::Decoration| {
let mut push_line = |msg: String,
bar_key: &str,
percent: f64,
decoration: glances_core::Decoration| {
let bar = glances_core::units::bar_percent(percent, bar_size, &bar_char, true);
lines.push(vec![
self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false),
self.model.curse_add_line("[", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(bar, decoration, false, false, false),
self.model.curse_add_line("]", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(" ", glances_core::Decoration::Default, false, false, false),
self.model.curse_add_line(
msg,
glances_core::Decoration::Default,
false,
false,
false,
),
self.model
.curse_add_line("[", glances_core::Decoration::Bold, false, false, false),
self.model
.curse_add_line(bar, decoration, false, false, false),
self.model
.curse_add_line("]", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(
" ",
glances_core::Decoration::Default,
false,
false,
false,
),
]);
let _ = bar_key;
};
@@ -230,7 +314,11 @@ impl Plugin for QuicklookPlugin {
for cpu in sorted.iter().take(self.max_cpu_display) {
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0);
let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0);
let msg = if cpu_id < 10 { format!("{:<3}{cpu_id} ", key.to_uppercase()) } else { format!("{cpu_id:4} ") };
let msg = if cpu_id < 10 {
format!("{:<3}{cpu_id} ", key.to_uppercase())
} else {
format!("{cpu_id:4} ")
};
let decoration = self.decoration_for_percpu(cpu_id);
push_line(msg, key, total, decoration);
}
@@ -240,14 +328,28 @@ impl Plugin for QuicklookPlugin {
.skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get("total").and_then(Value::as_f64))
.collect();
let mean = if hidden.is_empty() { 0.0 } else { hidden.iter().sum::<f64>() / hidden.len() as f64 };
let mean = if hidden.is_empty() {
0.0
} else {
hidden.iter().sum::<f64>() / hidden.len() as f64
};
let decoration = self.decoration_for_percpu(-1);
push_line(format!("{:<3}* ", key.to_uppercase()), key, mean, decoration);
push_line(
format!("{:<3}* ", key.to_uppercase()),
key,
mean,
decoration,
);
}
}
} else if let Some(value) = s.get(key).and_then(Value::as_f64) {
let decoration = self.model.view_decoration(None, key);
push_line(format!("{:<4} ", key.to_uppercase()), key, value, decoration);
push_line(
format!("{:<4} ", key.to_uppercase()),
key,
value,
decoration,
);
}
}
for line in lines {
@@ -261,7 +363,11 @@ impl Plugin for QuicklookPlugin {
impl QuicklookPlugin {
fn decoration_for_percpu(&self, cpu_id: i64) -> glances_core::Decoration {
let key = if cpu_id < 0 { "other".to_string() } else { cpu_id.to_string() };
let key = if cpu_id < 0 {
"other".to_string()
} else {
cpu_id.to_string()
};
match self.model.get_view(None, "percpu_decoration", &key) {
Value::String(name) => glances_core::Decoration::from_name(&name),
_ => glances_core::Decoration::Default,
+72 -35
View File
@@ -64,16 +64,18 @@ impl Plugin for RaidPlugin {
let status = fields[2].to_string();
let raid_type = fields
.iter()
.find(|f| f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath"))
.find(|f| {
f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath")
})
.map(|f| f.to_string())
.unwrap_or_else(|| "unknown".to_string());
// Devices (name[index]).
let mut components = serde_json::Map::new();
for field in fields.iter().skip(3) {
if let Some(device) = field.split('[').next() {
if !device.is_empty() {
components.insert(device.to_string(), json!("active sync"));
}
if let Some(device) = field.split('[').next()
&& !device.is_empty()
{
components.insert(device.to_string(), json!("active sync"));
}
}
// The following line contains blocks and [used/available] [UU].
@@ -81,30 +83,30 @@ impl Plugin for RaidPlugin {
let mut available = components.len();
if index + 1 < lines.len() {
let detail = lines[index + 1];
if let Some(start) = detail.rfind('[') {
if let Some(end) = detail[start..].find(']') {
let counts = &detail[start + 1..start + end];
if let Some((u, a)) = counts.split_once('/') {
used = u.parse().unwrap_or(used as i64) as usize;
available = a.parse().unwrap_or(available as i64) as usize;
}
if let Some(start) = detail.rfind('[')
&& let Some(end) = detail[start..].find(']')
{
let counts = &detail[start + 1..start + end];
if let Some((u, a)) = counts.split_once('/') {
used = u.parse().unwrap_or(used as i64) as usize;
available = a.parse().unwrap_or(available as i64) as usize;
}
}
if let Some(start) = detail.rfind('[') {
if let Some(end) = detail[start..].find(']') {
let flags = &detail[start + 1..start + end];
if flags.len() == components.len() {
let names: Vec<String> = components.keys().cloned().collect();
for (position, flag) in flags.chars().enumerate() {
if let Some(device) = names.get(position) {
let status = match flag {
'U' => "active sync",
'_' => "faulty",
'S' => "spare",
_ => "unknown",
};
components.insert(device.clone(), json!(status));
}
if let Some(start) = detail.rfind('[')
&& let Some(end) = detail[start..].find(']')
{
let flags = &detail[start + 1..start + end];
if flags.len() == components.len() {
let names: Vec<String> = components.keys().cloned().collect();
for (position, flag) in flags.chars().enumerate() {
if let Some(device) = names.get(position) {
let status = match flag {
'U' => "active sync",
'_' => "faulty",
'S' => "spare",
_ => "unknown",
};
components.insert(device.clone(), json!(status));
}
}
}
@@ -146,8 +148,20 @@ impl Plugin for RaidPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>7}", "Used"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "Avail"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Used"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Avail"),
glances_core::Decoration::Default,
false,
false,
false,
));
let mut names: Vec<&String> = arrays.keys().collect();
names.sort();
for name in names {
@@ -157,25 +171,48 @@ impl Plugin for RaidPlugin {
let status = array.get("status").and_then(Value::as_str).unwrap_or("");
let used = array.get("used").and_then(Value::as_i64).unwrap_or(0);
let available = array.get("available").and_then(Value::as_i64).unwrap_or(0);
let array_type = array.get("type").and_then(Value::as_str).unwrap_or("unknown");
let array_type = array
.get("type")
.and_then(Value::as_str)
.unwrap_or("unknown");
let decoration = raid_alert(status, used, available, array_type);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", name.chars().take(name_max_width).collect::<String>(), width = name_max_width),
format!(
"{:<width$}",
name.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
decoration,
false,
false,
false,
));
ret.push(self.model.curse_add_line(format!("{used:>7}"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{available:>7}"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{used:>7}"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{available:>7}"),
glances_core::Decoration::Default,
false,
false,
false,
));
}
ret
}
}
fn raid_alert(status: &str, used: i64, available: i64, array_type: &str) -> glances_core::Decoration {
fn raid_alert(
status: &str,
used: i64,
available: i64,
array_type: &str,
) -> glances_core::Decoration {
if status != "active" || used < available {
return glances_core::Decoration::Critical;
}
+108 -40
View File
@@ -49,27 +49,36 @@ fn read_hwmon() -> Vec<Value> {
let name = file.file_name().to_string_lossy().to_string();
let path = file.path();
// Temperatures (millidegrees).
if let Some(rest) = name.strip_prefix("temp") {
if let Some(rest) = rest.strip_suffix("_input") {
let Some(raw) = read_value(&path) else {
continue;
};
let value = raw / 1000.0;
let label = read_label(&dir, &format!("temp{rest}_label"), &format!("temp{rest}"));
let warning = read_label_value(&dir, &format!("temp{rest}_max")).map(|v| v / 1000.0);
let critical = read_label_value(&dir, &format!("temp{rest}_crit")).map(|v| v / 1000.0);
stats.push(sensor_item(&label, "C", value, warning, critical, "temperature_core"));
}
if let Some(rest) = name.strip_prefix("temp")
&& let Some(rest) = rest.strip_suffix("_input")
{
let Some(raw) = read_value(&path) else {
continue;
};
let value = raw / 1000.0;
let label = read_label(&dir, &format!("temp{rest}_label"), &format!("temp{rest}"));
let warning =
read_label_value(&dir, &format!("temp{rest}_max")).map(|v| v / 1000.0);
let critical =
read_label_value(&dir, &format!("temp{rest}_crit")).map(|v| v / 1000.0);
stats.push(sensor_item(
&label,
"C",
value,
warning,
critical,
"temperature_core",
));
}
// Fans (RPM).
if let Some(rest) = name.strip_prefix("fan") {
if let Some(rest) = rest.strip_suffix("_input") {
let Some(value) = read_value(&path) else {
continue;
};
let label = read_label(&dir, &format!("fan{rest}_label"), &format!("fan{rest}"));
stats.push(sensor_item(&label, "R", value, None, None, "fan_speed"));
}
if let Some(rest) = name.strip_prefix("fan")
&& let Some(rest) = rest.strip_suffix("_input")
{
let Some(value) = read_value(&path) else {
continue;
};
let label = read_label(&dir, &format!("fan{rest}_label"), &format!("fan{rest}"));
stats.push(sensor_item(&label, "R", value, None, None, "fan_speed"));
}
}
}
@@ -88,7 +97,9 @@ fn read_batteries() -> Vec<Value> {
let dir = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
let is_battery = name.starts_with("BAT")
|| read_string(&dir.join("type")).map(|t| t == "Battery").unwrap_or(false);
|| read_string(&dir.join("type"))
.map(|t| t == "Battery")
.unwrap_or(false);
if !is_battery {
continue;
}
@@ -97,14 +108,23 @@ fn read_batteries() -> Vec<Value> {
};
let mut item = sensor_item("Battery", "%", value, None, None, "battery");
if let Some(status) = read_string(&dir.join("status")) {
item.as_object_mut().unwrap().insert("status".to_string(), json!(status));
item.as_object_mut()
.unwrap()
.insert("status".to_string(), json!(status));
}
stats.push(item);
}
stats
}
fn sensor_item(label: &str, unit: &str, value: f64, warning: Option<f64>, critical: Option<f64>, r#type: &str) -> Value {
fn sensor_item(
label: &str,
unit: &str,
value: f64,
warning: Option<f64>,
critical: Option<f64>,
r#type: &str,
) -> Value {
json!({
"label": label,
"unit": unit,
@@ -117,7 +137,9 @@ fn sensor_item(label: &str, unit: &str, value: f64, warning: Option<f64>, critic
}
fn read_string(path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok().map(|v| v.trim().to_string())
std::fs::read_to_string(path)
.ok()
.map(|v| v.trim().to_string())
}
fn read_value(path: &std::path::Path) -> Option<f64> {
@@ -154,23 +176,40 @@ impl Plugin for SensorsPlugin {
stats.extend(read_batteries());
// Apply show/hide filters and aliases.
stats.retain(|stat| {
let label = stat.get("label").and_then(Value::as_str).unwrap_or("").to_lowercase();
let label = stat
.get("label")
.and_then(Value::as_str)
.unwrap_or("")
.to_lowercase();
self.model.is_display(&label, "")
});
for stat in stats.iter_mut() {
if let (Some(label), Some(r#type)) = (
stat.get("label").and_then(Value::as_str).map(|s| s.to_lowercase()),
stat.get("type").and_then(Value::as_str).map(|s| s.to_lowercase()),
stat.get("label")
.and_then(Value::as_str)
.map(|s| s.to_lowercase()),
stat.get("type")
.and_then(Value::as_str)
.map(|s| s.to_lowercase()),
) {
if let Some(alias) = self.model.has_alias(&format!("{}_{}", label, r#type)) {
stat.as_object_mut().unwrap().insert("label".to_string(), json!(alias));
stat.as_object_mut()
.unwrap()
.insert("label".to_string(), json!(alias));
} else if let Some(alias) = self.model.has_alias(&label) {
stat.as_object_mut().unwrap().insert("label".to_string(), json!(alias));
stat.as_object_mut()
.unwrap()
.insert("label".to_string(), json!(alias));
}
}
}
// Sort by label.
stats.sort_by_key(|stat| stat.get("label").and_then(Value::as_str).unwrap_or("").to_lowercase());
stats.sort_by_key(|stat| {
stat.get("label")
.and_then(Value::as_str)
.unwrap_or("")
.to_lowercase()
});
Ok(Value::Array(stats))
}
@@ -196,14 +235,34 @@ impl Plugin for SensorsPlugin {
if warning.is_some() || critical.is_some() {
system_thresholds(value, warning, critical)
} else {
self.model.get_alert(value, 0.0, 100.0, true, false, Some(&label), Some(&r#type), false)
self.model.get_alert(
value,
0.0,
100.0,
true,
false,
Some(&label),
Some(&r#type),
false,
)
}
} else if r#type == "battery" {
self.model.get_alert(100.0 - value, 0.0, 100.0, true, false, Some(&r#type), None, false)
self.model.get_alert(
100.0 - value,
0.0,
100.0,
true,
false,
Some(&r#type),
None,
false,
)
} else {
self.model.get_alert(value, 0.0, 100.0, true, false, Some(&r#type), None, false)
self.model
.get_alert(value, 0.0, 100.0, true, false, Some(&r#type), None, false)
};
self.model.set_view_decoration(Some(&label), "value", decoration);
self.model
.set_view_decoration(Some(&label), "value", decoration);
}
}
@@ -228,14 +287,22 @@ impl Plugin for SensorsPlugin {
false,
));
for i in items {
let item_key = i.get("label").and_then(Value::as_str).unwrap_or("").to_string();
let item_key = i
.get("label")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let label = i.get("label").and_then(Value::as_str).unwrap_or("");
let sensor_type = i.get("type").and_then(Value::as_str).unwrap_or("");
let unit = i.get("unit").and_then(Value::as_str).unwrap_or("");
let value = i.get("value").and_then(Value::as_f64);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", label.chars().take(name_max_width).collect::<String>(), width = name_max_width),
format!(
"{:<width$}",
label.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
@@ -244,11 +311,12 @@ impl Plugin for SensorsPlugin {
let Some(value) = value else {
continue;
};
let (value, unit) = if ctx.args.fahrenheit && sensor_type != "battery" && sensor_type != "fan_speed" {
(glances_core::to_fahrenheit(value), "F")
} else {
(value, unit)
};
let (value, unit) =
if ctx.args.fahrenheit && sensor_type != "battery" && sensor_type != "fan_speed" {
(glances_core::to_fahrenheit(value), "F")
} else {
(value, unit)
};
ret.push(self.model.curse_add_line(
format!("{:>14}", format!("{value:.0}{unit}")),
self.model.view_decoration(Some(&item_key), "value"),
+14 -2
View File
@@ -85,9 +85,21 @@ impl Plugin for SystemPlugin {
return ret;
}
let hostname = s.get("hostname").and_then(Value::as_str).unwrap_or("");
ret.push(self.model.curse_add_line(hostname, glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
hostname,
glances_core::Decoration::Title,
false,
false,
false,
));
let hr_name = s.get("hr_name").and_then(Value::as_str).unwrap_or("");
ret.push(self.model.curse_add_line(format!(" {hr_name} "), glances_core::Decoration::Default, true, false, false));
ret.push(self.model.curse_add_line(
format!(" {hr_name} "),
glances_core::Decoration::Default,
true,
false,
false,
));
ret
}
}
+34 -7
View File
@@ -63,8 +63,14 @@ impl Plugin for WifiPlugin {
continue;
}
let ssid = fields[0].trim_end_matches(':').to_string();
let quality_link = fields[2].trim_end_matches('.').parse::<f64>().unwrap_or(0.0);
let quality_level = fields[3].trim_end_matches('.').parse::<f64>().unwrap_or(0.0);
let quality_link = fields[2]
.trim_end_matches('.')
.parse::<f64>()
.unwrap_or(0.0);
let quality_level = fields[3]
.trim_end_matches('.')
.parse::<f64>()
.unwrap_or(0.0);
stats.push(json!({
"key": "ssid",
"ssid": ssid,
@@ -105,7 +111,8 @@ impl Plugin for WifiPlugin {
} else {
Decoration::Ok
};
self.model.set_view_decoration(Some(&ssid), "quality_level", decoration);
self.model
.set_view_decoration(Some(&ssid), "quality_level", decoration);
}
}
@@ -115,7 +122,14 @@ impl Plugin for WifiPlugin {
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
if self
.model
.stats
.as_array()
.map(|a| a.is_empty())
.unwrap_or(true)
|| ctx.max_width == 0
{
return ret;
}
let if_name_max_width = ctx.max_width.saturating_sub(5);
@@ -126,16 +140,29 @@ impl Plugin for WifiPlugin {
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>7}", "dBm"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>7}", "dBm"),
glances_core::Decoration::Default,
false,
false,
false,
));
let Some(items) = self.model.stats.as_array() else {
return ret;
};
for i in items {
let ssid = i.get("ssid").and_then(Value::as_str).unwrap_or("");
let level = i.get("quality_level").and_then(Value::as_f64).unwrap_or(0.0);
let level = i
.get("quality_level")
.and_then(Value::as_f64)
.unwrap_or(0.0);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", ssid.chars().take(if_name_max_width).collect::<String>(), width = if_name_max_width),
format!(
"{:<width$}",
ssid.chars().take(if_name_max_width).collect::<String>(),
width = if_name_max_width
),
glances_core::Decoration::Default,
false,
false,
+288 -117
View File
@@ -3,7 +3,9 @@
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use glances_core::plugin::{Align, Decoration, StatLine};
use glances_core::processes::processes;
use glances_core::{Args, RenderContext};
@@ -18,17 +20,7 @@ use std::io::Stdout;
use std::time::{Duration, Instant};
const LEFT_SIDEBAR: [&str; 11] = [
"network",
"ports",
"wifi",
"diskio",
"fs",
"irq",
"folders",
"raid",
"sensors",
"ip",
"now",
"network", "ports", "wifi", "diskio", "fs", "irq", "folders", "raid", "sensors", "ip", "now",
];
const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"];
@@ -54,7 +46,11 @@ pub fn run(engine: &mut Engine) -> Result<()> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = ratatui::Terminal::new(backend)?;
let mut tui = Tui { popup: Popup::None, input: String::new(), quit: false };
let mut tui = Tui {
popup: Popup::None,
input: String::new(),
quit: false,
};
let result = run_loop(&mut terminal, engine, &mut tui);
disable_raw_mode()?;
@@ -76,10 +72,10 @@ fn run_loop(
tui.draw(terminal, engine)?;
count += 1;
if let Some(max) = engine.args.stop_after {
if count >= max {
break;
}
if let Some(max) = engine.args.stop_after
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
@@ -101,7 +97,11 @@ fn run_loop(
}
impl Tui {
fn draw(&self, terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>, engine: &mut Engine) -> Result<()> {
fn draw(
&self,
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
engine: &mut Engine,
) -> Result<()> {
terminal.draw(|frame| draw_frame(frame, engine, self))?;
Ok(())
}
@@ -122,14 +122,39 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
// Header.
let header_height = 1u16;
let top_height = tui.top_height(engine, args, area.width);
let footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill { 3 } else { 0 };
let main_height = area.height.saturating_sub(header_height + top_height + footer_height);
let footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill {
3
} else {
0
};
let main_height = area
.height
.saturating_sub(header_height + top_height + footer_height);
let header_area = Rect { x: 0, y: 0, width: area.width, height: header_height };
let top_area = Rect { x: 0, y: header_height, width: area.width, height: top_height };
let main_area = Rect { x: 0, y: header_height + top_height, width: area.width, height: main_height };
let footer_area =
Rect { x: 0, y: area.height.saturating_sub(footer_height), width: area.width, height: footer_height };
let header_area = Rect {
x: 0,
y: 0,
width: area.width,
height: header_height,
};
let top_area = Rect {
x: 0,
y: header_height,
width: area.width,
height: top_height,
};
let main_area = Rect {
x: 0,
y: header_height + top_height,
width: area.width,
height: main_height,
};
let footer_area = Rect {
x: 0,
y: area.height.saturating_sub(footer_height),
width: area.width,
height: footer_height,
};
tui.draw_header(frame, header_area, engine, args);
tui.draw_top(frame, top_area, engine, args);
@@ -140,7 +165,6 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
}
impl Tui {
fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 {
if args.disable_top {
return 1;
@@ -157,53 +181,67 @@ impl Tui {
}
}
// Quicklook.
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
if plugin.is_enabled() && !args.disable_quicklook {
let ql_width = quicklook_width(width);
let ctx = render_ctx(args, &engine.runtime, ql_width as usize);
let lines = split_lines(plugin.render(&ctx));
height = height.max(lines.len());
}
if let Some(plugin) = engine.plugins.get_mut("quicklook")
&& plugin.is_enabled()
&& !args.disable_quicklook
{
let ql_width = quicklook_width(width);
let ctx = render_ctx(args, &engine.runtime, ql_width as usize);
let lines = split_lines(plugin.render(&ctx));
height = height.max(lines.len());
}
(height.min(20) as u16).max(1)
}
fn draw_header(
&self,
frame: &mut Frame,
area: Rect,
engine: &mut Engine,
args: &Args,
) {
fn draw_header(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
let x = area.x;
// System information.
if let Some(plugin) = engine.plugins.get_mut("system") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
let style = lines
.first()
.and_then(|l| l.first())
.map(|s| style_for(s.decoration, args))
.unwrap_or_default();
let width = text.chars().count() as u16;
frame.render_widget(Paragraph::new(text).style(style), Rect { x, y: area.y, width: width.min(area.width), height: 1 });
}
if let Some(plugin) = engine.plugins.get_mut("system")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines
.first()
.map(|l| l.iter().map(|s| s.msg.clone()).collect())
.unwrap_or_default();
let style = lines
.first()
.and_then(|l| l.first())
.map(|s| style_for(s.decoration, args))
.unwrap_or_default();
let width = text.chars().count() as u16;
frame.render_widget(
Paragraph::new(text).style(style),
Rect {
x,
y: area.y,
width: width.min(area.width),
height: 1,
},
);
}
// Uptime on the right.
if let Some(plugin) = engine.plugins.get_mut("uptime") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
let width = text.chars().count() as u16;
if width + 2 < area.width {
frame.render_widget(
Paragraph::new(text),
Rect { x: area.width.saturating_sub(width), y: area.y, width, height: 1 },
);
}
if let Some(plugin) = engine.plugins.get_mut("uptime")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines
.first()
.map(|l| l.iter().map(|s| s.msg.clone()).collect())
.unwrap_or_default();
let width = text.chars().count() as u16;
if width + 2 < area.width {
frame.render_widget(
Paragraph::new(text),
Rect {
x: area.width.saturating_sub(width),
y: area.y,
width,
height: 1,
},
);
}
}
}
@@ -212,22 +250,27 @@ impl Tui {
let mut x = area.x;
// Collect the rendered blocks with their natural widths.
struct Block {
name: String,
lines: Vec<Vec<StatLine>>,
natural_width: usize,
}
let mut blocks: Vec<Block> = Vec::new();
if !args.disable_quicklook {
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
if plugin.is_enabled() {
let width = quicklook_width(area.width) as usize;
let ctx = render_ctx(args, &engine.runtime, width);
let lines = split_lines(plugin.render(&ctx));
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
blocks.push(Block { name: "quicklook".to_string(), lines, natural_width: natural.max(10) });
}
}
if !args.disable_quicklook
&& let Some(plugin) = engine.plugins.get_mut("quicklook")
&& plugin.is_enabled()
{
let width = quicklook_width(area.width) as usize;
let ctx = render_ctx(args, &engine.runtime, width);
let lines = split_lines(plugin.render(&ctx));
let natural = lines
.iter()
.map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
}
for name in TOP_PLUGINS {
if let Some(plugin) = engine.plugins.get_mut(name) {
@@ -236,8 +279,15 @@ impl Tui {
}
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
blocks.push(Block { name: name.to_string(), lines, natural_width: natural.max(10) });
let natural = lines
.iter()
.map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
}
}
if blocks.is_empty() {
@@ -247,7 +297,9 @@ impl Tui {
// Allocate the available width proportionally to the natural widths.
let total_natural: usize = blocks.iter().map(|b| b.natural_width).sum();
let spacing = 2usize * blocks.len().saturating_sub(1);
let available = (area.width as usize).saturating_sub(spacing).max(blocks.len() * 8);
let available = (area.width as usize)
.saturating_sub(spacing)
.max(blocks.len() * 8);
let mut widths: Vec<u16> = Vec::new();
for block in &blocks {
let mut width = (available * block.natural_width / total_natural).max(8);
@@ -267,7 +319,17 @@ impl Tui {
if x + width > area.x + area.width {
break;
}
draw_lines(frame, Rect { x, y: area.y, width, height: area.height }, &block.lines, args);
draw_lines(
frame,
Rect {
x,
y: area.y,
width,
height: area.height,
},
&block.lines,
args,
);
x += width + 2;
}
}
@@ -304,7 +366,17 @@ impl Tui {
break;
}
let height = height.min(area.y + area.height - y);
draw_lines(frame, Rect { x: area.x, y, width: left_width, height }, &lines, args);
draw_lines(
frame,
Rect {
x: area.x,
y,
width: left_width,
height,
},
&lines,
args,
);
y += height;
}
}
@@ -318,14 +390,24 @@ impl Tui {
let mut y = area.y;
// Process count.
if let Some(plugin) = engine.plugins.get_mut("processcount") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let lines = split_lines(plugin.render(&ctx));
let height = (lines.len() as u16).min(2);
draw_lines(frame, Rect { x: right_x, y, width: right_width, height }, &lines, args);
y += height;
}
if let Some(plugin) = engine.plugins.get_mut("processcount")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let lines = split_lines(plugin.render(&ctx));
let height = (lines.len() as u16).min(2);
draw_lines(
frame,
Rect {
x: right_x,
y,
width: right_width,
height,
},
&lines,
args,
);
y += height;
}
// Alerts footer (up to 4 lines).
@@ -334,20 +416,40 @@ impl Tui {
let list_height = area.y + area.height - y;
let process_height = list_height.saturating_sub(alert_height);
let list_name = if args.programs { "programlist" } else { "processlist" };
if let Some(plugin) = engine.plugins.get_mut(list_name) {
if plugin.is_enabled() && process_height > 1 {
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let mut lines = split_lines(plugin.render(&ctx));
lines.truncate(process_height as usize);
draw_lines(frame, Rect { x: right_x, y, width: right_width, height: process_height }, &lines, args);
}
let list_name = if args.programs {
"programlist"
} else {
"processlist"
};
if let Some(plugin) = engine.plugins.get_mut(list_name)
&& plugin.is_enabled()
&& process_height > 1
{
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let mut lines = split_lines(plugin.render(&ctx));
lines.truncate(process_height as usize);
draw_lines(
frame,
Rect {
x: right_x,
y,
width: right_width,
height: process_height,
},
&lines,
args,
);
}
y += process_height;
if alert_height > 0 {
draw_lines(
frame,
Rect { x: right_x, y, width: right_width, height: alert_height },
Rect {
x: right_x,
y,
width: right_width,
height: alert_height,
},
&event_lines,
args,
);
@@ -360,14 +462,20 @@ impl Tui {
if events.is_empty() {
return lines;
}
lines.push(vec![StatLine::new("EVENTS HISTORY").decoration(Decoration::Title)]);
lines.push(vec![
StatLine::new("EVENTS HISTORY").decoration(Decoration::Title),
]);
for event in events.events.iter().rev().take(4) {
let state = match event.state.as_str() {
"CRITICAL" => Decoration::CriticalLog,
"WARNING" => Decoration::WarningLog,
_ => Decoration::Default,
};
let end = if event.is_ongoing() { String::new() } else { " (ended)".to_string() };
let end = if event.is_ongoing() {
String::new()
} else {
" (ended)".to_string()
};
lines.push(vec![
StatLine::new(format!("{} ", event.state)).decoration(state),
StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)),
@@ -401,7 +509,9 @@ impl Tui {
Line::from("4 Full quicklook"),
Line::from("5 Disable top menu"),
Line::from("6 Mean GPU mode"),
Line::from("a c m i o p t u Sort processes (auto/cpu/mem/io/cpu_time/name/time/user)"),
Line::from(
"a c m i o p t u Sort processes (auto/cpu/mem/io/cpu_time/name/time/user)",
),
Line::from("</> Cycle sort key"),
Line::from("UP/DOWN Select process"),
Line::from("+ / - Increase/decrease nice level"),
@@ -420,7 +530,12 @@ impl Tui {
];
let block = Block::default().borders(Borders::ALL).title("Help");
frame.render_widget(Clear, area);
frame.render_widget(Paragraph::new(lines).block(block).wrap(Wrap { trim: false }), area);
frame.render_widget(
Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false }),
area,
);
}
fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) {
@@ -435,7 +550,11 @@ impl Tui {
KeyCode::Enter => {
let filter = self.input.clone();
let mut procs = processes().lock().unwrap();
procs.filter.set_filter(if filter.is_empty() { None } else { Some(&filter) });
procs.filter.set_filter(if filter.is_empty() {
None
} else {
Some(&filter)
});
self.popup = Popup::None;
}
KeyCode::Esc => self.popup = Popup::None,
@@ -485,7 +604,9 @@ impl Tui {
args.meangpu = !args.meangpu;
engine.runtime.meangpu = args.meangpu;
}
KeyCode::Char('/') => engine.runtime.process_short_name = !engine.runtime.process_short_name,
KeyCode::Char('/') => {
engine.runtime.process_short_name = !engine.runtime.process_short_name
}
KeyCode::Char('a') => set_sort(engine, "auto"),
KeyCode::Char('c') => set_sort(engine, "cpu_percent"),
KeyCode::Char('m') => set_sort(engine, "memory_percent"),
@@ -502,7 +623,13 @@ impl Tui {
KeyCode::Char('-') => nice_change(engine, false),
KeyCode::Char('k') => self.popup = Popup::Kill,
KeyCode::Char('f') => {
self.input = processes().lock().unwrap().filter.filter_input().unwrap_or("").to_string();
self.input = processes()
.lock()
.unwrap()
.filter
.filter_input()
.unwrap_or("")
.to_string();
self.popup = Popup::Filter;
}
KeyCode::Char('E') => processes().lock().unwrap().filter.set_filter(None),
@@ -521,12 +648,16 @@ impl Tui {
KeyCode::Char('L') => args.diskio_latency = !args.diskio_latency,
KeyCode::Char('F') => engine.runtime.fs_free_space = !engine.runtime.fs_free_space,
KeyCode::Char('S') => engine.runtime.sparkline = !engine.runtime.sparkline,
KeyCode::Char('M') => engine.runtime.reset_minmax_tag = !engine.runtime.reset_minmax_tag,
KeyCode::Char('M') => {
engine.runtime.reset_minmax_tag = !engine.runtime.reset_minmax_tag
}
KeyCode::Char('j') => {
args.programs = !args.programs;
engine.runtime.programs = args.programs;
}
KeyCode::Char(c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R')) => {
KeyCode::Char(
c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R'),
) => {
toggle_plugin_by_key(engine, c);
}
_ => {}
@@ -534,8 +665,16 @@ impl Tui {
}
}
fn render_ctx<'a>(args: &'a Args, runtime: &'a glances_core::Runtime, max_width: usize) -> RenderContext<'a> {
RenderContext { args, runtime, max_width }
fn render_ctx<'a>(
args: &'a Args,
runtime: &'a glances_core::Runtime,
max_width: usize,
) -> RenderContext<'a> {
RenderContext {
args,
runtime,
max_width,
}
}
fn quicklook_width(width: u16) -> u16 {
@@ -587,7 +726,11 @@ fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec<StatLine>], args: &Arg
}
fn style_for(decoration: Decoration, args: &Args) -> Style {
let bold = if args.disable_bold { Modifier::empty() } else { Modifier::BOLD };
let bold = if args.disable_bold {
Modifier::empty()
} else {
Modifier::BOLD
};
let bg = args.disable_bg;
match decoration {
Decoration::Default | Decoration::Password => Style::default(),
@@ -596,7 +739,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style {
Style::default().add_modifier(bold | Modifier::UNDERLINED)
}
Decoration::Title => Style::default().add_modifier(bold),
Decoration::Ok | Decoration::Process | Decoration::Status => Style::default().fg(Color::Green),
Decoration::Ok | Decoration::Process | Decoration::Status => {
Style::default().fg(Color::Green)
}
Decoration::Max => Style::default().fg(Color::Green).add_modifier(bold),
Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue),
Decoration::CarefulLog => {
@@ -630,7 +775,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style {
}
}
Decoration::CpuTime => Style::default().fg(Color::Magenta),
Decoration::ProcessSelected => Style::default().fg(Color::Green).add_modifier(bold | Modifier::UNDERLINED),
Decoration::ProcessSelected => Style::default()
.fg(Color::Green)
.add_modifier(bold | Modifier::UNDERLINED),
Decoration::Error => Style::default().fg(Color::Red),
Decoration::Separator => Style::default().fg(Color::DarkGray),
}
@@ -643,7 +790,15 @@ fn set_sort(_engine: &mut Engine, key: &str) {
}
fn cycle_sort(engine: &mut Engine, forward: bool) {
const KEYS: [&str; 7] = ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
const KEYS: [&str; 7] = [
"cpu_percent",
"memory_percent",
"username",
"cpu_times",
"io_counters",
"name",
"cpu_num",
];
let current = processes().lock().unwrap().sort_key.clone();
let index = KEYS.iter().position(|k| *k == current).unwrap_or(0);
let next = if forward {
@@ -680,7 +835,11 @@ fn nice_change(engine: &mut Engine, increase: bool) {
}
fn toggle_processes(engine: &mut Engine) {
let enabled = engine.plugins.get("processlist").map(|p| p.is_enabled()).unwrap_or(false);
let enabled = engine
.plugins
.get("processlist")
.map(|p| p.is_enabled())
.unwrap_or(false);
for name in ["processcount", "processlist", "programlist"] {
if let Some(plugin) = engine.plugins.get_mut(name) {
plugin.set_enabled(!enabled);
@@ -752,7 +911,13 @@ mod tests {
fn test_tui_layout_renders() {
use clap::Parser;
use glances_core::Config;
let mut args = Args::parse_from(["glances", "--disable-plugin", "all", "--enable-plugin", "cpu,mem,load,processlist,processcount,network,fs,diskio,now,system,uptime,quicklook"]);
let mut args = Args::parse_from([
"glances",
"--disable-plugin",
"all",
"--enable-plugin",
"cpu,mem,load,processlist,processcount,network,fs,diskio,now,system,uptime,quicklook",
]);
args.time = Some(1.0);
let config = Config::empty(false);
let mut engine = Engine::new(args.clone(), config);
@@ -760,8 +925,14 @@ mod tests {
engine.update();
let backend = TestBackend::new(140, 40);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
let tui = Tui { popup: Popup::None, input: String::new(), quit: false };
terminal.draw(|frame| draw_frame(frame, &mut engine, &tui)).unwrap();
let tui = Tui {
popup: Popup::None,
input: String::new(),
quit: false,
};
terminal
.draw(|frame| draw_frame(frame, &mut engine, &tui))
.unwrap();
let text = buffer_to_string(terminal.backend().buffer());
println!("{text}");
assert!(text.contains("CPU"));