M1: core engine, config, plugin model and core plugins

- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI
- Python-compatible config file handling, limits, views, rates, MMM, history
- process collection via sysinfo + procfs, filters, thresholds, events, actions
- 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...)
- stdout, stdout-csv, stdout-json, --issue, --modules-list output modes
- differential test harness against the Python implementation
This commit is contained in:
glances-rs
2026-10-09 23:25:35 +00:00
commit 975385da5d
61 changed files with 12049 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
name: CI
on:
push:
branches: [master]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
+4
View File
@@ -0,0 +1,4 @@
/target
**/*.rs.bk
*.log
.DS_Store
Generated
+2329
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
[workspace]
resolver = "2"
members = [
"crates/glances-core",
"crates/glances-plugins",
"crates/glances-tui",
"crates/glances-cli",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://example.invalid/glances-rs"
rust-version = "1.85"
[workspace.dependencies]
glances-core = { path = "crates/glances-core" }
glances-plugins = { path = "crates/glances-plugins" }
glances-tui = { path = "crates/glances-tui" }
anyhow = "1"
clap = { version = "4", features = ["derive", "wrap_help"] }
clap_complete = "4"
indexmap = { version = "2", features = ["serde"] }
libc = "0.2"
regex = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
thiserror = "2"
unicode-width = "0.2"
tracing = "0.1"
[profile.release]
lto = "thin"
strip = true
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Glances-rs contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
# glances-rs
A Rust rewrite of [Glances](https://github.com/nicolargo/glances), the system
monitoring tool, targeting Linux.
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
The following Python Glances features are intentionally **not** provided:
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:
| Crate | Purpose |
| --- | --- |
| `glances-core` | plugin model, config, limits, history, events, filters, output |
| `glances-plugins` | collectors and plugin registry/engine |
| `glances-tui` | terminal UI (ratatui) |
| `glances-cli` | the `glances` binary |
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.
+9
View File
@@ -0,0 +1,9 @@
✨ {{ gl.system['hostname'] }}{{ ' - ' + gl.ip['address'] if gl.ip['address'] else '' }}
⚙️ {{ gl.system['hr_name'] }} | Uptime: {{ gl.uptime }}
💡 LOAD {{ '%0.2f'| format(gl.load['min1']) }} {{ '%0.2f'| format(gl.load['min5']) }} {{ '%0.2f'| format(gl.load['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 %}
+23
View File
@@ -0,0 +1,23 @@
_____ _
/ ____| |
| | __| | __ _ _ __ ___ ___ ___
| | |_ | |/ _` | '_ \ / __/ _ \/ __|
| |__| | | (_| | | | | (_| __/\__
\_____|_|\__,_|_| |_|\___\___||___/
✨ {{ gl.system['hostname'] }}{{ ' - ' + gl.ip['address'] if gl.ip['address'] else '' }}
⚙️ {{ gl.system['hr_name'] }} | Uptime: {{ gl.uptime }}
💡 LOAD {{ '%0.2f'| format(gl.load['min1']) }} {{ '%0.2f'| format(gl.load['min5']) }} {{ '%0.2f'| format(gl.load['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 %}
+1086
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "glances-cli"
version.workspace = true
edition.workspace = true
[[bin]]
name = "glances"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap = { workspace = true, features = ["derive", "wrap_help"] }
clap_complete.workspace = true
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" }
serde_json.workspace = true
tracing.workspace = true
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+43
View File
@@ -0,0 +1,43 @@
//! Logging setup (port of `glances.logger`).
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
/// Return the Glances log file path.
pub fn log_filename() -> PathBuf {
let base = std::env::var("XDG_CACHE_HOME")
.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");
}
}
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
/// errors also go to stderr.
pub fn init(debug: bool) {
let log_file = log_filename();
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
.ok();
let level = if debug { "debug" } else { "info" };
let filter = EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
if let Some(file) = file {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(file)
.with_ansi(false)
.with_target(true)
.try_init()
.ok();
}
}
+191
View File
@@ -0,0 +1,191 @@
//! The `glances` binary.
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_plugins::engine::Engine;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
static RUNNING: AtomicBool = AtomicBool::new(true);
fn main() {
let args = Args::parse();
if args.version {
println!("{}", args.version_message(&logging::log_filename().display().to_string()));
return;
}
if let Some(shell) = &args.print_completion {
print_completion(shell);
return;
}
if let Some(feature) = args.unsupported() {
eprintln!("glances: error: {feature} is not supported by this Rust implementation");
std::process::exit(2);
}
logging::init(args.debug);
if let Err(err) = run(args) {
tracing::error!("{err}");
eprintln!("glances: {err}");
std::process::exit(1);
}
}
fn run(mut args: Args) -> Result<()> {
install_signal_handler();
let config = Config::new(args.conf_file.as_deref(), args.disable_config_exec);
// The configuration key can be overwritten from the command line (-t).
if args.time.is_none() {
let refresh = if config.has_section("global") {
config.get_float_value("global", "refresh", 2.0)
} else {
2.0
};
args.time = Some(refresh);
}
// --memory-leak: quiet mode, 60 seconds, 1 second refresh.
if args.memory_leak {
args.quiet = true;
if args.stop_after.is_none() {
args.stop_after = Some(60);
}
args.time = Some(1.0);
args.disable_history = true;
}
let mut engine = Engine::new(args.clone(), config);
if args.modules_list {
display_modules_list(&engine);
return Ok(());
}
if args.stdout_issue {
engine.update();
print!("{}", outputs::render_issue(&engine));
return Ok(());
}
if args.stdout.is_some() || args.stdout_json.is_some() || args.stdout_csv.is_some() {
return run_stdout(&mut engine, &args);
}
if args.stdout_fetch {
return tui_placeholder::run_fetch(&mut engine, &args);
}
if args.quiet {
return run_quiet(&mut engine, &args);
}
tui_placeholder::run(&mut engine, &args)
}
fn display_modules_list(engine: &Engine) {
let mut plugins = engine.get_plugins_list(false);
plugins.sort();
println!("Plugins list: {}", plugins.join(", "));
println!("Exporters list: ");
}
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()),
other => {
eprintln!("glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)");
std::process::exit(2);
}
}
}
fn install_signal_handler() {
let _ = ctrlc::set_handler(|| {
RUNNING.store(false, Ordering::SeqCst);
});
}
fn sleep_with_signal(duration: f64) {
let deadline = Instant::now() + Duration::from_secs_f64(duration.max(0.0));
while RUNNING.load(Ordering::SeqCst) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(50));
}
}
fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
let refresh = args.time.unwrap_or(2.0);
let iterations = args.stop_after;
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<_>>()
});
let csv_plugins = args.stdout_csv.as_deref().map(|value| {
value
.split(',')
.map(|p| p.split('.').next().unwrap_or(p).trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
});
let mut count: i64 = 0;
while RUNNING.load(Ordering::SeqCst) {
let start = Instant::now();
engine.update();
let output = if let Some(items) = &stdout_items {
outputs::render_stdout(engine, items)
} else if let Some(plugins) = &json_plugins {
format!("{}\n", outputs::render_stdout_json(engine, plugins))
} else if let Some(plugins) = &csv_plugins {
format!("{}\n", csv.render(engine, plugins))
} else {
String::new()
};
print!("{output}");
std::io::stdout().flush().ok();
count += 1;
if let Some(max) = iterations {
if count >= max {
break;
}
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
}
Ok(())
}
fn run_quiet(engine: &mut Engine, args: &Args) -> Result<()> {
let refresh = args.time.unwrap_or(2.0);
let mut count: i64 = 0;
while RUNNING.load(Ordering::SeqCst) {
let start = Instant::now();
engine.update();
count += 1;
if let Some(max) = args.stop_after {
if count >= max {
break;
}
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
}
Ok(())
}
+236
View File
@@ -0,0 +1,236 @@
//! Terminal output modes: `--stdout`, `--stdout-csv`, `--stdout-json`.
use glances_plugins::engine::Engine;
use serde_json::{Map, Value, json};
/// One `<plugin>[.<key>].<attribute>` item of the stdout modes.
#[derive(Debug, Clone, PartialEq)]
pub struct StdoutItem {
pub plugin: String,
pub key: Option<String>,
pub attribute: Option<String>,
}
pub fn build_stdout_list(value: &str) -> Vec<StdoutItem> {
value
.split(',')
.map(|item| {
let parts: Vec<&str> = item.split('.').collect();
match parts.len() {
1 => StdoutItem { plugin: parts[0].to_string(), key: None, attribute: None },
2 => StdoutItem {
plugin: parts[0].to_string(),
key: None,
attribute: Some(parts[1].to_string()),
},
_ => StdoutItem {
plugin: parts[0].to_string(),
key: Some(parts[1].to_string()),
attribute: Some(parts[2].to_string()),
},
}
})
.collect()
}
/// `--stdout`: one stat per line.
pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
let mut out = String::new();
for item in items {
let Some(plugin) = engine.get_plugin(&item.plugin) else {
continue;
};
if !plugin.is_enabled() {
continue;
}
let stat = plugin.export();
match &item.attribute {
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)));
} else {
tracing::error!("Can not display stat {}.{} (KeyError)", item.plugin, attribute);
}
}
Value::Array(list) => {
for entry in list {
let Some(entry) = entry.as_object() else {
continue;
};
let entry_key = entry
.get("key")
.and_then(Value::as_str)
.and_then(|key_field| entry.get(key_field))
.map(glances_core::py_str);
let Some(entry_key) = entry_key else {
continue;
};
if let Some(wanted) = &item.key {
if wanted != &entry_key {
continue;
}
}
if let Some(value) = entry.get(attribute) {
out.push_str(&format!(
"{}.{}.{}: {}\n",
item.plugin,
entry_key,
attribute,
glances_core::py_str(value)
));
}
}
}
_ => {}
},
None => {
out.push_str(&format!("{}: {}\n", item.plugin, glances_core::py_repr(&stat)));
}
}
}
out
}
/// `--stdout-json`: one JSON object per line.
pub fn render_stdout_json(engine: &Engine, plugins: &[String]) -> String {
let mut result = Map::new();
for name in plugins {
let Some(plugin) = engine.get_plugin(name) else {
continue;
};
if plugin.is_enabled() {
result.insert(name.clone(), plugin.export());
}
}
serde_json::to_string(&Value::Object(result)).unwrap_or_else(|_| "{}".to_string())
}
/// `--stdout-csv`: all the stats in one line, with a header.
#[derive(Debug, Default)]
pub struct CsvRenderer {
header: bool,
list_keys: std::collections::HashMap<String, Vec<String>>,
header_field_names: std::collections::HashMap<String, Vec<String>>,
}
impl CsvRenderer {
pub fn new() -> Self {
Self { header: true, ..Default::default() }
}
pub fn render(&mut self, engine: &Engine, plugins: &[String]) -> String {
let mut line = String::new();
for name in plugins {
let Some(plugin) = engine.get_plugin(name) else {
continue;
};
if !plugin.is_enabled() {
continue;
}
let stat = plugin.export();
if self.header {
line.push_str(&self.build_header(name, &stat));
} else {
line.push_str(&self.build_data(name, &stat));
}
}
self.header = false;
line.pop();
line
}
fn build_header(&mut self, plugin: &str, stat: &Value) -> String {
let mut line = String::new();
match stat {
Value::Object(map) => {
for key in map.keys() {
line.push_str(&format!("{plugin}.{key},"));
}
}
Value::Array(list) => {
let mut keys_order = Vec::new();
for entry in list {
let Some(entry) = entry.as_object() else {
continue;
};
let Some(identity_field) = entry.get("key").and_then(Value::as_str) else {
continue;
};
let Some(identity) = entry.get(identity_field) else {
continue;
};
let identity = glances_core::py_str(identity);
keys_order.push(identity.clone());
for key in entry.keys() {
line.push_str(&format!("{plugin}.{identity}.{key},"));
}
}
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());
}
}
_ => {
line.push_str(&format!("{plugin},"));
}
}
line
}
fn build_data(&self, plugin: &str, stat: &Value) -> String {
let mut line = String::new();
match stat {
Value::Object(map) => {
for value in map.values() {
line.push_str(&format!("{},", glances_core::py_str(value)));
}
}
Value::Array(list) => {
let mut current = std::collections::HashMap::new();
for entry in list {
let Some(entry) = entry.as_object() else {
continue;
};
let Some(identity_field) = entry.get("key").and_then(Value::as_str) else {
continue;
};
let Some(identity) = entry.get(identity_field) else {
continue;
};
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(&[]) {
let entry = current.get(identity).unwrap_or(&empty);
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),
None => "N/A".to_string(),
};
line.push_str(&format!("{text},"));
}
}
}
_ => {
line.push_str(&format!("{},", glances_core::py_str(stat)));
}
}
line
}
}
/// `--issue`: dump every plugin stats (used for bug reports).
pub fn render_issue(engine: &Engine) -> String {
let mut out = String::new();
out.push_str(&format!("Glances version: {}\n", crate::VERSION));
for name in engine.get_plugins_list(false) {
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())));
}
out
}
+14
View File
@@ -0,0 +1,14 @@
//! 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(_engine: &mut Engine, _args: &Args) -> Result<()> {
anyhow::bail!("terminal UI is not implemented yet (use --stdout, --stdout-csv or --stdout-json)")
}
pub fn run_fetch(_engine: &mut Engine, _args: &Args) -> Result<()> {
anyhow::bail!("fetch mode is not implemented yet")
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "glances-core"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
clap = { workspace = true, features = ["derive", "wrap_help"] }
indexmap.workspace = true
libc.workspace = true
regex.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
shlex = "2.0.1"
sysinfo = "0.39.6"
thiserror.workspace = true
tracing.workspace = true
unicode-width.workspace = true
+280
View File
@@ -0,0 +1,280 @@
//! On-alert actions (port of `glances.actions`).
use crate::secure::{sanitize_value, secure_popen};
use crate::timer::Timer;
use serde_json::Value;
use std::collections::HashMap;
/// Render a (small) Mustache template with JSON data.
///
/// Supported tags: `{{name}}` (HTML escaped), `{{{name}}}` and `{{&name}}`
/// (raw), dotted names, and `{{#name}}...{{/name}}` / `{{^name}}...{{/name}}`
/// sections. Anything else is refused so a malformed template can never be
/// executed half-rendered.
pub fn render_template(template: &str, data: &Value) -> Result<String, String> {
render_range(template, data, &mut 0)
}
fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result<String, String> {
let mut out = String::new();
while *pos < template.len() {
let rest = &template[*pos..];
let Some(open) = rest.find("{{") else {
out.push_str(rest);
*pos = template.len();
break;
};
out.push_str(&rest[..open]);
*pos += open;
// Standalone-ish: read the tag.
let tag_start = *pos + 2;
let after = &template[tag_start..];
let (tag, raw, tag_end) = if let Some(stripped) = after.strip_prefix('{') {
// Triple mustache
let Some(close) = stripped.find("}}}") else {
return Err("unclosed mustache tag".to_string());
};
(stripped[..close].trim().to_string(), true, tag_start + 1 + close + 3)
} else {
let Some(close) = after.find("}}") else {
return Err("unclosed mustache tag".to_string());
};
(after[..close].trim().to_string(), false, tag_start + close + 2)
};
if let Some(name) = tag.strip_prefix('#') {
// Section: find the matching close tag.
let body_start = tag_end;
let (body, body_end) = find_section(&template[body_start..], name.trim())?;
*pos = body_start + body_end;
let value = lookup(data, name.trim());
match value {
Some(Value::Array(items)) => {
for item in &items {
let mut inner_pos = 0;
out.push_str(&render_range(body, item, &mut inner_pos)?);
}
}
Some(Value::Bool(true)) => {
let mut inner_pos = 0;
out.push_str(&render_range(body, data, &mut inner_pos)?);
}
Some(Value::Object(map)) => {
let merged = merge_context(data, &Value::Object(map.clone()));
let mut inner_pos = 0;
out.push_str(&render_range(body, &merged, &mut inner_pos)?);
}
Some(Value::String(s)) if !s.is_empty() => {
let mut inner_pos = 0;
out.push_str(&render_range(body, data, &mut inner_pos)?);
}
_ => {}
}
} else if let Some(name) = tag.strip_prefix('^') {
let body_start = tag_end;
let (body, body_end) = find_section(&template[body_start..], name.trim())?;
*pos = body_start + body_end;
let value = lookup(data, name.trim());
let falsy = match value {
None | Some(Value::Null) | Some(Value::Bool(false)) => true,
Some(Value::Array(items)) => items.is_empty(),
Some(Value::String(s)) => s.is_empty(),
_ => false,
};
if falsy {
let mut inner_pos = 0;
out.push_str(&render_range(body, data, &mut inner_pos)?);
}
} else if let Some(name) = tag.strip_prefix('/') {
return Err(format!("unexpected closing tag {{{{/{}}}}}", name.trim()));
} else {
*pos = tag_end;
let name = tag.strip_prefix('&').unwrap_or(&tag);
let value = lookup(data, name.trim());
let text = value_to_text(value);
if raw || tag.starts_with('&') {
out.push_str(&text);
} else {
out.push_str(&html_escape(&text));
}
}
}
Ok(out)
}
/// Find the body and length of a section, starting after its opening tag.
fn find_section<'a>(template: &'a str, name: &str) -> Result<(&'a str, usize), String> {
let open = format!("{{{{#{name}}}}}");
let close = format!("{{{{/{name}}}}}");
let mut depth = 0usize;
let mut cursor = 0usize;
while cursor < template.len() {
let next_open = template[cursor..].find(&open).map(|i| cursor + i);
let next_close = template[cursor..].find(&close).map(|i| cursor + i);
match (next_open, next_close) {
(Some(o), Some(c)) if c < o => {
if depth == 0 {
return Ok((&template[..c], c + close.len()));
}
depth -= 1;
cursor = c + close.len();
}
(Some(o), _) => {
depth += 1;
cursor = o + open.len();
}
(None, Some(c)) => {
if depth == 0 {
return Ok((&template[..c], c + close.len()));
}
depth -= 1;
cursor = c + close.len();
}
(None, None) => return Err(format!("unclosed mustache section {name}")),
}
}
Err(format!("unclosed mustache section {name}"))
}
fn merge_context(data: &Value, extra: &Value) -> Value {
match (data, extra) {
(Value::Object(base), Value::Object(extra)) => {
let mut merged = base.clone();
for (k, v) in extra {
merged.insert(k.clone(), v.clone());
}
Value::Object(merged)
}
_ => extra.clone(),
}
}
fn lookup(data: &Value, name: &str) -> Option<Value> {
if name == "." {
return Some(data.clone());
}
let mut current = data;
for part in name.split('.') {
current = current.get(part)?;
}
Some(current.clone())
}
fn value_to_text(value: Option<Value>) -> String {
match value {
None | Some(Value::Null) => String::new(),
Some(Value::String(s)) => s,
Some(Value::Bool(b)) => if b { "True" } else { "False" }.to_string(),
Some(Value::Number(n)) => n.to_string(),
Some(other) => crate::units::py_str(&other),
}
}
fn html_escape(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}
/// This class manages actions when an alert is reached.
#[derive(Debug)]
pub struct Actions {
disable_config_exec: bool,
status: HashMap<String, String>,
start_timer: Timer,
}
impl Actions {
pub fn new(disable_config_exec: bool, refresh_time: f64) -> Self {
Self {
disable_config_exec,
status: HashMap::new(),
// Avoid any trigger when Glances is started (issue #732).
start_timer: Timer::new(refresh_time * 2.0),
}
}
pub fn set(&mut self, stat_name: &str, criticality: &str) {
self.status.insert(stat_name.to_string(), criticality.to_string());
}
pub fn get(&self, stat_name: &str) -> Option<&str> {
self.status.get(stat_name).map(|s| s.as_str())
}
/// Run the commands (in background in Python; synchronously here).
pub fn run(
&mut self,
stat_name: &str,
criticality: &str,
commands: &[String],
repeat: bool,
mustache_dict: Option<&Value>,
) -> bool {
if (self.get(stat_name) == Some(criticality) && !repeat) || !self.start_timer.finished() {
return false;
}
tracing::debug!(
"{} action {:?} for {} ({}) with stats {:?}",
if repeat { "Repeat" } else { "Run" },
commands,
stat_name,
criticality,
mustache_dict
);
let safe = mustache_dict.map(sanitize_value);
let render = |arg: &str| -> Result<String, String> {
if let Some(safe) = &safe {
render_template(arg, safe)
} else {
Ok(arg.to_string())
}
};
for cmd in commands {
tracing::info!("Action triggered for {stat_name} ({criticality}): {cmd}");
let ret = secure_popen(cmd, !self.disable_config_exec, Some(&render));
tracing::debug!("Action result for {stat_name} ({criticality}): {ret}");
}
self.set(stat_name, criticality);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_render_simple() {
let data = json!({"name": "nginx", "pid": 42});
assert_eq!(render_template("kill {{pid}} {{name}}", &data).unwrap(), "kill 42 nginx");
assert_eq!(render_template("raw {{{name}}}", &data).unwrap(), "raw nginx");
}
#[test]
fn test_render_escape() {
let data = json!({"name": "a'b"});
assert_eq!(render_template("{{name}}", &data).unwrap(), "a&#39;b");
assert_eq!(render_template("{{{name}}}", &data).unwrap(), "a'b");
}
#[test]
fn test_render_section() {
let data = json!({"items": [1, 2]});
assert_eq!(render_template("{{#items}}x{{/items}}", &data).unwrap(), "xx");
assert_eq!(render_template("{{^missing}}y{{/missing}}", &data).unwrap(), "y");
}
#[test]
fn test_actions_no_trigger_before_start_timer() {
let mut actions = Actions::new(false, 100.0);
assert!(!actions.run("cpu_user", "critical", &["/bin/true".to_string()], false, None));
}
}
+503
View File
@@ -0,0 +1,503 @@
//! Command line arguments (drop-in compatible with Glances standalone mode).
//!
//! Flags belonging to intentionally unsupported features (web server, client/
//! server, exporters...) are still accepted so that the CLI can fail with a
//! clear message instead of an "unknown argument" error.
use clap::{ArgAction, Parser, ValueEnum};
pub const GLANCES_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const GLANCES_API_VERSION: &str = "4";
/// Available process sort keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SortProcesses {
#[value(name = "cpu_percent")]
CpuPercent,
#[value(name = "memory_percent")]
MemoryPercent,
#[value(name = "username")]
Username,
#[value(name = "cpu_times")]
CpuTimes,
#[value(name = "io_counters")]
IoCounters,
#[value(name = "name")]
Name,
#[value(name = "cpu_num")]
CpuNum,
}
impl SortProcesses {
pub fn as_str(self) -> &'static str {
match self {
SortProcesses::CpuPercent => "cpu_percent",
SortProcesses::MemoryPercent => "memory_percent",
SortProcesses::Username => "username",
SortProcesses::CpuTimes => "cpu_times",
SortProcesses::IoCounters => "io_counters",
SortProcesses::Name => "name",
SortProcesses::CpuNum => "cpu_num",
}
}
}
#[derive(Parser, Debug, Clone)]
#[command(
name = "glances",
disable_version_flag = true,
after_help = EXAMPLES
)]
pub struct Args {
/// enable debug mode
#[arg(short = 'd', long = "debug", action = ArgAction::SetTrue)]
pub debug: bool,
/// display the version and exit
#[arg(short = 'V', long = "version", action = ArgAction::SetTrue)]
pub version: bool,
/// path to the configuration file
#[arg(short = 'C', long = "config", value_name = "FILE")]
pub conf_file: Option<String>,
/// path to additional plugin directory (Rust plugins only; Python plugins are not supported)
#[arg(short = 'P', long = "plugins", value_name = "DIR")]
pub plugin_dir: Option<String>,
/// display modules (plugins & exports) list and exit
#[arg(long = "modules-list", visible_alias = "module-list", action = ArgAction::SetTrue)]
pub modules_list: bool,
/// disable plugin (comma-separated list or all)
#[arg(
long = "disable-plugin",
visible_aliases = ["disable-plugins", "disable"],
value_name = "PLUGINS"
)]
pub disable_plugin: Option<String>,
/// enable plugin (comma-separated list)
#[arg(
long = "enable-plugin",
visible_aliases = ["enable-plugins", "enable"],
value_name = "PLUGINS"
)]
pub enable_plugin: Option<String>,
/// disable process module
#[arg(long = "disable-process", action = ArgAction::SetTrue)]
pub disable_process: bool,
/// light mode for Curses UI (disable all but the top menu)
#[arg(long = "light", visible_alias = "enable-light", action = ArgAction::SetTrue)]
pub enable_light: bool,
/// task's cpu usage will be divided by the total number of CPUs
#[arg(short = '0', long = "disable-irix", action = ArgAction::SetTrue)]
pub disable_irix: bool,
/// start Glances in per CPU mode
#[arg(short = '1', long = "percpu", visible_alias = "per-cpu", action = ArgAction::SetTrue)]
pub percpu: bool,
/// disable network, disk I/O, FS and sensors modules
#[arg(short = '2', long = "disable-left-sidebar", action = ArgAction::SetTrue)]
pub disable_left_sidebar: bool,
/// disable quick look module
#[arg(short = '3', long = "disable-quicklook", action = ArgAction::SetTrue)]
pub disable_quicklook: bool,
/// disable all but quick look and load
#[arg(short = '4', long = "full-quicklook", action = ArgAction::SetTrue)]
pub full_quicklook: bool,
/// disable top menu (QL, CPU, MEM, SWAP and LOAD)
#[arg(short = '5', long = "disable-top", action = ArgAction::SetTrue)]
pub disable_top: bool,
/// start Glances in mean GPU mode
#[arg(short = '6', long = "meangpu", action = ArgAction::SetTrue)]
pub meangpu: bool,
/// disable stats history
#[arg(long = "disable-history", action = ArgAction::SetTrue)]
pub disable_history: bool,
/// disable bold mode in the terminal
#[arg(long = "disable-bold", action = ArgAction::SetTrue)]
pub disable_bold: bool,
/// disable background colors in the terminal
#[arg(long = "disable-bg", action = ArgAction::SetTrue)]
pub disable_bg: bool,
/// enable IRQ module
#[arg(long = "enable-irq", action = ArgAction::SetTrue)]
pub enable_irq: bool,
/// enable extended stats on top process
#[arg(long = "enable-process-extended", action = ArgAction::SetTrue)]
pub enable_process_extended: bool,
/// disable separator in the UI (between top and others modules)
#[arg(long = "disable-separator", action = ArgAction::SetTrue)]
pub disable_separator: bool,
/// disable cursor (process selection) in the UI
#[arg(long = "disable-cursor", action = ArgAction::SetTrue)]
pub disable_cursor: bool,
/// Use arrow keys to sort the process list instead of the SHIFT+key combinations
#[arg(long = "arrow-keys-sort", action = ArgAction::SetTrue)]
pub arrow_keys_sort: bool,
/// Sort processes by: cpu_percent, memory_percent, username, cpu_times, io_counters, name, cpu_num
#[arg(long = "sort-processes", value_name = "KEY")]
pub sort_processes_key: Option<SortProcesses>,
/// Accumulate processes by program
#[arg(long = "programs", visible_alias = "program", action = ArgAction::SetTrue)]
pub programs: bool,
/// stop Glances after n refresh
#[arg(long = "stop-after", value_name = "N")]
pub stop_after: Option<i64>,
/// do not display the curses interface
#[arg(short = 'q', long = "quiet", action = ArgAction::SetTrue)]
pub quiet: bool,
/// set the process filter pattern (regular expression)
#[arg(short = 'f', long = "process-filter", value_name = "PATTERN")]
pub process_filter: Option<String>,
/// set a process list to focus on (comma-separated list of Glances filter)
#[arg(long = "process-focus", value_name = "FILTER")]
pub process_focus: Option<String>,
/// force short name for processes name
#[arg(long = "process-short-name", overrides_with = "process_long_name", action = ArgAction::SetTrue)]
pub process_short_name_flag: bool,
/// force long name for processes name
#[arg(long = "process-long-name", overrides_with = "process_short_name_flag", action = ArgAction::SetTrue)]
pub process_long_name: bool,
/// display stats to stdout, one stat per line (comma-separated list of plugins/plugins.attribute)
#[arg(long = "stdout", value_name = "PLUGINS")]
pub stdout: Option<String>,
/// display stats to stdout, JSON format (comma-separated list of plugins/plugins.attribute)
#[arg(long = "stdout-json", value_name = "PLUGINS")]
pub stdout_json: Option<String>,
/// display stats to stdout, CSV format (comma-separated list of plugins/plugins.attribute)
#[arg(long = "stdout-csv", value_name = "PLUGINS")]
pub stdout_csv: Option<String>,
/// test all plugins and exit (please copy/paste the output if you open an issue)
#[arg(long = "issue", action = ArgAction::SetTrue)]
pub stdout_issue: bool,
/// test memory leak (quiet mode during 60 seconds)
#[arg(long = "memory-leak", action = ArgAction::SetTrue)]
pub memory_leak: bool,
/// hide kernel threads in the process list (not available on Windows)
#[arg(long = "hide-kernel-threads", action = ArgAction::SetTrue)]
pub no_kernel_threads: bool,
/// display network rate in bytes per second
#[arg(short = 'b', long = "byte", action = ArgAction::SetTrue)]
pub byte: bool,
/// show RAM Fs in the DiskIO plugin
#[arg(long = "diskio-show-ramfs", action = ArgAction::SetTrue)]
pub diskio_show_ramfs: bool,
/// show IO per second in the DiskIO plugin
#[arg(long = "diskio-iops", action = ArgAction::SetTrue)]
pub diskio_iops: bool,
/// show IO latency in the DiskIO plugin
#[arg(long = "diskio-latency", action = ArgAction::SetTrue)]
pub diskio_latency: bool,
/// display temperature in Fahrenheit (default is Celsius)
#[arg(long = "fahrenheit", action = ArgAction::SetTrue)]
pub fahrenheit: bool,
/// display FS free space instead of used
#[arg(long = "fs-free-space", action = ArgAction::SetTrue)]
pub fs_free_space: bool,
/// display sparklines instead of bar in the curses interface
#[arg(long = "sparkline", action = ArgAction::SetTrue)]
pub sparkline: bool,
/// disable unicode characters in the curses interface
#[arg(long = "disable-unicode", action = ArgAction::SetTrue)]
pub disable_unicode: bool,
/// hide public information (like public IP)
#[arg(long = "hide-public-info", action = ArgAction::SetTrue)]
pub hide_public_info: bool,
/// disable backtick command execution in configuration values and shell operator interpretation (&&, |, >) in AMP commands
#[arg(long = "disable-config-exec", action = ArgAction::SetTrue)]
pub disable_config_exec: bool,
/// disable online Glances version check
#[arg(long = "disable-check-update", action = ArgAction::SetTrue)]
pub disable_check_update: bool,
/// strftime format string for displaying current date in standalone mode
#[arg(long = "strftime", value_name = "FORMAT", default_value = "")]
pub strftime_format: String,
/// set minimum refresh rate in seconds [default: 2 sec]
#[arg(short = 't', long = "time", value_name = "SECOND")]
pub time: Option<f64>,
/// display a (neo)fetch like summary and exit
#[arg(long = "fetch", visible_alias = "stdout-fetch", action = ArgAction::SetTrue)]
pub stdout_fetch: bool,
/// overwrite default fetch template file
#[arg(
long = "fetch-template",
visible_alias = "stdout-fetch-template",
value_name = "FILE",
default_value = ""
)]
pub fetch_template: String,
/// print shell completion and exit (bash, zsh or fish)
#[arg(long = "print-completion", value_name = "SHELL")]
pub print_completion: Option<String>,
// ------------------------------------------------------------------
// Unsupported features: accepted for a clear error message.
// ------------------------------------------------------------------
#[arg(short = 'w', long = "webserver", action = ArgAction::SetTrue, hide = true)]
pub webserver: bool,
#[arg(long = "disable-webui", action = ArgAction::SetTrue, hide = true)]
pub disable_webui: bool,
#[arg(long = "enable-mcp", action = ArgAction::SetTrue, hide = true)]
pub enable_mcp: bool,
#[arg(long = "mcp-path", value_name = "PATH", hide = true)]
pub mcp_path: Option<String>,
#[arg(long = "open-web-browser", action = ArgAction::SetTrue, hide = true)]
pub open_web_browser: bool,
#[arg(short = 'c', long = "client", value_name = "SERVER", hide = true)]
pub client: Option<String>,
#[arg(short = 's', long = "server", action = ArgAction::SetTrue, hide = true)]
pub server: bool,
#[arg(long = "browser", action = ArgAction::SetTrue, hide = true)]
pub browser: bool,
#[arg(long = "disable-autodiscover", action = ArgAction::SetTrue, hide = true)]
pub disable_autodiscover: bool,
#[arg(short = 'p', long = "port", value_name = "PORT", hide = true)]
pub port: Option<i64>,
#[arg(short = 'B', long = "bind", value_name = "ADDRESS", hide = true)]
pub bind_address: Option<String>,
#[arg(short = 'u', value_name = "USERNAME", hide = true)]
pub username_used: Option<String>,
#[arg(long = "username", action = ArgAction::SetTrue, hide = true)]
pub username_prompt: bool,
#[arg(long = "password", action = ArgAction::SetTrue, hide = true)]
pub password_prompt: bool,
#[arg(long = "snmp-community", value_name = "COMMUNITY", hide = true)]
pub snmp_community: Option<String>,
#[arg(long = "snmp-port", value_name = "PORT", hide = true)]
pub snmp_port: Option<i64>,
#[arg(long = "snmp-version", value_name = "VERSION", hide = true)]
pub snmp_version: Option<String>,
#[arg(long = "snmp-user", value_name = "USER", hide = true)]
pub snmp_user: Option<String>,
#[arg(long = "snmp-auth", value_name = "AUTH", hide = true)]
pub snmp_auth: Option<String>,
#[arg(long = "snmp-force", action = ArgAction::SetTrue, hide = true)]
pub snmp_force: bool,
#[arg(long = "cached-time", value_name = "SEC", hide = true)]
pub cached_time: Option<i64>,
#[arg(long = "export", value_name = "EXPORTERS", hide = true)]
pub export: Option<String>,
#[arg(long = "export-csv-file", value_name = "FILE", hide = true)]
pub export_csv_file: Option<String>,
#[arg(long = "export-csv-overwrite", action = ArgAction::SetTrue, hide = true)]
pub export_csv_overwrite: bool,
#[arg(long = "export-json-file", value_name = "FILE", hide = true)]
pub export_json_file: Option<String>,
#[arg(long = "export-graph-path", value_name = "DIR", hide = true)]
pub export_graph_path: Option<String>,
#[arg(long = "export-process-filter", value_name = "FILTER", hide = true)]
pub export_process_filter: Option<String>,
#[arg(long = "api-doc", action = ArgAction::SetTrue, hide = true)]
pub stdout_api_doc: bool,
#[arg(long = "api-restful-doc", action = ArgAction::SetTrue, hide = true)]
pub stdout_api_restful_doc: bool,
#[arg(long = "trace-malloc", action = ArgAction::SetTrue, hide = true)]
pub trace_malloc: bool,
}
const EXAMPLES: &str = "\
Examples of use:
Monitor local machine (standalone mode):
$ glances
Display all Glances modules (plugins) and exit:
$ glances --module-list
Display stats to stdout (one stat per line, possible to go inside stats using plugin.attribute):
$ glances --stdout now,cpu.user,mem.used,load
Display JSON stats to stdout (one stats per line):
$ glances --stdout-json now,cpu,mem,load
Display CSV stats to stdout (all stats in one line):
$ glances --stdout-csv now,cpu.user,mem.used,load
Enable some plugins disabled by default (comma-separated list):
$ glances --enable-plugin sensors
Disable some plugins (comma-separated list):
$ glances --disable-plugin network,ports
Disable all plugins except some (comma-separated list):
$ glances --disable-plugin all --enable-plugin cpu,mem,load";
impl Args {
/// Return the reason why this command line can not be handled, if any.
pub fn unsupported(&self) -> Option<&'static str> {
let checks: &[(bool, &'static str)] = &[
(self.webserver, "web server mode (-w/--webserver)"),
(self.enable_mcp, "the MCP server (--enable-mcp)"),
(self.client.is_some(), "client mode (-c/--client)"),
(self.server, "server mode (-s/--server)"),
(self.browser, "the server browser (--browser)"),
(self.export.is_some(), "export modules (--export)"),
(self.stdout_api_doc, "the Python API documentation (--api-doc)"),
(self.stdout_api_restful_doc, "the RESTful API documentation (--api-restful-doc)"),
(self.trace_malloc, "Python memory tracing (--trace-malloc)"),
];
checks.iter().find(|(used, _)| *used).map(|(_, name)| *name)
}
pub fn process_short_name(&self) -> bool {
!self.process_long_name
}
pub fn enable_separator(&self) -> bool {
!self.disable_separator
}
/// Python-compatible version message.
pub fn version_message(&self, log_file: &str) -> String {
format!(
"Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}",
"4.5.7",
GLANCES_API_VERSION,
GLANCES_VERSION,
log_file
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(argv: &[&str]) -> Args {
Args::try_parse_from(argv).expect("args should parse")
}
#[test]
fn test_kept_flags_parse() {
let args = parse(&[
"glances",
"-d",
"-C",
"/tmp/glances.conf",
"--disable-plugin",
"all",
"--enable-plugin",
"cpu,mem",
"--light",
"-0",
"-1",
"-2",
"-3",
"-4",
"-5",
"-6",
"--disable-history",
"--disable-bold",
"--disable-bg",
"--enable-irq",
"--enable-process-extended",
"--disable-separator",
"--disable-cursor",
"--arrow-keys-sort",
"--sort-processes",
"memory_percent",
"--programs",
"--stop-after",
"3",
"-q",
"-f",
"python",
"--process-focus",
"user:root",
"--process-long-name",
"--stdout",
"cpu,mem",
"--stdout-json",
"cpu",
"--stdout-csv",
"cpu",
"--issue",
"--hide-kernel-threads",
"-b",
"--diskio-show-ramfs",
"--diskio-iops",
"--diskio-latency",
"--fahrenheit",
"--fs-free-space",
"--sparkline",
"--disable-unicode",
"--hide-public-info",
"--disable-config-exec",
"--disable-check-update",
"--strftime",
"%Y",
"-t",
"5",
"--fetch",
"--fetch-template",
"/tmp/tpl",
"--print-completion",
"bash",
]);
assert!(args.export.is_none());
assert!(!args.process_short_name());
assert_eq!(args.sort_processes_key, Some(SortProcesses::MemoryPercent));
assert_eq!(args.time, Some(5.0));
}
#[test]
fn test_unsupported_flags_detected() {
assert!(parse(&["glances", "-w"]).unsupported().is_some());
assert!(parse(&["glances", "-s"]).unsupported().is_some());
assert!(parse(&["glances", "-c", "host"]).unsupported().is_some());
assert!(parse(&["glances", "--export", "csv"]).unsupported().is_some());
assert!(parse(&["glances"]).unsupported().is_none());
}
#[test]
fn test_version_flag_parses() {
assert!(parse(&["glances", "-V"]).version);
}
}
+392
View File
@@ -0,0 +1,392 @@
//! Configuration file handling (drop-in compatible with `glances.conf`).
//!
//! The file format is the Python `ConfigParser` INI dialect: sections in
//! brackets, `key = value` or `key: value` options, `#`/`;` comments and
//! indented continuation lines. Option names are lower-cased; section names
//! are kept as is.
use indexmap::IndexMap;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const CONFIG_FILENAME: &str = "glances.conf";
#[derive(Debug, Clone, Default)]
pub struct Config {
sections: IndexMap<String, IndexMap<String, String>>,
loaded_file: Option<PathBuf>,
disable_config_exec: bool,
}
impl Config {
/// Build the configuration by searching the standard paths (unless a
/// custom `-C` path is given) and reading the first existing file.
pub fn new(config_dir: Option<&str>, disable_config_exec: bool) -> Self {
let mut config = Config {
sections: IndexMap::new(),
loaded_file: None,
disable_config_exec,
};
config.read(config_dir);
config
}
/// Build an empty configuration (used by tests and by callers that
/// already read a file).
pub fn empty(disable_config_exec: bool) -> Self {
let mut config = Config::default();
config.disable_config_exec = disable_config_exec;
config.set_defaults();
config
}
/// Parse a configuration from a string (used by tests).
pub fn from_str(content: &str, disable_config_exec: bool) -> Self {
let mut config = Config {
sections: parse_ini(content),
loaded_file: None,
disable_config_exec,
};
config.set_defaults();
config
}
fn config_file_paths(config_dir: Option<&str>) -> Vec<PathBuf> {
let mut paths = Vec::new();
// Custom path given with -C.
if let Some(dir) = config_dir {
paths.push(PathBuf::from(dir));
}
// User config dir: ~/.config/glances
let user_base = std::env::var("XDG_CONFIG_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config")));
if let Some(base) = user_base {
paths.push(base.join("glances").join(CONFIG_FILENAME));
}
// System-wide config dir: /etc/glances
paths.push(PathBuf::from("/etc/glances").join(CONFIG_FILENAME));
// Default location: <prefix>/share/doc/glances (and the checkout
// layout next to the executable, useful while developing).
if let Ok(exe) = std::env::current_exe() {
if let Some(bin_dir) = exe.parent() {
for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME));
paths.push(prefix.join("conf").join(CONFIG_FILENAME));
}
}
}
paths
}
fn read(&mut self, config_dir: Option<&str>) {
for config_file in Self::config_file_paths(config_dir) {
if config_file.is_file() {
match std::fs::read_to_string(&config_file) {
Ok(content) => {
self.sections = parse_ini(&content);
self.loaded_file = Some(config_file.clone());
tracing::info!("Read configuration file '{}'", config_file.display());
}
Err(err) => {
tracing::error!("Can not read configuration file '{}': {err}", config_file.display());
std::process::exit(1);
}
}
break;
}
}
self.set_defaults();
}
/// Fill in the default values exactly like the Python implementation.
fn set_defaults(&mut self) {
// Globals
self.set_default("global", "strftime_format", "");
self.set_default("global", "check_update", "true");
// Quicklook
self.set_default_cwc("quicklook", Some("cpu"), ["50", "70", "90"]);
self.set_default_cwc("quicklook", Some("mem"), ["50", "70", "90"]);
self.set_default_cwc("quicklook", Some("swap"), ["50", "70", "90"]);
// CPU
self.set_default_cwc("cpu", Some("user"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("system"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("steal"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]);
// Context switches bottleneck identification #1212
let ctx_switches_bottleneck =
500000.0 * 0.10 * std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64;
let careful = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20));
let warning = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10));
let critical = format!("{ctx_switches_bottleneck}");
self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]);
// Per-CPU
self.set_default_cwc("percpu", Some("user"), ["50", "70", "90"]);
self.set_default_cwc("percpu", Some("system"), ["50", "70", "90"]);
// Load
self.set_default_cwc("load", None, ["0.7", "1.0", "5.0"]);
// Mem
self.set_default_cwc("mem", None, ["50", "70", "90"]);
// Swap
self.set_default_cwc("memswap", None, ["50", "70", "90"]);
// Network
self.set_default_cwc("network", Some("rx"), ["50", "70", "90"]);
self.set_default_cwc("network", Some("tx"), ["50", "70", "90"]);
// FS
self.set_default_cwc("fs", None, ["50", "70", "90"]);
// Sensors
self.set_default_cwc("sensors", Some("temperature_hdd"), ["45", "52", "60"]);
self.set_default_cwc("sensors", Some("battery"), ["70", "80", "90"]);
// Process list
self.set_default_cwc("processlist", Some("cpu"), ["50", "70", "90"]);
self.set_default_cwc("processlist", Some("mem"), ["50", "70", "90"]);
}
fn set_default_cwc<const N: usize>(&mut self, section: &str, option_header: Option<&str>, cwc: [&str; N]) {
let header = match option_header {
Some(header) => format!("{header}_"),
None => String::new(),
};
self.set_default(section, &format!("{header}careful"), cwc[0]);
self.set_default(section, &format!("{header}warning"), cwc[1]);
self.set_default(section, &format!("{header}critical"), cwc[2]);
}
fn set_default(&mut self, section: &str, option: &str, default: &str) {
let entry = self.sections.entry(section.to_string()).or_default();
entry.entry(option.to_string()).or_insert_with(|| default.to_string());
}
pub fn loaded_config_file(&self) -> Option<&Path> {
self.loaded_file.as_deref()
}
/// List all section names.
pub fn sections(&self) -> Vec<&str> {
self.sections.keys().map(|s| s.as_str()).collect()
}
pub fn has_section(&self, section: &str) -> bool {
self.sections.contains_key(section)
}
pub fn has_option(&self, section: &str, option: &str) -> bool {
self.sections
.get(section)
.map(|s| s.contains_key(&option.to_lowercase()))
.unwrap_or(false)
}
/// List `(key, value)` pairs of a section.
pub fn items(&self, section: &str) -> Vec<(String, String)> {
self.sections
.get(section)
.map(|s| s.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
/// Get a raw value. Dynamic values between backticks are executed unless
/// `--disable-config-exec` is set.
pub fn get_value(&self, section: &str, option: &str, default: Option<&str>) -> Option<String> {
let mut ret = self
.sections
.get(section)
.and_then(|s| s.get(&option.to_lowercase()))
.cloned()
.or_else(|| default.map(|d| d.to_string()));
if let Some(value) = ret.as_mut() {
for command in find_backticks(value) {
if self.disable_config_exec {
tracing::warn!("Config exec disabled: skipping command `{command}` in [{section}] {option}");
} else {
tracing::warn!("Executing config command `{command}` for [{section}] {option}");
let output = system_exec(&command);
*value = value.replace(&format!("`{command}`"), &output);
}
}
}
ret
}
pub fn get_list_value(
&self,
section: &str,
option: &str,
default: Option<Vec<String>>,
separator: char,
) -> Option<Vec<String>> {
self.get_value(section, option, None)
.map(|v| v.split(separator).map(|s| s.to_string()).collect())
.or(default)
}
pub fn get_int_value(&self, section: &str, option: &str, default: i64) -> i64 {
self.get_value(section, option, None)
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(default)
}
pub fn get_float_value(&self, section: &str, option: &str, default: f64) -> f64 {
self.get_value(section, option, None)
.and_then(|v| v.trim().parse::<f64>().ok())
.unwrap_or(default)
}
pub fn get_bool_value(&self, section: &str, option: &str, default: bool) -> bool {
match self.get_value(section, option, None) {
Some(v) => parse_bool(&v).unwrap_or(default),
None => default,
}
}
}
/// Parse the true/false flavours accepted by Python's `ConfigParser`.
pub fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_lowercase().as_str() {
"1" | "yes" | "true" | "on" => Some(true),
"0" | "no" | "false" | "off" => Some(false),
_ => None,
}
}
fn parse_ini(content: &str) -> IndexMap<String, IndexMap<String, String>> {
let mut sections: IndexMap<String, IndexMap<String, String>> = IndexMap::new();
let mut current_section: Option<String> = None;
let mut last_key: Option<String> = None;
for raw_line in content.lines() {
let line = raw_line.trim_end();
let trimmed = line.trim_start();
// Blank line: keep the last key for continuation values.
if trimmed.is_empty() {
continue;
}
// Comment.
if trimmed.starts_with('#') || trimmed.starts_with(';') {
continue;
}
// Section header.
if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.starts_with("[[") {
let name = trimmed[1..trimmed.len() - 1].trim().to_string();
sections.entry(name.clone()).or_default();
current_section = Some(name);
last_key = None;
continue;
}
// Continuation line (indented).
if line.starts_with(' ') || line.starts_with('\t') {
if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref()) {
if let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key)) {
value.push('\n');
value.push_str(trimmed);
}
}
continue;
}
// Key/value.
if let Some(pos) = trimmed.find(['=', ':']) {
let key = trimmed[..pos].trim().to_lowercase();
let value = trimmed[pos + 1..].trim();
if let Some(section) = current_section.as_ref() {
sections
.entry(section.clone())
.or_default()
.insert(key.clone(), value.to_string());
last_key = Some(key);
}
}
}
sections
}
fn find_backticks(value: &str) -> Vec<String> {
let mut result = Vec::new();
let mut chars = value.char_indices().peekable();
while let Some((start, c)) = chars.next() {
if c == '`' {
for (end, c2) in chars.by_ref() {
if c2 == '`' {
result.push(value[start + 1..end].to_string());
break;
}
}
}
}
result
}
/// Execute a system command without a shell and return its trimmed stdout.
fn system_exec(command: &str) -> String {
let parts: Vec<&str> = command.split(' ').collect();
let Some((program, args)) = parts.split_first() else {
return String::new();
};
match Command::new(program).args(args).output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).trim_end().to_string(),
Err(e) => format!("ERROR: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ini() {
let config = Config::from_str(
"# comment\n[global]\nrefresh = 5\nhistory_size: 100\n\n[cpu]\nuser_careful=10\n",
false,
);
assert!(config.has_section("global"));
assert_eq!(config.get_int_value("global", "refresh", 2), 5);
assert_eq!(config.get_int_value("global", "history_size", 0), 100);
assert_eq!(config.get_float_value("cpu", "user_careful", 0.0), 10.0);
}
#[test]
fn test_defaults() {
let config = Config::empty(false);
assert_eq!(config.get_float_value("cpu", "user_careful", 0.0), 50.0);
assert_eq!(config.get_float_value("cpu", "user_warning", 0.0), 70.0);
assert_eq!(config.get_float_value("cpu", "user_critical", 0.0), 90.0);
assert_eq!(config.get_float_value("load", "careful", 0.0), 0.7);
assert_eq!(config.get_float_value("load", "critical", 0.0), 5.0);
assert!(config.has_section("quicklook"));
assert!(config.has_section("processlist"));
}
#[test]
fn test_bool_values() {
let config = Config::from_str("[global]\ncheck_update=off\n", false);
assert!(!config.get_bool_value("global", "check_update", true));
let config = Config::from_str("[x]\ndisable=True\n", false);
assert!(config.get_bool_value("x", "disable", false));
}
#[test]
fn test_backtick_exec() {
let config = Config::from_str("[x]\nprefix=`echo hello`\n", false);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello");
let config = Config::from_str("[x]\nprefix=`echo hello`\n", true);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "`echo hello`");
}
}
+328
View File
@@ -0,0 +1,328 @@
//! Events list (port of `glances.events_list` and `glances.event`).
use crate::processes::{processes, sort_stats};
use crate::thresholds::thresholds;
use indexmap::IndexMap;
use serde_json::Value;
use std::sync::Mutex;
use std::sync::OnceLock;
/// Static decision tree for the global alert message.
const TREE: [(&str, &[&str], u32); 9] = [
("EVENTS history", &[], 0),
("High CPU user mode", &["cpu_user"], 2),
("High CPU kernel usage", &["cpu_system"], 2),
("High CPU I/O waiting", &["cpu_iowait"], 2),
("Large CPU stolen time. System running the hypervisor is too busy.", &["cpu_steal"], 2),
("High CPU niced value", &["cpu_niced"], 2),
("System overloaded in the last 5 minutes", &["load"], 2),
("High swap (paging) usage", &["memswap"], 2),
("High memory consumption", &["mem"], 2),
];
/// Parse the decision tree and return the message for the current thresholds.
pub fn build_global_message() -> String {
let guard = thresholds().lock().unwrap();
let current = guard.all();
let mut best_weight = 0u32;
let mut best_min = 0u32;
let mut best_msg = TREE[0].0;
for (msg, names, min) in TREE {
let weight: u32 = names.iter().filter_map(|n| current.get(*n)).map(|t| t.value() as u32).sum();
if weight > best_weight {
best_weight = weight;
best_min = min;
best_msg = msg;
}
}
if best_weight >= best_min { best_msg.to_string() } else { TREE[0].0.to_string() }
}
#[derive(Debug, Clone)]
pub struct Event {
pub begin: f64,
pub end: f64,
pub state: String,
pub r#type: String,
pub min: f64,
pub max: f64,
pub sum: f64,
pub count: i64,
pub avg: f64,
pub top: Vec<String>,
pub desc: String,
pub sort: String,
pub global_msg: String,
top_dict: IndexMap<String, i64>,
}
impl Event {
pub fn is_ongoing(&self) -> bool {
self.end == -1.0
}
pub fn is_finished(&self) -> bool {
self.end != -1.0
}
pub fn to_json(&self) -> Value {
serde_json::json!({
"begin": self.begin,
"end": self.end,
"state": self.state,
"type": self.r#type,
"min": self.min,
"max": self.max,
"sum": self.sum,
"count": self.count,
"avg": self.avg,
"top": self.top,
"desc": self.desc,
"sort": self.sort,
"global": self.global_msg,
})
}
fn update(
&mut self,
state: &str,
value: f64,
sort_key: &str,
proc_list: &[Value],
proc_desc: &str,
global_msg: &str,
) {
self.end = -1.0;
self.min = self.min.min(value);
self.max = self.max.max(value);
self.sum += value;
self.count += 1;
self.avg = self.sum / self.count as f64;
if state == "CRITICAL" {
// Avoid changing from CRITICAL to WARNING.
self.state = state.to_string();
self.sort = sort_key.to_string();
let mut sorted: Vec<Value> = proc_list.to_vec();
sort_stats(&mut sorted, sort_key, "memory_percent", true);
for process in sorted.iter().take(6) {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
*self.top_dict.entry(name).or_insert(0) += 1;
}
let mut counts: Vec<(String, i64)> = self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect();
counts.sort_by(|a, b| b.1.cmp(&a.1));
self.top = counts.into_iter().take(3).map(|(name, _)| name).collect();
} else {
self.top_dict.clear();
}
self.desc = proc_desc.to_string();
self.global_msg = global_msg.to_string();
}
}
#[derive(Debug)]
pub struct EventsList {
pub events: Vec<Event>,
pub max_events: usize,
pub min_duration: f64,
pub min_interval: f64,
}
impl Default for EventsList {
fn default() -> Self {
Self::new(10, 6.0, 6.0)
}
}
impl EventsList {
pub fn new(max_events: usize, min_duration: f64, min_interval: f64) -> Self {
Self { events: Vec::new(), max_events, min_duration, min_interval }
}
pub fn get(&self) -> Vec<Value> {
self.events.iter().map(Event::to_json).collect()
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
fn event_exist(&self, event_time: f64, event_type: &str) -> Option<usize> {
self.events.iter().position(|e| {
(e.is_ongoing() || (event_time - e.end) < self.min_interval) && e.r#type == event_type
})
}
pub fn get_event_sort_key(event_type: &str) -> &'static str {
if event_type.starts_with("MEM") {
"memory_percent"
} else if event_type.starts_with("CPU_IOWAIT") {
"io_counters"
} else {
"cpu_percent"
}
}
pub fn set_process_sort(event_type: &str) {
let mut procs = processes().lock().unwrap();
if procs.auto_sort {
let key = Self::get_event_sort_key(event_type);
procs.set_sort_key(key, true);
procs.auto_sort = true;
}
}
pub fn reset_process_sort() {
let mut procs = processes().lock().unwrap();
if procs.auto_sort {
procs.set_sort_key("auto", true);
}
}
/// Add a new item to the events list.
pub fn add(&mut self, event_state: &str, event_type: &str, event_value: f64, proc_desc: &str) -> usize {
let event_time = chrono::Utc::now().timestamp() as f64;
let global_message = build_global_message();
let proc_list: Vec<Value> = processes().lock().unwrap().get_list(false).to_vec();
match self.event_exist(event_time, event_type) {
None => {
self.create_event(event_time, event_state, event_type, event_value, proc_desc, &global_message);
}
Some(index) => {
self.update_event(
event_time,
index,
event_state,
event_type,
event_value,
&proc_list,
proc_desc,
&global_message,
);
}
}
self.len()
}
fn create_event(
&mut self,
event_time: f64,
event_state: &str,
event_type: &str,
event_value: f64,
proc_desc: &str,
global_message: &str,
) {
if event_state != "WARNING" && event_state != "CRITICAL" {
return;
}
Self::set_process_sort(event_type);
let sort = processes().lock().unwrap().sort_key.clone();
let event = Event {
begin: event_time,
end: -1.0,
state: event_state.to_string(),
r#type: event_type.to_string(),
min: event_value,
max: event_value,
sum: event_value,
count: 1,
avg: event_value,
top: Vec::new(),
desc: proc_desc.to_string(),
sort,
global_msg: global_message.to_string(),
top_dict: IndexMap::new(),
};
self.events.insert(0, event);
if self.len() > self.max_events {
self.events.pop();
}
}
#[allow(clippy::too_many_arguments)]
fn update_event(
&mut self,
event_time: f64,
event_index: usize,
event_state: &str,
event_type: &str,
event_value: f64,
proc_list: &[Value],
proc_desc: &str,
global_message: &str,
) {
let is_ongoing = self.events[event_index].is_ongoing();
if (event_state == "OK" || event_state == "CAREFUL") && is_ongoing {
self.close_event(event_time, event_index);
} else if event_state == "OK" || event_state == "CAREFUL" {
// Event is already closed, do nothing.
} else {
Self::set_process_sort(event_type);
let sort_key = Self::get_event_sort_key(event_type).to_string();
let event = &mut self.events[event_index];
event.update(event_state, event_value, &sort_key, proc_list, proc_desc, global_message);
}
}
fn close_event(&mut self, event_time: f64, event_index: usize) {
Self::reset_process_sort();
if event_time - self.events[event_index].begin >= self.min_duration {
self.events[event_index].end = event_time;
} else {
self.events.remove(event_index);
}
}
/// Clean the events list by deleting finished items.
pub fn clean(&mut self, critical: bool) -> usize {
let mut clean_events = Vec::new();
while let Some(event) = self.events.pop() {
if event.end < 0.0 || (!critical && event.state.starts_with("CRITICAL")) {
clean_events.insert(0, event);
}
}
self.events = clean_events;
self.len()
}
}
/// Global events list shared by all plugins.
pub fn events() -> &'static Mutex<EventsList> {
static EVENTS: OnceLock<Mutex<EventsList>> = OnceLock::new();
EVENTS.get_or_init(|| Mutex::new(EventsList::default()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_events_add() {
let mut events = EventsList::new(10, 0.0, 6.0);
events.add("WARNING", "CPU", 42.0, "");
assert_eq!(events.len(), 1);
let json = events.get();
assert_eq!(json[0]["type"], "CPU");
assert_eq!(json[0]["state"], "WARNING");
}
#[test]
fn test_clean() {
let mut events = EventsList::new(10, 0.0, 6.0);
events.add("WARNING", "CPU", 42.0, "");
events.add("CRITICAL", "MEM", 80.0, "");
// Close both events.
events.events[0].end = 100.0;
events.events[1].end = 100.0;
// clean(false) removes the finished WARNING but keeps CRITICAL.
assert_eq!(events.clean(false), 1);
assert_eq!(events.get()[0]["state"], "CRITICAL");
assert_eq!(events.clean(true), 0);
}
}
+154
View File
@@ -0,0 +1,154 @@
//! Process filtering (port of `glances.filter`).
use regex::Regex;
use serde_json::Value;
#[derive(Debug, Clone, Default)]
pub struct Filter {
filter_input: Option<String>,
filter: Option<String>,
filter_re: Option<Regex>,
filter_key: Option<String>,
}
impl Filter {
pub fn filter_input(&self) -> Option<&str> {
self.filter_input.as_deref()
}
pub fn filter(&self) -> Option<&str> {
self.filter.as_deref()
}
pub fn filter_key(&self) -> Option<&str> {
self.filter_key.as_deref()
}
/// Set the filter (as a string) and compile the regular expression.
pub fn set_filter(&mut self, value: Option<&str>) {
self.filter_input = value.map(|v| v.to_string());
match value {
None => {
self.filter = None;
self.filter_key = None;
}
Some(value) => {
// Split once: everything after the first colon is the regex.
match value.split_once(':') {
Some((key, rest)) => {
self.filter = Some(rest.to_string());
self.filter_key = Some(key.to_string());
}
None => {
self.filter = Some(value.to_string());
self.filter_key = None;
}
}
}
}
self.filter_re = None;
if let Some(filter) = &self.filter {
tracing::debug!(
"Set filter to {} on {}",
filter,
self.filter_key.as_deref().unwrap_or("name or cmdline")
);
match Regex::new(filter) {
Ok(re) => self.filter_re = Some(re),
Err(e) => {
tracing::error!("Cannot compile filter regex: {filter} ({e})");
self.filter = None;
self.filter_re = None;
self.filter_key = None;
}
}
}
}
/// Return True if the process item matches the current filter.
pub fn is_filtered(&self, process: &Value) -> bool {
let Some(filter_re) = self.filter_re.as_ref() else {
return false;
};
if self.filter.is_none() {
return false;
}
if self.filter_key.is_none() {
return self.filter_matches(process, "name", filter_re) || self.filter_matches(process, "cmdline", filter_re);
}
self.filter_matches(process, self.filter_key.as_deref().unwrap_or("name"), filter_re)
}
fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool {
let Some(value) = process.get(key) else {
return false;
};
let text = match value {
Value::String(s) => s.clone(),
Value::Array(items) => {
if key == "cmdline" && !items.is_empty() {
items[0].as_str().unwrap_or_default().to_string()
} else {
items.iter().map(|i| i.as_str().unwrap_or_default()).collect::<Vec<_>>().join(" ")
}
}
Value::Null => return false,
other => crate::units::py_str(other),
};
filter_re.is_match(&text) && filter_re.find(&text).map(|m| m.len() == text.len()).unwrap_or(false)
}
}
#[derive(Debug, Clone, Default)]
pub struct FilterList {
filters: Vec<Filter>,
}
impl FilterList {
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
pub fn filters(&self) -> &[Filter] {
&self.filters
}
/// Set the filter list from a comma separated list of filters.
pub fn set_filter(&mut self, value: Option<&str>) {
self.filters.clear();
if let Some(value) = value {
for f in value.split(',') {
let mut filter = Filter::default();
filter.set_filter(Some(f));
self.filters.push(filter);
}
}
}
/// Return True if the process is filtered by at least one filter.
pub fn is_filtered(&self, process: &Value) -> bool {
self.filters.iter().any(|f| f.is_filtered(process))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_filter_by_name() {
let mut f = Filter::default();
f.set_filter(Some(".*python.*"));
assert!(f.is_filtered(&json!({"name": "python3", "cmdline": ["python3"]})));
assert!(!f.is_filtered(&json!({"name": "bash", "cmdline": ["bash"]})));
}
#[test]
fn test_filter_by_key() {
let mut f = Filter::default();
f.set_filter(Some("username:root"));
assert!(f.is_filtered(&json!({"name": "x", "username": "root"})));
assert!(!f.is_filtered(&json!({"name": "x", "username": "bob"})));
}
}
+110
View File
@@ -0,0 +1,110 @@
//! Stats history (port of `glances.attribute` / `glances.history`).
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use std::collections::VecDeque;
/// One historized attribute: a ring buffer of `(date, value)` tuples.
#[derive(Debug, Clone)]
pub struct Attribute {
pub name: String,
pub description: String,
pub value: Option<f64>,
pub history: VecDeque<(DateTime<Utc>, f64)>,
pub history_max_size: Option<usize>,
}
impl Attribute {
pub fn new(name: impl Into<String>, description: impl Into<String>, history_max_size: Option<usize>) -> Self {
Self {
name: name.into(),
description: description.into(),
value: None,
history: VecDeque::new(),
history_max_size,
}
}
pub fn set_value(&mut self, value: f64) {
self.value = Some(value);
let now = Utc::now();
if let Some(max) = self.history_max_size {
if self.history.len() >= max {
self.history.pop_front();
}
self.history.push_back((now, value));
}
}
pub fn history_reset(&mut self) {
self.history.clear();
}
pub fn history_raw(&self, nb: usize) -> Vec<(DateTime<Utc>, f64)> {
if nb == 0 {
self.history.iter().cloned().collect()
} else {
self.history.iter().rev().take(nb).collect::<Vec<_>>().into_iter().rev().cloned().collect()
}
}
pub fn history_json(&self, nb: usize) -> Vec<(String, f64)> {
self.history_raw(nb)
.into_iter()
.map(|(date, value)| (date.to_rfc3339(), value))
.collect()
}
}
/// A dict of historized attributes.
#[derive(Debug, Clone, Default)]
pub struct History {
pub attributes: IndexMap<String, Attribute>,
}
impl History {
pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option<usize>) {
let attribute = self
.attributes
.entry(key.to_string())
.or_insert_with(|| Attribute::new(key, description, history_max_size));
attribute.set_value(value);
}
pub fn reset(&mut self) {
for attribute in self.attributes.values_mut() {
attribute.history_reset();
}
}
pub fn get(&self, nb: usize) -> IndexMap<String, Vec<(DateTime<Utc>, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_raw(nb))).collect()
}
pub fn get_json(&self, nb: usize) -> IndexMap<String, Vec<(String, f64)>> {
self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_json(nb))).collect()
}
/// Return the values of one item (used for TUI trends).
pub fn item_values(&self, item: &str) -> Option<Vec<f64>> {
self.attributes.get(item).map(|a| a.history.iter().map(|(_, v)| *v).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_history_ring_buffer() {
let mut history = History::default();
for i in 0..5 {
history.add("x", i as f64, "", Some(3));
}
let attr = history.attributes.get("x").unwrap();
assert_eq!(attr.history.len(), 3);
assert_eq!(attr.value, Some(4.0));
let raw = history.get(0);
assert_eq!(raw["x"].len(), 3);
}
}
+29
View File
@@ -0,0 +1,29 @@
//! glances-core: shared foundations of the glances-rs rewrite.
//!
//! This crate contains the pieces that are common to every plugin and output:
//! argument parsing, configuration file handling, the plugin model (limits,
//! views, history, rates...), formatting helpers, the event/threshold/action
//! machinery and filtering.
pub mod actions;
pub mod args;
pub mod config;
pub mod events;
pub mod filter;
pub mod history;
pub mod plugin;
pub mod processes;
pub mod secure;
pub mod state;
pub mod thresholds;
pub mod timer;
pub mod units;
pub use args::Args;
pub use config::Config;
pub use plugin::{
Align, Decoration, Field, HistoryItem, Limit, Plugin, PluginModel, RenderContext, StatLine,
UpdateContext,
};
pub use state::Runtime;
pub use units::{auto_unit, py_repr, py_str, to_fahrenheit};
File diff suppressed because it is too large Load Diff
+719
View File
@@ -0,0 +1,719 @@
//! Process collection and management (port of `glances.processes`).
//!
//! The process list is shared between the processlist, processcount,
//! programlist, amps plugins and the event/action machinery, exactly like the
//! `glances_processes` global in the Python implementation.
use crate::filter::{Filter, FilterList};
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
use sysinfo::{ProcessesToUpdate, System, Users};
/// Mandatory process stats which can not be disabled by the user.
pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"];
/// Available process sort keys.
pub const SORT_PROCESSES_STATS: [&str; 7] =
["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
/// Sort keys as displayed to humans.
pub fn sort_for_human(key: &str) -> &str {
match key {
"io_counters" => "disk IO",
"cpu_percent" => "CPU consumption",
"memory_percent" => "memory consumption",
"cpu_times" => "process time",
"username" => "user name",
"name" => "processs name",
"cpu_num" => "CPU core number",
"auto" => "auto",
_ => "None",
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcessCount {
pub total: i64,
pub running: i64,
pub sleeping: i64,
pub thread: i64,
pub pid_max: Option<i64>,
}
#[derive(Debug)]
pub struct Processes {
system: System,
users: Users,
users_timer: f64,
pub processlist: Vec<Value>,
pub processlist_export: Vec<Value>,
pub processcount: ProcessCount,
pub sort_key: String,
pub auto_sort: bool,
pub sort_reverse: bool,
pub disable_tag: bool,
pub disable_extended_tag: bool,
pub extended_process_pid: Option<i64>,
pub extended_stats: Option<Value>,
pub no_kernel_threads: bool,
pub disable_io_counters: bool,
pub disable_gids: bool,
pub disable_cpu_num: bool,
pub disable_stats: Vec<String>,
/// List of processes to focus on.
pub filter_focus: FilterList,
/// Process filter (regular expression).
pub filter: Filter,
/// List of processes stats to export.
pub filter_export: FilterList,
pub max_values: HashMap<String, f64>,
io_old: HashMap<i64, [u64; 2]>,
programs: Vec<Value>,
programs_valid: bool,
}
impl Default for Processes {
fn default() -> Self {
Self::new()
}
}
impl Processes {
pub fn new() -> Self {
let system = System::new();
let users = Users::new_with_refreshed_list();
let mut p = Self {
system,
users,
users_timer: 0.0,
processlist: Vec::new(),
processlist_export: Vec::new(),
processcount: ProcessCount::default(),
sort_key: "cpu_percent".to_string(),
auto_sort: true,
sort_reverse: true,
disable_tag: false,
disable_extended_tag: false,
extended_process_pid: None,
extended_stats: None,
no_kernel_threads: false,
disable_io_counters: false,
disable_gids: false,
disable_cpu_num: false,
disable_stats: Vec::new(),
filter_focus: FilterList::default(),
filter: Filter::default(),
filter_export: FilterList::default(),
max_values: HashMap::new(),
io_old: HashMap::new(),
programs: Vec::new(),
programs_valid: false,
};
p.reset_max_values();
p
}
pub fn reset_max_values(&mut self) {
self.max_values.clear();
self.max_values.insert("cpu_percent".to_string(), 0.0);
self.max_values.insert("memory_percent".to_string(), 0.0);
}
pub fn set_sort_key(&mut self, key: &str, auto: bool) {
if key == "auto" {
self.auto_sort = true;
self.sort_key = "cpu_percent".to_string();
} else {
self.auto_sort = auto;
self.sort_key = key.to_string();
}
self.sort_reverse = !matches!(self.sort_key.as_str(), "name" | "username" | "cpu_num");
}
pub fn enable(&mut self) {
self.disable_tag = false;
}
pub fn disable(&mut self) {
self.disable_tag = true;
}
pub fn enable_extended(&mut self) {
self.disable_extended_tag = false;
}
pub fn disable_extended(&mut self) {
self.disable_extended_tag = true;
}
pub fn pid_max(&self) -> Option<i64> {
std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::<i64>().ok())
}
pub fn processes_count(&self, max_processes: Option<i64>) -> i64 {
match max_processes {
Some(max) => (max - 2).min(self.processcount.total - 1),
None => 0,
}
}
/// Update the processes stats.
pub fn update(&mut self, cursor_position: i64, args: &crate::args::Args) -> &[Value] {
if self.disable_tag {
return &[];
}
self.no_kernel_threads = args.no_kernel_threads;
let time_since_update = crate::timer::get_time_since_last_update("process_disk");
// Refresh the user list at most every 60 seconds.
if self.users_timer <= 0.0 {
self.users = Users::new_with_refreshed_list();
self.users_timer = 60.0;
}
self.users_timer -= 1.0;
self.system.refresh_memory();
self.system.refresh_processes(ProcessesToUpdate::All, true);
let total_mem = self.system.total_memory().max(1) as f64;
let mut processlist: Vec<Value> = Vec::with_capacity(self.system.processes().len());
for (pid, process) in self.system.processes() {
// Skip threads: the process list shows processes only.
if process.thread_kind().is_some() {
continue;
}
let pid = pid.as_u32() as i64;
if self.no_kernel_threads && process.parent().map(|p| p.as_u32()) == Some(2) {
continue;
}
let stat = read_proc_stat(pid);
let name = process.name().to_string_lossy().to_string();
let cmdline: Vec<String> =
process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect();
let username = process
.user_id()
.and_then(|uid| self.users.get_user_by_id(uid))
.map(|u| u.name().to_string())
.unwrap_or_default();
let status = stat.as_ref().map(|s| s.state.clone()).unwrap_or_else(|| "?".to_string());
let nice = stat.as_ref().map(|s| s.nice).unwrap_or(0);
let num_threads = stat.as_ref().map(|s| s.num_threads).unwrap_or(0);
let cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0);
let (user_time, system_time) =
stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0));
let rss = process.memory() as f64;
let vms = process.virtual_memory() as f64;
let memory_percent = rss * 100.0 / total_mem;
let mut proc = Map::new();
proc.insert("pid".to_string(), json!(pid));
proc.insert("name".to_string(), json!(name));
proc.insert("cmdline".to_string(), json!(cmdline));
proc.insert("username".to_string(), json!(username));
proc.insert("num_threads".to_string(), json!(num_threads));
proc.insert("cpu_percent".to_string(), json!(process.cpu_usage() as f64));
proc.insert("memory_percent".to_string(), json!(memory_percent));
proc.insert("memory_info".to_string(), json!({"rss": rss, "vms": vms}));
proc.insert("status".to_string(), json!(status));
proc.insert("nice".to_string(), json!(nice));
proc.insert(
"cpu_times".to_string(),
json!({"user": user_time, "system": system_time, "iowait": 0.0}),
);
proc.insert("cpu_num".to_string(), json!(cpu_num));
// IO counters: [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
let (read_bytes, write_bytes) = match &stat {
Some(stat) => (stat.read_bytes, stat.write_bytes),
None => {
let disk = process.disk_usage();
(disk.total_read_bytes, disk.total_written_bytes)
}
};
let (io_tag, io_old) = match self.io_old.get(&pid) {
Some(old) => (1, *old),
None => (0, [0, 0]),
};
self.io_old.insert(pid, [read_bytes, write_bytes]);
proc.insert(
"io_counters".to_string(),
json!([read_bytes, write_bytes, io_old[0], io_old[1], io_tag]),
);
// GIDs
if let Some(stat) = &stat {
proc.insert(
"gids".to_string(),
json!({"real": stat.gid_real, "effective": stat.gid_effective, "saved": stat.gid_saved}),
);
}
// Metadata
proc.insert("key".to_string(), json!("pid"));
proc.insert("time_since_update".to_string(), json!(time_since_update));
processlist.push(Value::Object(proc));
}
// Sort processes by the current sort key.
sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse);
self.update_processcount(&processlist);
// Extended stats for the selected process.
for (position, proc) in processlist.iter_mut().enumerate() {
let pid = proc.get("pid").and_then(Value::as_i64).unwrap_or(0);
if args.enable_process_extended
&& !args.programs
&& !self.disable_extended_tag
&& position as i64 == cursor_position
&& !args.disable_cursor
{
self.extended_process_pid = Some(pid);
}
if let Some(target) = self.extended_process_pid {
if target == pid {
let extended = self.set_extended_stats(pid, proc);
if let Value::Object(map) = extended {
if let Some(obj) = proc.as_object_mut() {
for (k, v) in map {
obj.insert(k, v);
}
}
}
}
}
}
// Remove non running processes from the IO cache.
let running: std::collections::HashSet<i64> =
processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect();
self.io_old.retain(|pid, _| running.contains(pid));
// Export list.
self.processlist_export = self.update_export_list(&processlist);
// Filtered process list.
let filtered = self.update_list(&processlist);
self.programs_valid = false;
// Compute the maximum values for CPU and MEM.
for key in ["cpu_percent", "memory_percent"] {
if self.disable_stats.iter().any(|s| s == key) {
continue;
}
let max = filtered
.iter()
.filter_map(|p| p.get(key).and_then(Value::as_f64))
.fold(f64::NEG_INFINITY, f64::max);
if max.is_finite() {
self.max_values.insert(key.to_string(), max);
}
}
self.processlist = filtered;
&self.processlist
}
fn set_extended_stats(&mut self, pid: i64, proc: &Value) -> Value {
let mut ret = Map::new();
// CPU and MEM min/max/mean
let previous = self.extended_stats.clone();
let prefix = |ret: &mut Map<String, Value>,
stat_prefix: &str,
current: f64,
previous: Option<&Value>| {
let prev = previous.and_then(|p| p.as_object());
let min_key = format!("{stat_prefix}_min");
let max_key = format!("{stat_prefix}_max");
let mean_sum_key = format!("{stat_prefix}_mean_sum");
let mean_counter_key = format!("{stat_prefix}_mean_counter");
let prev_min = prev.and_then(|p| p.get(&min_key)).and_then(Value::as_f64);
let prev_max = prev.and_then(|p| p.get(&max_key)).and_then(Value::as_f64);
let prev_sum = prev.and_then(|p| p.get(&mean_sum_key)).and_then(Value::as_f64);
let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64);
let min = prev_min.map(|m| m.min(current)).unwrap_or(current);
let max = prev_max.map(|m| m.max(current)).unwrap_or(current);
let sum = prev_sum.map(|s| s + current).unwrap_or(current);
let count = prev_count.map(|c| c + 1).unwrap_or(1);
ret.insert(min_key, json!(min));
ret.insert(max_key, json!(max));
ret.insert(mean_sum_key, json!(sum));
ret.insert(mean_counter_key, json!(count));
ret.insert(format!("{stat_prefix}_mean"), json!(sum / count as f64));
};
let cpu_percent = proc.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
prefix(&mut ret, "cpu", cpu_percent, previous.as_ref());
prefix(&mut ret, "memory", memory_percent, previous.as_ref());
ret.insert("extended_stats".to_string(), json!(true));
let extended = Value::Object(ret);
self.extended_stats = Some(extended.clone());
self.extended_process_pid = Some(pid);
extended
}
pub fn update_processcount(&mut self, processlist: &[Value]) {
self.processcount.pid_max = self.pid_max();
self.processcount.running = processlist
.iter()
.filter(|p| p.get("status").and_then(Value::as_str) == Some("R"))
.count() as i64;
self.processcount.sleeping = processlist
.iter()
.filter(|p| p.get("status").and_then(Value::as_str) == Some("S"))
.count() as i64;
self.processcount.thread = processlist
.iter()
.filter_map(|p| p.get("num_threads").and_then(Value::as_i64))
.sum();
self.processcount.total = processlist.len() as i64;
}
fn update_list(&self, processlist: &[Value]) -> Vec<Value> {
if !self.filter_focus.is_empty() {
return processlist.iter().filter(|p| self.filter_focus.is_filtered(p)).cloned().collect();
}
if self.filter.filter().is_none() {
return processlist.to_vec();
}
processlist.iter().filter(|p| self.filter.is_filtered(p)).cloned().collect()
}
fn update_export_list(&self, processlist: &[Value]) -> Vec<Value> {
if self.filter_export.is_empty() {
return Vec::new();
}
processlist.iter().filter(|p| self.filter_export.is_filtered(p)).cloned().collect()
}
/// Get the process list (sorted or not).
pub fn get_list(&self, sorted: bool) -> &[Value] {
let _ = sorted; // the list is always kept sorted after update()
&self.processlist
}
/// Get the process list as programs (aggregated by name).
pub fn get_list_as_programs(&mut self) -> &[Value] {
if !self.programs_valid {
self.programs = processes_to_programs(&self.processlist);
self.programs_valid = true;
}
&self.programs
}
pub fn get_export(&self) -> &[Value] {
&self.processlist_export
}
pub fn get_stats(&self, pid: i64) -> Option<&Value> {
self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid))
}
/// Decrease the nice level of a process (higher priority).
pub fn nice_decrease(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32;
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current - 1) } == 0 {
tracing::info!("Set nice level of process {pid} to {} (higher the priority)", current - 1);
} else {
tracing::warn!("Can not decrease (higher the priority) the nice level of process {pid} (access denied)");
}
}
/// Increase the nice level of a process (lower priority).
pub fn nice_increase(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32;
if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current + 1) } == 0 {
tracing::info!("Set nice level of process {pid} to {} (lower the priority)", current + 1);
} else {
tracing::warn!("Can not increase (lower the priority) the nice level of process {pid} (access denied)");
}
}
/// Kill a process.
pub fn kill(&self, pid: i64) {
assert!(pid != std::process::id() as i64, "Glances can kill itself...");
tracing::debug!("Send kill signal to process: {pid}");
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGKILL);
}
}
}
/// The global process manager (equivalent of `glances_processes`).
pub fn processes() -> &'static Mutex<Processes> {
static PROCESSES: OnceLock<Mutex<Processes>> = OnceLock::new();
PROCESSES.get_or_init(|| Mutex::new(Processes::new()))
}
fn weighted(value: Option<f64>) -> f64 {
value.unwrap_or(f64::NEG_INFINITY)
}
/// Sort a list of process dicts by the given key.
pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &str, reverse: bool) {
let key_of = |process: &Value, key: &str| -> f64 {
match key {
"io_counters" => {
// [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
let counters = process.get("io_counters").and_then(Value::as_array);
let read = counters.and_then(|c| c.first()).and_then(Value::as_f64).unwrap_or(0.0);
let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0);
let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0);
let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0);
read - read_old + write - write_old
}
"cpu_times" => {
let times = process.get("cpu_times");
let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0);
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0);
user + system
}
_ => process.get(key).and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY),
}
};
let string_key = |process: &Value, key: &str| -> String {
process.get(key).and_then(Value::as_str).unwrap_or("~").to_string()
};
stats.sort_by(|a, b| {
let (primary, secondary) = match sorted_by {
"name" | "username" => {
let cmp = string_key(a, sorted_by).cmp(&string_key(b, sorted_by));
let ord = if reverse { cmp.reverse() } else { cmp };
return ord;
}
"cpu_num" => {
let av = a.get("cpu_num").and_then(Value::as_i64).unwrap_or(0);
let bv = b.get("cpu_num").and_then(Value::as_i64).unwrap_or(0);
let ord = av.cmp(&bv);
return if reverse { ord.reverse() } else { ord };
}
_ => (key_of(a, sorted_by), key_of(b, sorted_by)),
};
let ord = weighted(Some(primary))
.partial_cmp(&weighted(Some(secondary)))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
weighted(Some(key_of(a, sorted_by_secondary)))
.partial_cmp(&weighted(Some(key_of(b, sorted_by_secondary))))
.unwrap_or(std::cmp::Ordering::Equal)
});
if reverse { ord.reverse() } else { ord }
});
}
/// Aggregate processes by program name (port of `processes_to_programs`).
pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
let no_io = [0u64; 5];
let mut programs: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
for process in processlist {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string();
let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0);
let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({}));
let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({}));
let io_counters: Vec<u64> = process
.get("io_counters")
.and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec());
let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0);
let username = process.get("username").and_then(Value::as_str).unwrap_or("_").to_string();
let nice = process.get("nice").and_then(Value::as_i64).unwrap_or(0);
let status = process.get("status").and_then(Value::as_str).unwrap_or("?").to_string();
let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null);
match programs.get_mut(&name) {
None => {
programs.insert(
name.clone(),
json!({
"time_since_update": time_since_update,
"num_threads": num_threads,
"cpu_percent": cpu_percent,
"memory_percent": memory_percent,
"cpu_times": cpu_times,
"memory_info": memory_info,
"io_counters": io_counters,
"childrens": [pid],
"name": name,
"cmdline": [name],
"pid": "_",
"username": username,
"nice": nice,
"status": status,
}),
);
}
Some(program) => {
let Some(program) = program.as_object_mut() else {
continue;
};
let add = |program: &mut Map<String, Value>, key: &str, value: f64| {
let current = program.get(key).and_then(Value::as_f64).unwrap_or(0.0);
program.insert(key.to_string(), json!(current + value));
};
add(program, "num_threads", num_threads);
add(program, "cpu_percent", cpu_percent);
add(program, "memory_percent", memory_percent);
for key in ["user", "system", "iowait"] {
let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
if value != 0.0 || cpu_times.get(key).is_some() {
if let Some(times) = program.get_mut("cpu_times").and_then(Value::as_object_mut) {
let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0);
times.insert(key.to_string(), json!(current + value));
}
}
}
if let Some(program_memory) = program.get_mut("memory_info").and_then(Value::as_object_mut) {
if let Some(addition) = memory_info.as_object() {
for (key, value) in addition {
let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0);
let value = value.as_f64().unwrap_or(0.0);
program_memory.insert(key.clone(), json!(current + value));
}
}
}
// io_counters: sum slots 0..4, OR the tag.
let mut summed: Vec<u64> = vec![0; 5];
let existing: Vec<u64> = program
.get("io_counters")
.and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec());
for index in 0..4 {
summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0);
}
summed[4] = existing.get(4).copied().unwrap_or(0).max(io_counters.get(4).copied().unwrap_or(0));
program.insert("io_counters".to_string(), json!(summed));
if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) {
children.push(json!(pid));
}
let same = |program: &Map<String, Value>, key: &str, value: &str| {
program.get(key).and_then(Value::as_str) == Some(value)
};
if !same(program, "username", &username) {
program.insert("username".to_string(), json!("_"));
}
if program.get("nice").and_then(Value::as_i64) != Some(nice) {
program.insert("nice".to_string(), json!("_"));
}
if !same(program, "status", &status) {
program.insert("status".to_string(), json!("_"));
}
}
}
}
programs
.into_values()
.map(|mut program| {
if let Some(program) = program.as_object_mut() {
let nprocs = program.get("childrens").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0);
program.insert("nprocs".to_string(), json!(nprocs));
}
program
})
.collect()
}
/// A few fields grabbed straight from `/proc/<pid>/stat` and `/proc/<pid>/io`.
#[derive(Debug, Clone, Default)]
struct ProcStat {
state: String,
nice: i64,
num_threads: i64,
processor: i64,
utime: f64,
stime: f64,
read_bytes: u64,
write_bytes: u64,
gid_real: i64,
gid_effective: i64,
gid_saved: i64,
}
fn read_proc_stat(pid: i64) -> Option<ProcStat> {
let content = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
// The process name may contain spaces and parenthesis: strip it.
let close = content.rfind(')')?;
let name = &content[..=close];
let fields: Vec<&str> = content[close + 1..].split_whitespace().collect();
// fields[0] = state (stat field 3), so stat field N (1-based) is fields[N-3].
let field = |n: usize| fields.get(n).copied().unwrap_or("");
let state = field(0).chars().next().unwrap_or('?').to_ascii_uppercase().to_string();
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
let ticks = if ticks <= 0.0 { 100.0 } else { ticks };
let mut stat = ProcStat {
state,
nice: field(16).parse().unwrap_or(0),
num_threads: field(17).parse().unwrap_or(0),
processor: field(36).parse().unwrap_or(0),
utime: field(11).parse::<f64>().unwrap_or(0.0) / ticks,
stime: field(12).parse::<f64>().unwrap_or(0.0) / ticks,
..Default::default()
};
let _ = name;
if let Ok(io) = std::fs::read_to_string(format!("/proc/{pid}/io")) {
for line in io.lines() {
if let Some(value) = line.strip_prefix("read_bytes:") {
stat.read_bytes = value.trim().parse().unwrap_or(0);
} else if let Some(value) = line.strip_prefix("write_bytes:") {
stat.write_bytes = value.trim().parse().unwrap_or(0);
}
}
}
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) {
if let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) {
let gids: Vec<i64> = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect();
stat.gid_real = gids.first().copied().unwrap_or(0);
stat.gid_effective = gids.get(1).copied().unwrap_or(0);
stat.gid_saved = gids.get(2).copied().unwrap_or(0);
}
}
Some(stat)
}
fn read_nice(pid: i64) -> Option<i64> {
read_proc_stat(pid).map(|s| s.nice)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_processes_update() {
use clap::Parser;
let args = crate::args::Args::parse_from(["glances"]);
let mut processes = Processes::new();
let list = processes.update(0, &args).to_vec();
assert!(!list.is_empty());
assert!(list[0].get("pid").is_some());
assert!(list[0].get("name").is_some());
}
#[test]
fn test_sort_stats() {
let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})];
sort_stats(&mut stats, "cpu_percent", "memory_percent", true);
assert_eq!(stats[0]["name"], "b");
}
}
+203
View File
@@ -0,0 +1,203 @@
//! Secure shell helpers (port of `glances.secure`).
//!
//! By default the shell-like operators `&&`, `|` and `>` are interpreted.
//! When `allow_operators` is false the command is run as a single process and
//! the operators are passed verbatim. `render` is applied to every argument
//! *after* the command line has been tokenized so that untrusted values can
//! not change the argument boundaries.
use std::io::Write;
use std::process::{Command, Stdio};
const OPERATORS: [&str; 5] = ["&&", "|", ">>", ">", "&"];
/// Replace shell operators by spaces, recursing into containers.
pub fn sanitize_value(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::String(s) => {
let mut s = s.clone();
for op in OPERATORS {
s = s.replace(op, " ");
}
serde_json::Value::String(s)
}
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(sanitize_value).collect())
}
serde_json::Value::Object(map) => {
serde_json::Value::Object(map.iter().map(|(k, v)| (k.clone(), sanitize_value(v))).collect())
}
other => other.clone(),
}
}
/// Split a command string into an argument list. Spaces are separators except
/// within single or double quotes (the surrounding quotes are removed).
pub fn split_args(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> Result<Vec<String>, String> {
let mut args: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_single = false;
let mut in_double = false;
let mut has_content = false;
for c in cmd.chars() {
if in_single {
if c == '\'' {
in_single = false;
} else {
current.push(c);
}
continue;
}
if in_double {
if c == '"' {
in_double = false;
} else {
current.push(c);
}
continue;
}
match c {
'\'' => {
in_single = true;
has_content = true;
}
'"' => {
in_double = true;
has_content = true;
}
c if c.is_whitespace() => {
if has_content {
args.push(std::mem::take(&mut current));
has_content = false;
}
}
c => {
current.push(c);
has_content = true;
}
}
}
if has_content {
args.push(current);
}
match render {
None => Ok(args),
Some(render) => args.into_iter().map(|arg| render(&arg)).collect(),
}
}
fn run_argv(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
let args = match split_args(cmd, render) {
Ok(args) => args,
Err(e) => return format!("Glances error: {e}"),
};
if args.is_empty() {
return String::new();
}
let mut command = Command::new(&args[0]);
command.args(&args[1..]);
match command.output() {
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if stderr.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
stderr
}
}
Err(e) => format!("ERROR: {e}"),
}
}
fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
let mut cmd = cmd;
let mut stdout_redirect: Option<String> = None;
let mut redirect_split = cmd.splitn(3, '>');
let first = redirect_split.next().unwrap_or_default();
let second = redirect_split.next();
let third = redirect_split.next();
if third.is_some() {
return format!("Glances error: Only one file redirection allowed ({cmd})");
}
if let Some(target) = second {
let target = target.trim();
let target = match render {
Some(render) => match render(target) {
Ok(t) => t,
Err(e) => return format!("Glances error: {e}"),
},
None => target.to_string(),
};
stdout_redirect = Some(target);
cmd = first;
}
// Tokenize (and render) every stage before spawning anything.
let mut stages: Vec<Vec<String>> = Vec::new();
for sub_cmd in cmd.split('|') {
match split_args(sub_cmd, render) {
Ok(args) => stages.push(args),
Err(e) => return format!("Glances error: {e}"),
}
}
if stages.is_empty() || stages.iter().any(|s| s.is_empty()) {
return String::new();
}
let mut children: Vec<std::process::Child> = Vec::new();
let mut previous_stdout: Option<std::process::ChildStdout> = None;
for args in &stages {
let mut command = Command::new(&args[0]);
command.args(&args[1..]).stderr(Stdio::piped());
match previous_stdout.take() {
Some(stdout) => {
command.stdin(Stdio::from(stdout));
}
None => {
command.stdin(Stdio::null());
}
}
let mut child = match command.stdout(Stdio::piped()).spawn() {
Ok(child) => child,
Err(e) => return format!("ERROR: {e}"),
};
previous_stdout = child.stdout.take();
children.push(child);
}
let last = children.pop().expect("at least one child");
let output = match last.wait_with_output() {
Ok(output) => output,
Err(e) => return format!("ERROR: {e}"),
};
for mut child in children {
let _ = child.wait();
}
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !stderr.is_empty() {
return stderr;
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(target) = stdout_redirect {
if let Ok(mut file) = std::fs::File::create(&target) {
let _ = file.write_all(stdout.as_bytes());
}
}
stdout
}
/// Run a command line, interpreting (or not) the shell operators.
pub fn secure_popen(cmd: &str, allow_operators: bool, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String {
if !allow_operators {
return run_argv(cmd, render);
}
let mut ret = String::new();
for part in cmd.split("&&") {
ret.push_str(&secure_popen_one(part, render));
}
ret
}
+58
View File
@@ -0,0 +1,58 @@
//! Mutable runtime state (hotkey toggles and shared UI state).
/// Runtime state shared between the engine, the plugins and the TUI.
#[derive(Debug, Clone)]
pub struct Runtime {
/// Display Rx and Tx sum instead of per interface values.
pub network_sum: bool,
/// Display cumulative network values instead of rates.
pub network_cumul: bool,
/// Per CPU mode.
pub percpu: bool,
/// Mean GPU mode.
pub meangpu: bool,
/// Accumulate processes by program.
pub programs: bool,
/// Force short process names.
pub process_short_name: bool,
/// Generate graph (influxdb export in Python; kept for compatibility).
pub generate_graph: bool,
/// Help screen tag.
pub help_tag: bool,
/// DiskIO: show IOPS instead of bytes.
pub diskio_iops: bool,
/// DiskIO: show latency.
pub diskio_latency: bool,
/// FS: show free space instead of used.
pub fs_free_space: bool,
/// Sparklines instead of bars.
pub sparkline: bool,
/// Cursor position in the process list.
pub cursor_position: i64,
/// Maximum number of processes displayed.
pub max_processes: Option<i64>,
/// Reset min/max tag (TUI 'M' key).
pub reset_minmax_tag: bool,
}
impl Runtime {
pub fn new(args: &crate::args::Args, fs_free_space_config: bool) -> Self {
Self {
network_sum: false,
network_cumul: false,
percpu: args.percpu,
meangpu: args.meangpu,
programs: args.programs,
process_short_name: args.process_short_name(),
generate_graph: false,
help_tag: false,
diskio_iops: args.diskio_iops,
diskio_latency: args.diskio_latency,
fs_free_space: args.fs_free_space || fs_free_space_config,
sparkline: args.sparkline,
cursor_position: 0,
max_processes: None,
reset_minmax_tag: false,
}
}
}
+78
View File
@@ -0,0 +1,78 @@
//! Thresholds (OK, CAREFUL, WARNING, CRITICAL).
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Threshold {
Ok,
Careful,
Warning,
Critical,
}
impl Threshold {
pub fn from_str(name: &str) -> Option<Self> {
match name {
"OK" => Some(Threshold::Ok),
"CAREFUL" => Some(Threshold::Careful),
"WARNING" => Some(Threshold::Warning),
"CRITICAL" => Some(Threshold::Critical),
_ => None,
}
}
pub fn value(self) -> u8 {
match self {
Threshold::Ok => 0,
Threshold::Careful => 1,
Threshold::Warning => 2,
Threshold::Critical => 3,
}
}
pub fn description(self) -> &'static str {
match self {
Threshold::Ok => "OK",
Threshold::Careful => "CAREFUL",
Threshold::Warning => "WARNING",
Threshold::Critical => "CRITICAL",
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Thresholds {
map: HashMap<String, Threshold>,
}
impl Thresholds {
pub fn get(&self, stat_name: &str) -> Option<Threshold> {
self.map.get(stat_name).copied()
}
pub fn all(&self) -> &HashMap<String, Threshold> {
&self.map
}
pub fn add(&mut self, stat_name: &str, threshold: &str) -> bool {
match Threshold::from_str(threshold) {
Some(t) => {
self.map.insert(stat_name.to_string(), t);
true
}
None => false,
}
}
pub fn reset(&mut self) {
self.map.clear();
}
}
/// Global thresholds registry shared by all plugins.
pub fn thresholds() -> &'static Mutex<Thresholds> {
static THRESHOLDS: OnceLock<Mutex<Thresholds>> = OnceLock::new();
THRESHOLDS.get_or_init(|| Mutex::new(Thresholds::default()))
}
+113
View File
@@ -0,0 +1,113 @@
//! Simple timers and the global "time since last update" registry.
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::time::Instant;
/// The timer class. A simple chronometer.
#[derive(Debug, Clone)]
pub struct Timer {
duration: f64,
target: Instant,
}
impl Timer {
pub fn new(duration: f64) -> Self {
let mut timer = Self { duration, target: Instant::now() };
timer.start();
timer
}
pub fn start(&mut self) {
self.target = Instant::now() + std::time::Duration::from_secs_f64(self.duration.max(0.0));
}
pub fn reset(&mut self, duration: Option<f64>) {
if let Some(duration) = duration {
self.set(duration);
}
self.start();
}
/// Elapsed seconds since the timer was started.
pub fn get(&self) -> f64 {
self.duration - self.target.saturating_duration_since(Instant::now()).as_secs_f64()
}
pub fn set(&mut self, duration: f64) {
self.duration = duration;
}
pub fn duration(&self) -> f64 {
self.duration
}
pub fn finished(&self) -> bool {
Instant::now() > self.target
}
}
/// The counter class, used to log durations.
#[derive(Debug, Clone)]
pub struct Counter {
start: Instant,
}
impl Default for Counter {
fn default() -> Self {
Self::new()
}
}
impl Counter {
pub fn new() -> Self {
Self { start: Instant::now() }
}
pub fn reset(&mut self) {
self.start = Instant::now();
}
pub fn get(&self) -> f64 {
self.start.elapsed().as_secs_f64()
}
}
fn last_update_times() -> &'static Mutex<HashMap<String, Instant>> {
static MAP: OnceLock<Mutex<HashMap<String, Instant>>> = OnceLock::new();
MAP.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Return the elapsed time since the last update for the given key.
///
/// The first call for a key returns 1 second, mirroring the Python
/// implementation.
pub fn get_time_since_last_update(key: &str) -> f64 {
let now = Instant::now();
let mut map = last_update_times().lock().unwrap();
let since = match map.get(key) {
Some(last) => now.duration_since(*last).as_secs_f64(),
None => 1.0,
};
map.insert(key.to_string(), now);
since
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timer_finished() {
let timer = Timer::new(0.0);
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(timer.finished());
}
#[test]
fn time_since_first_call_is_one() {
let v = get_time_since_last_update("test-key");
assert_eq!(v, 1.0);
}
}
+245
View File
@@ -0,0 +1,245 @@
//! Formatting helpers (units, Python-compatible `str()`/`repr()`).
use serde_json::Value;
/// Convert Celsius to Fahrenheit.
pub fn to_fahrenheit(celsius: f64) -> f64 {
celsius * 1.8 + 32.0
}
/// Make a nice human-readable string out of a number (port of `auto_unit`).
pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol: &str) -> String {
let symbols: &[char] = &['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
let symbols: Vec<char> = match symbols.iter().position(|&s| s.to_string() == min_symbol) {
Some(idx) => symbols[idx..].to_vec(),
None => symbols.to_vec(),
};
let prefix = |symbol: char| -> f64 {
match symbol {
'Y' => 1208925819614629174706176.0,
'Z' => 1180591620717411303424.0,
'E' => 1152921504606846976.0,
'P' => 1125899906842624.0,
'T' => 1099511627776.0,
'G' => 1073741824.0,
'M' => 1048576.0,
'K' => 1024.0,
_ => 1.0,
}
};
if number.is_nan() {
return none_symbol.to_string();
}
if number == 0.0 {
// Avoid 0.0
return "0".to_string();
}
if !number.is_finite() {
return format!("{number}");
}
// If the value was given as an integer, decimal_precision is 0.
let mut decimal_precision = if number.fract() != 0.0 { 2 } else { 0 };
for &symbol in symbols.iter().rev() {
let value = number / prefix(symbol);
if value > 1.0 {
decimal_precision = 0;
if value <= 9.995 {
decimal_precision = 2;
} else if value < 99.95 {
decimal_precision = 1;
}
if low_precision {
if symbol == 'M' || symbol == 'K' {
decimal_precision = 0;
} else {
decimal_precision = decimal_precision.min(1);
}
} else if symbol == 'K' {
decimal_precision = 0;
}
return format!("{value:.decimal_precision$}{symbol}");
}
}
format!("{number:.decimal_precision$}")
}
/// Parse a value like `'12.5 MB'` into a float (port of `string_value_to_float`).
pub fn string_value_to_float(s: &str) -> Option<f64> {
let cleaned: String = s.chars().filter(|c| *c != ' ').collect();
let mut value: Option<f64> = None;
let mut unit: Option<String> = None;
let mut digits = String::new();
let mut rest = String::new();
for c in cleaned.chars() {
if c.is_ascii_digit() || c == '.' {
if rest.is_empty() {
digits.push(c);
continue;
}
}
rest.push(c);
}
if !digits.is_empty() {
value = digits.parse::<f64>().ok();
}
if !rest.is_empty() {
unit = Some(rest.to_uppercase());
}
let value = value?;
let multiplier = match unit.as_deref() {
None => 1.0,
Some("B") => 1.0,
Some("KB") => 1000.0,
Some("MB") => 1_000_000.0,
Some("GB") => 1_000_000_000.0,
Some("TB") => 1_000_000_000_000.0,
Some("PB") => 1_000_000_000_000_000.0,
Some(_) => return None,
};
Some(value * multiplier)
}
/// Python `str()` for a JSON value (used by stdout/csv outputs).
pub fn py_str(value: &Value) -> String {
match value {
Value::Null => "None".to_string(),
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
Value::Number(n) => py_number(n),
Value::String(s) => s.clone(),
other => py_repr(other),
}
}
fn py_number(n: &serde_json::Number) -> String {
if let Some(i) = n.as_i64() {
return i.to_string();
}
if let Some(u) = n.as_u64() {
return u.to_string();
}
let f = n.as_f64().unwrap_or(0.0);
py_float(f)
}
/// Python `repr()` for a float.
pub fn py_float(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "inf" } else { "-inf" }.to_string();
}
if f.fract() == 0.0 && f.abs() < 1e16 {
format!("{f:.1}")
} else {
format!("{f}")
}
}
/// Python `repr()` for a JSON value (single quotes, True/False/None).
pub fn py_repr(value: &Value) -> String {
match value {
Value::Null => "None".to_string(),
Value::Bool(b) => if *b { "True" } else { "False" }.to_string(),
Value::Number(n) => py_number(n),
Value::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
Value::Array(items) => {
let inner: Vec<String> = items.iter().map(py_repr).collect();
format!("[{}]", inner.join(", "))
}
Value::Object(map) => {
let inner: Vec<String> = map
.iter()
.map(|(k, v)| format!("'{}': {}", k.replace('\'', "\\'"), py_repr(v)))
.collect();
format!("{{{}}}", inner.join(", "))
}
}
}
/// Extract a f64 from a JSON value the way Python would accept a number.
pub fn as_f64(value: &Value) -> Option<f64> {
match value {
Value::Number(n) => n.as_f64(),
Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
}
}
/// Simple mean subsampling (port of `subsample`).
pub fn subsample(data: &[f64], sampling: usize) -> Vec<f64> {
if data.len() <= sampling || sampling == 0 {
return data.to_vec();
}
let sampling_length = (data.len() as f64 / sampling as f64).round() as usize;
(0..sampling)
.map(|s| {
let start = s * sampling_length;
let end = ((s + 1) * sampling_length).min(data.len());
let slice = &data[start..end];
slice.iter().sum::<f64>() / slice.len() as f64
})
.collect()
}
/// Return a list of the substrings in the input string, using `sep` as the
/// separator char and `esc` as the escape character (port of `split_esc`,
/// only the explicit separator flavour is required).
pub fn split_esc(input: &str, sep: char, esc: char) -> Vec<String> {
let mut ret: Vec<String> = vec![String::new()];
let mut esc_seen = false;
for ch in input.chars() {
if !esc_seen {
if ch == esc {
esc_seen = true;
} else if ch == sep {
ret.push(String::new());
} else {
ret.last_mut().unwrap().push(ch);
}
} else {
ret.last_mut().unwrap().push(ch);
esc_seen = false;
}
}
ret
}
/// Replace special chars by another (new lines, tabs...) or escape for a
/// shell by adding a `$` prefix in front of them. (Only the replacement
/// flavour is needed by the plugins.)
pub fn replace_special_chars(input: &str, by: char) -> String {
input.replace("\r\n", &by.to_string()).replace('\n', &by.to_string()).replace('\t', &by.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_auto_unit() {
assert_eq!(auto_unit(613421788.0, false, "K", "-"), "585M");
assert_eq!(auto_unit(5307033647.0, false, "K", "-"), "4.94G");
assert_eq!(auto_unit(5307033647.0, true, "K", "-"), "4.9G");
assert_eq!(auto_unit(44968414685.0, false, "K", "-"), "41.9G");
assert_eq!(auto_unit(838471403472.0, false, "K", "-"), "781G");
assert_eq!(auto_unit(9683209690677.0, false, "K", "-"), "8.81T");
assert_eq!(auto_unit(1073741824.0, false, "K", "-"), "1024M");
assert_eq!(auto_unit(1181116006.0, false, "K", "-"), "1.10G");
assert_eq!(auto_unit(0.0, false, "K", "-"), "0");
}
#[test]
fn test_py_repr() {
assert_eq!(py_repr(&json!({"a": 1, "b": "x"})), "{'a': 1, 'b': 'x'}");
assert_eq!(py_str(&json!(1234.0)), "1234.0");
assert_eq!(py_str(&json!(5)), "5");
assert_eq!(py_str(&json!(true)), "True");
assert_eq!(py_str(&json!(null)), "None");
}
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "glances-plugins"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
glances-core = { version = "0.1.0", path = "../glances-core" }
indexmap.workspace = true
libc.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sysinfo = "0.39.6"
thiserror.workspace = true
tracing.workspace = true
+564
View File
@@ -0,0 +1,564 @@
//! Low level Linux collectors (procfs / sysfs) shared by the plugins.
//!
//! The values and semantics mirror what psutil returns on Linux, so the
//! plugin stats stay compatible with the Python implementation.
use indexmap::IndexMap;
use std::collections::HashMap;
/// System wide or per-CPU time counters (in seconds, like psutil).
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct CpuTimes {
pub user: f64,
pub nice: f64,
pub system: f64,
pub idle: f64,
pub iowait: f64,
pub irq: f64,
pub softirq: f64,
pub steal: f64,
pub guest: f64,
pub guest_nice: f64,
}
impl CpuTimes {
pub fn total(&self) -> f64 {
self.user
+ self.nice
+ self.system
+ self.idle
+ self.iowait
+ self.irq
+ self.softirq
+ self.steal
+ self.guest
+ self.guest_nice
}
pub fn busy(&self) -> f64 {
self.total() - self.idle
}
pub fn deltas(&self, previous: &CpuTimes) -> CpuTimes {
CpuTimes {
user: self.user - previous.user,
nice: self.nice - previous.nice,
system: self.system - previous.system,
idle: self.idle - previous.idle,
iowait: self.iowait - previous.iowait,
irq: self.irq - previous.irq,
softirq: self.softirq - previous.softirq,
steal: self.steal - previous.steal,
guest: self.guest - previous.guest,
guest_nice: self.guest_nice - previous.guest_nice,
}
}
/// Percentages of each field since the previous sample (psutil
/// `cpu_times_percent`).
pub fn times_percent(&self, previous: &CpuTimes) -> CpuTimes {
let deltas = self.deltas(previous);
let all_delta = deltas.total();
let scale = 100.0 / all_delta.max(1.0);
let perc = |value: f64| -> f64 { (value * scale).round().clamp(0.0, 100.0) };
CpuTimes {
user: perc(deltas.user),
nice: perc(deltas.nice),
system: perc(deltas.system),
idle: perc(deltas.idle),
iowait: perc(deltas.iowait),
irq: perc(deltas.irq),
softirq: perc(deltas.softirq),
steal: perc(deltas.steal),
guest: perc(deltas.guest),
guest_nice: perc(deltas.guest_nice),
}
}
/// Total CPU usage since the previous sample (psutil `cpu_percent`).
pub fn percent(&self, previous: &CpuTimes) -> f64 {
let deltas = self.deltas(previous);
let all_delta = deltas.total();
let busy_delta = deltas.busy();
if all_delta == 0.0 {
return 0.0;
}
((busy_delta / all_delta) * 100.0 * 10.0).round() / 10.0
}
}
fn clock_ticks() -> f64 {
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if ticks <= 0 { 100.0 } else { ticks as 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 };
CpuTimes {
user: value(0),
nice: value(1),
system: value(2),
idle: value(3),
iowait: value(4),
irq: value(5),
softirq: value(6),
steal: value(7),
guest: value(8),
guest_nice: value(9),
}
}
/// Read the system wide CPU times from `/proc/stat`.
pub fn read_cpu_times() -> Option<CpuTimes> {
let content = std::fs::read_to_string("/proc/stat").ok()?;
let line = content.lines().next()?;
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.first() != Some(&"cpu") {
return None;
}
Some(parse_cpu_line(&fields[1..]))
}
/// Read the per-CPU times from `/proc/stat`.
pub fn read_percpu_times() -> Vec<CpuTimes> {
let Ok(content) = std::fs::read_to_string("/proc/stat") else {
return Vec::new();
};
content
.lines()
.filter(|line| line.starts_with("cpu") && !line.starts_with("cpu "))
.map(|line| {
let fields: Vec<&str> = line.split_whitespace().collect();
parse_cpu_line(&fields[1..])
})
.collect()
}
/// Read `/proc/meminfo` values in bytes.
pub fn read_meminfo() -> HashMap<String, u64> {
let mut values = HashMap::new();
let Ok(content) = std::fs::read_to_string("/proc/meminfo") else {
return values;
};
for line in content.lines() {
let Some((key, rest)) = line.split_once(':') else {
continue;
};
let mut parts = rest.split_whitespace();
let Some(value) = parts.next().and_then(|v| v.parse::<u64>().ok()) else {
continue;
};
let unit = parts.next().unwrap_or("");
let bytes = if unit.starts_with("kB") { value * 1024 } else { value };
values.insert(key.trim().to_string(), bytes);
}
values
}
/// Read `/proc/vmstat` counters.
pub fn read_vmstat() -> HashMap<String, u64> {
let mut values = HashMap::new();
let Ok(content) = std::fs::read_to_string("/proc/vmstat") else {
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);
}
}
}
values
}
pub fn page_size() -> u64 {
let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if size <= 0 { 4096 } else { size as u64 }
}
/// Read the load average (1, 5, 15 minutes).
pub fn read_loadavg() -> Option<(f64, f64, f64)> {
let content = std::fs::read_to_string("/proc/loadavg").ok()?;
let fields: Vec<&str> = content.split_whitespace().collect();
Some((
fields.first()?.parse().ok()?,
fields.get(1)?.parse().ok()?,
fields.get(2)?.parse().ok()?,
))
}
/// Read the boot time (epoch seconds).
pub fn boot_time() -> Option<u64> {
let content = std::fs::read_to_string("/proc/stat").ok()?;
for line in content.lines() {
if let Some(value) = line.strip_prefix("btime ") {
return value.trim().parse().ok();
}
}
None
}
pub fn uptime_seconds() -> Option<f64> {
let content = std::fs::read_to_string("/proc/uptime").ok()?;
content.split_whitespace().next()?.parse().ok()
}
/// Number of logical CPUs.
pub fn logical_core_count() -> usize {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
}
/// Number of physical cores, parsed from `/proc/cpuinfo`.
pub fn physical_core_count() -> Option<usize> {
let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
let mut pairs: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
let mut physical_id = String::new();
let mut core_id = String::new();
let mut has_ids = false;
for line in content.lines().chain(std::iter::once("")) {
if line.trim().is_empty() {
if !physical_id.is_empty() && !core_id.is_empty() {
pairs.insert((physical_id.clone(), core_id.clone()));
has_ids = true;
}
physical_id.clear();
core_id.clear();
continue;
}
let Some((key, value)) = line.split_once(':') else {
continue;
};
match key.trim() {
"physical id" => physical_id = value.trim().to_string(),
"core id" => core_id = value.trim().to_string(),
_ => {}
}
}
if has_ids && !pairs.is_empty() {
Some(pairs.len())
} else {
// Fallback: count unique processor entries.
let count = content
.lines()
.filter(|l| l.starts_with("processor") && l.contains(':'))
.count();
if count > 0 { Some(count) } else { None }
}
}
/// Return the CPU model name from `/proc/cpuinfo`.
pub fn cpu_name() -> String {
let fallback = format!("CPU {}", std::env::consts::ARCH);
let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") else {
return fallback;
};
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("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}");
}
}
}
}
ret
}
/// CPU frequency information (`/sys/devices/system/cpu/cpu0/cpufreq`).
pub fn cpu_frequency() -> (Option<f64>, Option<f64>) {
let current = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
.ok()
.and_then(|v| v.trim().parse::<f64>().ok())
.map(|khz| khz / 1000.0);
let max = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
.ok()
.and_then(|v| v.trim().parse::<f64>().ok())
.map(|khz| khz / 1000.0);
(current, max)
}
/// `/proc/net/dev` counters.
#[derive(Debug, Clone, Copy, Default)]
pub struct NetDev {
pub bytes_recv: u64,
pub packets_recv: u64,
pub errin: u64,
pub dropin: u64,
pub bytes_sent: u64,
pub packets_sent: u64,
pub errout: u64,
pub dropout: u64,
}
pub fn read_net_dev() -> IndexMap<String, NetDev> {
let mut result = IndexMap::new();
let Ok(content) = std::fs::read_to_string("/proc/net/dev") else {
return result;
};
for line in content.lines().skip(2) {
let Some((interface, values)) = line.split_once(':') else {
continue;
};
let fields: Vec<u64> = values.split_whitespace().filter_map(|v| v.parse().ok()).collect();
if fields.len() < 16 {
continue;
}
result.insert(
interface.trim().to_string(),
NetDev {
bytes_recv: fields[0],
packets_recv: fields[1],
errin: fields[2],
dropin: fields[3],
bytes_sent: fields[8],
packets_sent: fields[9],
errout: fields[10],
dropout: fields[11],
},
);
}
result
}
/// Interface speed in Mbps (0 when unknown).
pub fn interface_speed(interface: &str) -> u64 {
std::fs::read_to_string(format!("/sys/class/net/{interface}/speed"))
.ok()
.and_then(|v| v.trim().parse::<i64>().ok())
.filter(|v| *v > 0)
.map(|v| v as u64)
.unwrap_or(0)
}
/// Interface flags from `/sys/class/net/<if>/flags`.
pub fn interface_flags(interface: &str) -> u64 {
std::fs::read_to_string(format!("/sys/class/net/{interface}/flags"))
.ok()
.and_then(|v| u64::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok())
.unwrap_or(0)
}
/// Interface MTU.
pub fn interface_mtu(interface: &str) -> u64 {
std::fs::read_to_string(format!("/sys/class/net/{interface}/mtu"))
.ok()
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0)
}
/// Return the list of interface names.
pub fn interface_names() -> Vec<String> {
let mut names = Vec::new();
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
names.push(entry.file_name().to_string_lossy().to_string());
}
}
names.sort();
names
}
/// Return True if the interface has at least one non link-layer address.
pub fn interface_has_ip(interface: &str) -> bool {
let mut result = false;
unsafe {
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
if libc::getifaddrs(&mut ifap) != 0 {
return false;
}
let mut current = ifap;
while !current.is_null() {
let addr = (*current).ifa_addr;
if !addr.is_null() {
let name = std::ffi::CStr::from_ptr((*current).ifa_name);
if name.to_string_lossy() == interface {
let family = (*addr).sa_family as i32;
if family == libc::AF_INET || family == libc::AF_INET6 {
result = true;
break;
}
}
}
current = (*current).ifa_next;
}
libc::freeifaddrs(ifap);
}
result
}
/// `/proc/diskstats` entry.
#[derive(Debug, Clone, Copy, Default)]
pub struct DiskStat {
pub read_count: u64,
pub reads_merged: u64,
pub sectors_read: u64,
pub read_time: u64,
pub write_count: u64,
pub writes_merged: u64,
pub sectors_written: u64,
pub write_time: u64,
}
pub const SECTOR_SIZE: u64 = 512;
pub fn read_diskstats() -> IndexMap<String, DiskStat> {
let mut result = IndexMap::new();
let Ok(content) = std::fs::read_to_string("/proc/diskstats") else {
return result;
};
for line in content.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 14 {
continue;
}
let name = fields[2].to_string();
let value = |index: usize| -> u64 { fields.get(index).and_then(|v| v.parse().ok()).unwrap_or(0) };
result.insert(
name,
DiskStat {
read_count: value(3),
reads_merged: value(4),
sectors_read: value(5),
read_time: value(6),
write_count: value(7),
writes_merged: value(8),
sectors_written: value(9),
write_time: value(10),
},
);
}
result
}
/// `/proc/interrupts` entries (name, total count).
pub fn read_interrupts() -> Vec<(String, u64)> {
let Ok(content) = std::fs::read_to_string("/proc/interrupts") else {
return Vec::new();
};
let mut result = Vec::new();
for line in content.lines().skip(1) {
let Some((irq, rest)) = line.split_once(':') else {
continue;
};
let irq = irq.trim();
let parts: Vec<&str> = rest.split_whitespace().collect();
// Last part is the name, the numbers before are the per-CPU counts.
let numbers: u64 = parts[..parts.len().saturating_sub(1)]
.iter()
.filter_map(|v| v.parse::<u64>().ok())
.sum();
let name = parts.last().copied().unwrap_or("");
let label = if name.is_empty() || name.chars().all(|c| c.is_ascii_digit()) {
irq.to_string()
} else {
format!("{irq}: {name}")
};
result.push((label, numbers));
}
result
}
/// Mount entry from `/proc/mounts`.
#[derive(Debug, Clone, Default)]
pub struct MountInfo {
pub device: String,
pub mount_point: String,
pub fs_type: String,
pub options: String,
}
pub fn read_mounts() -> Vec<MountInfo> {
let Ok(content) = std::fs::read_to_string("/proc/mounts") else {
return Vec::new();
};
content
.lines()
.filter_map(|line| {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 4 {
return None;
}
Some(MountInfo {
device: fields[0].replace("\\040", " "),
mount_point: fields[1].replace("\\040", " "),
fs_type: fields[2].to_string(),
options: fields[3].to_string(),
})
})
.collect()
}
/// Disk usage of a mount point (psutil `disk_usage`).
#[derive(Debug, Clone, Copy, Default)]
pub struct FsUsage {
pub total: u64,
pub used: u64,
pub free: u64,
pub percent: f64,
}
pub fn disk_usage(path: &str) -> Option<FsUsage> {
let c_path = std::ffi::CString::new(path).ok()?;
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
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 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 })
}
/// Linux distribution name/version (`/etc/os-release`).
pub fn read_os_release() -> (String, String) {
let mut name = String::new();
let mut version = String::new();
if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
for line in content.lines() {
if let Some(value) = line.strip_prefix("NAME=") {
name = value.trim_matches('"').to_string();
} else if let Some(value) = line.strip_prefix("VERSION_ID=") {
version = value.trim_matches('"').to_string();
}
}
}
(name, version)
}
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()) };
if result == 0 {
let end = buffer.iter().position(|c| *c == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..end]).to_string()
} else {
"localhost".to_string()
}
}
pub fn is_admin() -> bool {
unsafe { libc::getuid() == 0 }
}
/// 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())
}
/// Get the current date formatted with a Python strftime format.
pub fn strftime(format: &str) -> String {
chrono::Local::now().format(format).to_string()
}
+128
View File
@@ -0,0 +1,128 @@
//! CPU percent state shared between the CPU, per-CPU and quicklook plugins
//! (port of `glances.cpu_percent`).
use crate::collectors::{CpuTimes, logical_core_count, read_cpu_times, read_percpu_times};
use serde_json::{Value, json};
use std::sync::Mutex;
use std::sync::OnceLock;
#[derive(Debug, Clone, Default)]
pub struct CpuInfo {
pub cpu_name: String,
pub cpu_hz_current: Option<f64>,
pub cpu_hz: Option<f64>,
}
#[derive(Debug)]
pub struct CpuPercentState {
/// Cache duration in seconds.
cached_timer_cpu: f64,
prev_cpu: Option<CpuTimes>,
prev_percpu: Vec<CpuTimes>,
cpu_percent: f64,
percpu_percent: Vec<Value>,
}
impl CpuPercentState {
fn new(cached_timer_cpu: f64) -> Self {
let mut state = Self {
cached_timer_cpu,
prev_cpu: None,
prev_percpu: Vec::new(),
cpu_percent: 0.0,
percpu_percent: Vec::new(),
};
state.cpu_percent = state.compute_cpu();
state.percpu_percent = state.compute_percpu();
state
}
/// 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();
self.cpu_percent
}
fn compute_cpu(&mut self) -> f64 {
let Some(current) = read_cpu_times() else {
return 0.0;
};
match self.prev_cpu {
Some(previous) => {
let percent = current.percent(&previous);
self.prev_cpu = Some(current);
percent
}
None => {
// The first call is meaningless (psutil compares two
// consecutive samples).
let second = read_cpu_times().unwrap_or(current);
let percent = second.percent(&current);
self.prev_cpu = Some(second);
percent
}
}
}
/// Update and/or return the per-CPU list.
pub fn get_percpu(&mut self) -> &[Value] {
self.percpu_percent = self.compute_percpu();
&self.percpu_percent
}
fn compute_percpu(&mut self) -> Vec<Value> {
let current = read_percpu_times();
let mut stats = Vec::new();
for (number, cpu_times) in current.iter().enumerate() {
let percent = self
.prev_percpu
.get(number)
.map(|previous| cpu_times.times_percent(previous))
.unwrap_or_default();
let total = ((100.0 - percent.idle) * 10.0).round() / 10.0;
stats.push(json!({
"key": "cpu_number",
"cpu_number": number,
"total": total,
"user": percent.user,
"system": percent.system,
"idle": percent.idle,
"nice": percent.nice,
"iowait": percent.iowait,
"irq": percent.irq,
"softirq": percent.softirq,
"steal": percent.steal,
"guest": percent.guest,
"guest_nice": percent.guest_nice,
"dpc": Value::Null,
"interrupt": Value::Null,
}));
}
self.prev_percpu = current;
stats
}
pub fn get_info(&self) -> CpuInfo {
let (current, max) = crate::collectors::cpu_frequency();
CpuInfo {
cpu_name: crate::collectors::cpu_name(),
cpu_hz_current: current,
cpu_hz: max,
}
}
pub fn logical_core_count(&self) -> usize {
logical_core_count()
}
}
/// The shared CPU percent state (equivalent of the Python `cpu_percent`).
pub fn cpu_percent() -> &'static Mutex<CpuPercentState> {
static CPU_PERCENT: OnceLock<Mutex<CpuPercentState>> = OnceLock::new();
CPU_PERCENT.get_or_init(|| Mutex::new(CpuPercentState::new(2.0)))
}
+297
View File
@@ -0,0 +1,297 @@
//! Plugin registry and update engine.
use crate::plugins::*;
use glances_core::plugin::{Plugin, PluginModel, UpdateContext};
use glances_core::processes::processes;
use glances_core::{Args, Config, Runtime};
use indexmap::IndexMap;
use serde_json::Value;
use tracing::debug;
/// The update engine: holds the configuration, the runtime state and every
/// plugin instance.
pub struct Engine {
pub args: Args,
pub config: Config,
pub runtime: Runtime,
pub plugins: IndexMap<String, Box<dyn Plugin>>,
}
impl Engine {
pub fn new(args: Args, config: Config) -> Self {
let refresh_time = args.time.unwrap_or(2.0);
let fs_free_space_config = config.get_bool_value("fs", "free_space", false);
let runtime = Runtime::new(&args, fs_free_space_config);
let mut plugins: IndexMap<String, Box<dyn Plugin>> = IndexMap::new();
// Registration order mirrors the alphabetical plugin loading of the
// Python implementation (in particular processcount runs before
// processlist and programlist).
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("fs".to_string(), Box::new(FsPlugin::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("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("now".to_string(), Box::new(NowPlugin::new(&config, &args)));
plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::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("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)));
// 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 };
engine.apply_plugin_options();
engine.apply_process_options();
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);
}
}
}
// Availability (hardware/kernel dependent plugins).
let names: Vec<String> = self.plugins.keys().cloned().collect();
let mut available = std::collections::HashMap::new();
for name in &names {
let plugin = self.plugins.get_mut(name).unwrap();
available.insert(name.clone(), plugin.available());
}
for (name, is_available) in &available {
if !is_available {
debug!("{name} plugin is not available on this system");
self.plugins.get_mut(name).unwrap().set_enabled(false);
}
}
// --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();
}
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();
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);
}
} else {
for token in tokens {
if let Some(plugin) = self.plugins.get_mut(&token) {
plugin.set_enabled(false);
}
}
}
}
for token in &explicit_enable {
if let Some(plugin) = self.plugins.get_mut(token) {
plugin.set_enabled(true);
}
}
// --disable-process disables the process plugins.
if self.args.disable_process {
for name in ["processcount", "processlist", "programlist"] {
if let Some(plugin) = self.plugins.get_mut(name) {
plugin.set_enabled(false);
}
}
}
// Processlist needs processcount to update the shared process list.
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");
}
plugin.set_enabled(false);
}
}
}
// --light disables the left sidebar plugins.
if self.args.enable_light {
self.args.disable_left_sidebar = true;
for name in ["processlist", "programlist", "amps", "containers", "vms"] {
if let Some(plugin) = self.plugins.get_mut(name) {
plugin.set_enabled(false);
}
}
}
// --full-quicklook: only quicklook and load.
if self.args.full_quicklook {
self.runtime.help_tag = false;
for name in ["cpu", "mem", "memswap"] {
if let Some(plugin) = self.plugins.get_mut(name) {
plugin.set_enabled(false);
}
}
for name in ["quicklook", "load"] {
if let Some(plugin) = self.plugins.get_mut(name) {
plugin.set_enabled(true);
}
}
}
// --disable-top: disable the top menu plugins.
if self.args.disable_top {
for name in ["quicklook", "cpu", "mem", "memswap", "load"] {
if let Some(plugin) = self.plugins.get_mut(name) {
plugin.set_enabled(false);
}
}
}
// --enable-irq is required for the irq plugin.
if !self.args.enable_irq {
if let Some(plugin) = self.plugins.get_mut("irq") {
plugin.set_enabled(false);
}
} else if let Some(plugin) = self.plugins.get_mut("irq") {
plugin.set_enabled(true);
plugin.model_mut().refresh_timer = glances_core::timer::Timer::new(0.0);
}
}
fn apply_process_options(&mut self) {
let mut procs = processes().lock().unwrap();
procs.no_kernel_threads = self.args.no_kernel_threads;
if let Some(filter) = &self.args.process_filter {
procs.filter.set_filter(Some(filter));
}
if let Some(focus) = &self.args.process_focus {
procs.filter_focus.set_filter(Some(focus));
}
if self.args.enable_process_extended {
procs.enable_extended();
} else {
procs.disable_extended();
}
}
/// 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();
self.update_plugins(&enabled);
}
/// Update the given plugins.
pub fn update_plugins(&mut self, names: &[String]) {
for name in names {
self.update_plugin(name);
}
}
fn update_plugin(&mut self, name: &str) {
let Engine { plugins, args, config, runtime, .. } = self;
let Some(plugin) = plugins.get_mut(name) else {
return;
};
if !plugin.is_enabled() {
return;
}
// Check the refresh timer: return the previous stats if too early.
let init_value = plugin.init_value();
let model = plugin.model();
if !(model.refresh_timer.finished() || model.stats == init_value) {
return;
}
let key = plugin.get_key();
let mut ctx = UpdateContext {
args,
runtime,
config,
cursor_position: runtime.cursor_position,
};
let raw = match plugin.collect(&mut ctx) {
Ok(raw) => raw,
Err(err) => {
tracing::error!("Error while updating the {name} plugin ({err})");
return;
}
};
let refresh = plugin.model().get_refresh();
let mut raw = raw;
{
let model = plugin.model_mut();
model.time_since_last_update = glances_core::timer::get_time_since_last_update(name);
model.compute_rate(&mut raw, key);
model.update_mmm_fields(&mut raw);
model.stats = raw;
}
if let Err(err) = plugin.post_collect(&mut ctx) {
tracing::error!("Error while post-processing the {name} plugin ({err})");
}
plugin.update_views();
plugin.model_mut().update_stats_history();
let timer = &mut plugin.model_mut().refresh_timer;
timer.set(refresh);
timer.reset(None);
}
pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
self.plugins.get(name).map(|p| p.as_ref())
}
pub fn get_plugins_list(&self, enabled_only: bool) -> Vec<String> {
self.plugins
.iter()
.filter(|(_, p)| !enabled_only || p.is_enabled())
.map(|(name, _)| name.clone())
.collect()
}
/// Return all the stats as a dict (like `getAllAsDict`).
pub fn all_as_dict(&self, plugin_list: Option<&[String]>) -> serde_json::Map<String, Value> {
let names = match plugin_list {
Some(list) => list.to_vec(),
None => self.get_plugins_list(true),
};
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());
}
}
}
result
}
pub fn end(&mut self) {
for plugin in self.plugins.values_mut() {
plugin.exit();
}
}
}
/// One-shot update of one plugin (used by tests).
pub fn update_single(engine: &mut Engine, name: &str) {
engine.update_plugins(&[name.to_string()]);
}
#[allow(dead_code)]
fn unused(_: &PluginModel) {}
+17
View File
@@ -0,0 +1,17 @@
//! glances-plugins: collectors, plugin registry and update engine.
pub mod collectors;
pub mod cpu_percent;
pub mod engine;
pub mod plugins;
/// Build an `IndexMap<String, Field>` from name/field pairs.
#[macro_export]
macro_rules! fields {
($( $name:literal => $field:expr ),* $(,)?) => {{
#[allow(unused_mut)]
let mut map = indexmap::IndexMap::new();
$( map.insert($name.to_string(), $field); )*
map
}};
}
@@ -0,0 +1,53 @@
//! CPU core count plugin.
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct CorePlugin {
pub model: PluginModel,
}
impl CorePlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"phys" => Field::new("Number of physical cores (hyper thread CPUs are excluded).").unit("number"),
"log" => Field::new(
"Number of logical CPU cores. A logical CPU is the number of physical cores multiplied by the number of threads that can run on each core."
).unit("number"),
};
let model = PluginModel::new(
"core",
fields,
Vec::new(),
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for CorePlugin {
fn name(&self) -> &str {
"core"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
Ok(json!({
"phys": crate::collectors::physical_core_count(),
"log": crate::collectors::logical_core_count(),
}))
}
}
+141
View File
@@ -0,0 +1,141 @@
//! CPU plugin.
use crate::collectors::{CpuTimes, read_cpu_times};
use crate::cpu_percent::cpu_percent;
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct CpuPlugin {
pub model: PluginModel,
previous_times: Option<CpuTimes>,
pub nb_log_core: usize,
}
impl CpuPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"total" => Field::new("Sum of all CPU percentages (except idle).").unit("percent").log().mmm(),
"system" => Field::new("Percent time spent in kernel space. System CPU time is the time spent running code in the Operating System kernel.").unit("percent").log(),
"user" => Field::new("CPU percent time spent in user space. User CPU time is the time spent on the processor running your program's code (or code in libraries).").unit("percent").log(),
"iowait" => Field::new("*(Linux)*: percent time spent by the CPU waiting for I/O operations to complete.").unit("percent").log(),
"dpc" => Field::new("*(Windows)*: time spent servicing deferred procedure calls (DPCs)").unit("percent").log(),
"idle" => Field::new("percent of CPU used by any program. Every program or task that runs on a computer system occupies a certain amount of processing time on the CPU. If the CPU has completed all tasks it is idle.").unit("percent").optional(),
"irq" => Field::new("*(Linux and BSD)*: percent time spent servicing/handling hardware/software interrupts. Time servicing interrupts (hardware + software).").unit("percent").optional(),
"nice" => Field::new("*(Unix)*: percent time occupied by user level processes with a positive nice value. The time the CPU has spent running users' processes that are *niced*.").unit("percent").optional(),
"steal" => Field::new("*(Linux)*: percentage of time a virtual CPU waits for a real CPU while the hypervisor is servicing another virtual processor.").unit("percent").alert().optional(),
"guest" => Field::new("*(Linux)*: time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.").unit("percent").optional(),
"ctx_switches" => Field::new("number of context switches (voluntary + involuntary) per second. A context switch is a procedure that a computer's CPU (central processing unit) follows to change from one task to another while ensuring that the tasks do not conflict.").unit("number").rate().min_symbol("K").short_name("ctx_sw").optional(),
"interrupts" => Field::new("number of interrupts per second.").unit("number").rate().min_symbol("K").short_name("inter").optional(),
"soft_interrupts" => Field::new("number of software interrupts per second. Always set to 0 on Windows and SunOS.").unit("number").rate().min_symbol("K").short_name("sw_int").optional(),
"syscalls" => Field::new("number of system calls per second. Always 0 on Linux OS.").unit("number").rate().min_symbol("K").short_name("sys_call").optional(),
"cpucore" => Field::new("Total number of CPU core.").unit("number"),
"time_since_update" => Field::new("Number of seconds since last update.").unit("seconds"),
};
let items_history = vec![
HistoryItem::new("user", "User CPU usage"),
HistoryItem::new("system", "System CPU usage"),
];
let model = PluginModel::new(
"cpu",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
let nb_log_core = crate::collectors::logical_core_count();
Self { model, previous_times: None, nb_log_core }
}
fn times_percent(&mut self) -> CpuTimes {
let current = read_cpu_times().unwrap_or_default();
match self.previous_times {
Some(previous) => {
self.previous_times = Some(current);
current.times_percent(&previous)
}
None => {
let second = read_cpu_times().unwrap_or(current);
self.previous_times = Some(second);
second.times_percent(&current)
}
}
}
}
impl Plugin for CpuPlugin {
fn name(&self) -> &str {
"cpu"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let percent = self.times_percent();
let total = {
let mut state = cpu_percent().lock().unwrap();
state.get_cpu()
};
let mut stats = serde_json::Map::new();
stats.insert("total".to_string(), json!(total));
stats.insert("user".to_string(), json!(percent.user));
stats.insert("nice".to_string(), json!(percent.nice));
stats.insert("system".to_string(), json!(percent.system));
stats.insert("idle".to_string(), json!(percent.idle));
stats.insert("iowait".to_string(), json!(percent.iowait));
stats.insert("irq".to_string(), json!(percent.irq));
stats.insert("steal".to_string(), json!(percent.steal));
stats.insert("guest".to_string(), json!(percent.guest));
// Additional CPU stats from /proc/stat.
if let Ok(content) = std::fs::read_to_string("/proc/stat") {
let mut ctx_switches = 0u64;
let mut interrupts = 0u64;
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);
} else if let Some(rest) = line.strip_prefix("intr ") {
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);
}
}
stats.insert("ctx_switches".to_string(), json!(ctx_switches));
stats.insert("interrupts".to_string(), json!(interrupts));
stats.insert("soft_interrupts".to_string(), json!(soft_interrupts));
}
stats.insert("syscalls".to_string(), json!(0));
stats.insert("cpucore".to_string(), json!(self.nb_log_core));
Ok(Value::Object(stats))
}
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);
}
}
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,163 @@
//! Disk I/O plugin.
use crate::collectors::{SECTOR_SIZE, read_diskstats};
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config, Decoration};
use serde_json::{Value, json};
pub struct DiskioPlugin {
pub model: PluginModel,
}
impl DiskioPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"disk_name" => Field::new("Disk name."),
"read_count" => Field::new("Number of reads.").rate().unit("number"),
"write_count" => Field::new("Number of writes.").rate().unit("number"),
"read_bytes" => Field::new("Number of bytes read.").rate().unit("byte"),
"write_bytes" => Field::new("Number of bytes written.").rate().unit("byte"),
"read_time" => Field::new("Time spent reading.").rate().unit("millisecond"),
"write_time" => Field::new("Time spent writing.").rate().unit("millisecond"),
"read_latency" => Field::new("Mean time spent reading per operation.").unit("millisecond"),
"write_latency" => Field::new("Mean time spent writing per operation.").unit("millisecond"),
};
let items_history = vec![
HistoryItem::new("read_bytes_rate_per_sec", "Bytes read per second"),
HistoryItem::new("write_bytes_rate_per_sec", "Bytes write per second"),
];
let mut model = PluginModel::new(
"diskio",
fields,
items_history,
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
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()];
Self { model }
}
}
impl Plugin for DiskioPlugin {
fn name(&self) -> &str {
"diskio"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("disk_name")
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut stats = Vec::new();
for (disk_name, disk_stat) in read_diskstats() {
if !ctx.args.diskio_show_ramfs && disk_name.starts_with("ram") {
continue;
}
if !self.model.is_display(&disk_name, "") {
continue;
}
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_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"));
item.insert("disk_name".to_string(), json!(disk_name));
if let Some(alias) = self.model.has_alias(&disk_name) {
item.insert("alias".to_string(), json!(alias));
}
stats.push(Value::Object(item));
}
Ok(Value::Array(stats))
}
fn post_collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<()> {
if let Value::Array(items) = &mut self.model.stats {
for item in items.iter_mut() {
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
};
item.insert("read_latency".to_string(), json!(read_latency));
item.insert("write_latency".to_string(), json!(write_latency));
}
}
Ok(())
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let items: Vec<(String, f64, f64, f64, f64)> = match &self.model.stats {
Value::Array(list) => list
.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);
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);
for field in ["read_bytes", "read_bytes_rate_per_sec"] {
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);
}
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);
}
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);
}
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);
}
}
fn display_curse(&self) -> bool {
true
}
}
+120
View File
@@ -0,0 +1,120 @@
//! File system plugin.
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct FsPlugin {
pub model: PluginModel,
}
impl FsPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"device_name" => Field::new("Device name."),
"fs_type" => Field::new("File system type."),
"mnt_point" => Field::new("Mount point."),
"options" => Field::new("Mount options."),
"size" => Field::new("Total size.").unit("byte"),
"used" => Field::new("Used size.").unit("byte"),
"free" => Field::new("Free size.").unit("byte"),
"percent" => Field::new("File system usage in percent.").unit("percent"),
};
let items_history = vec![HistoryItem::new("percent", "File system usage in percent")];
let mut model = PluginModel::new(
"fs",
fields,
items_history,
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("mnt_point");
Self { model }
}
}
impl Plugin for FsPlugin {
fn name(&self) -> &str {
"fs"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("mnt_point")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let allowed_fs_types = self.model.get_conf_value("allow", "");
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()));
if !is_physical && !is_allowed {
continue;
}
if !self.model.is_display_any(&[&mount.mount_point, &mount.device], "") {
continue;
}
let Some(usage) = crate::collectors::disk_usage(&mount.mount_point) else {
continue;
};
let mnt_point = mount.mount_point.replace('\u{00a0}', " ");
let mut item = serde_json::Map::new();
item.insert("device_name".to_string(), json!(mount.device));
item.insert("fs_type".to_string(), json!(mount.fs_type));
item.insert("mnt_point".to_string(), json!(mnt_point));
item.insert("options".to_string(), json!(mount.options));
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("key".to_string(), json!("mnt_point"));
if let Some(alias) = self.model.has_alias(&mnt_point) {
item.insert("alias".to_string(), json!(alias));
}
stats.push(Value::Object(item));
}
Ok(Value::Array(stats))
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let items: Vec<(String, f64, f64, String)> = match &self.model.stats {
Value::Array(list) => list
.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 size = item.get("size")?.as_f64()?;
let free = item.get("free")?.as_f64()?;
Some((mnt, size, free, options))
})
.collect(),
_ => Vec::new(),
};
for (mnt_point, size, free, options) in items {
// Do not display thresholds for volumes mounted in 'ro' (#3143).
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);
}
}
fn display_curse(&self) -> bool {
true
}
}
+101
View File
@@ -0,0 +1,101 @@
//! IRQ plugin (Linux only).
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use indexmap::IndexMap;
use serde_json::{Value, json};
pub struct IrqPlugin {
pub model: PluginModel,
lasts: IndexMap<String, u64>,
}
impl IrqPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"irq_line" => Field::new("IRQ line name"),
"irq_rate" => Field::new("IRQ rate per second").unit("numberpersecond"),
};
let mut model = PluginModel::new(
"irq",
fields,
Vec::new(),
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("irq_line");
Self { model, lasts: IndexMap::new() }
}
}
impl Plugin for IrqPlugin {
fn name(&self) -> &str {
"irq"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("irq_line")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let time_since_update = glances_core::timer::get_time_since_last_update("irq");
let mut stats = Vec::new();
// /proc/interrupts may not exist in containers.
let Ok(content) = std::fs::read_to_string("/proc/interrupts") else {
return Ok(Value::Array(stats));
};
let mut lines = content.lines();
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 {
continue;
}
let first = fields[0].trim_end_matches(':');
let irq_line = if first.chars().all(|c| c.is_ascii_digit()) {
format!("{first}_{}", fields.last().copied().unwrap_or(""))
} else {
first.to_string()
};
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
}
_ => 0,
};
stats.push(json!({
"irq_line": irq_line,
"irq_rate": irq_rate,
"key": "irq_line",
"time_since_update": time_since_update,
}));
self.lasts.insert(irq_line, current);
}
// Keep the top 5 by rate.
stats.sort_by(|a, b| {
b.get("irq_rate")
.and_then(Value::as_i64)
.unwrap_or(0)
.cmp(&a.get("irq_rate").and_then(Value::as_i64).unwrap_or(0))
});
stats.truncate(5);
Ok(Value::Array(stats))
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,81 @@
//! Load average plugin.
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct LoadPlugin {
pub model: PluginModel,
}
impl LoadPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"min1" => Field::new("Average sum of the number of processes waiting in the run-queue plus the number currently executing over 1 minute.").unit("float").mmm(),
"min5" => Field::new("Average sum of the number of processes waiting in the run-queue plus the number currently executing over 5 minutes.").unit("float"),
"min15" => Field::new("Average sum of the number of processes waiting in the run-queue plus the number currently executing over 15 minutes.").unit("float"),
"cpucore" => Field::new("Total number of CPU core.").unit("number"),
};
let items_history = vec![
HistoryItem::new("min1", "1 minute load"),
HistoryItem::new("min5", "5 minutes load"),
HistoryItem::new("min15", "15 minutes load"),
];
let model = PluginModel::new(
"load",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for LoadPlugin {
fn name(&self) -> &str {
"load"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
match crate::collectors::read_loadavg() {
Some((min1, min5, min15)) => Ok(json!({
"min1": min1,
"min5": min5,
"min15": min15,
"cpucore": crate::collectors::logical_core_count(),
})),
None => Ok(json!({})),
}
}
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;
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);
self.model.set_view_decoration(None, "min5", decoration);
}
}
fn display_curse(&self) -> bool {
true
}
}
+116
View File
@@ -0,0 +1,116 @@
//! Memory plugin.
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct MemPlugin {
pub model: PluginModel,
/// Display `available` instead of `used` in the TUI.
pub available: bool,
}
impl MemPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"total" => Field::new("Total physical memory available.").unit("bytes").min_symbol("K"),
"available" => Field::new("The actual amount of available memory that can be given instantly to processes that request more memory in bytes; this is calculated by summing different memory values depending on the platform (e.g. free + buffers + cached on Linux) and it is supposed to be used to monitor actual memory usage in a cross platform fashion.").unit("bytes").min_symbol("K").short_name("avail"),
"percent" => Field::new("The percentage usage calculated as (total - available) / total * 100.").unit("percent").mmm(),
"used" => Field::new("Memory used, calculated differently depending on the platform and designed for informational purposes only.").unit("bytes").min_symbol("K"),
"free" => Field::new("Memory not being used at all (zeroed) that is readily available; note that this doesn't reflect the actual memory available (use 'available' instead).").unit("bytes").min_symbol("K"),
"active" => Field::new("*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.").unit("bytes").min_symbol("K").optional(),
"inactive" => Field::new("*(UNIX)*: memory that is marked as not used.").unit("bytes").min_symbol("K").short_name("inacti").optional(),
"buffers" => Field::new("*(Linux, BSD)*: cache for things like file system metadata.").unit("bytes").min_symbol("K").short_name("buffer").optional(),
"cached" => Field::new("*(Linux, BSD)*: cache for various things (including ZFS cache).").unit("bytes").min_symbol("K").optional(),
"wired" => Field::new("*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.").unit("bytes").min_symbol("K"),
"shared" => Field::new("*(BSD)*: memory that may be simultaneously accessed by multiple processes.").unit("bytes").min_symbol("K"),
};
let items_history = vec![HistoryItem::new("percent", "RAM memory usage")];
let model = PluginModel::new(
"mem",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
let available = model
.get_conf_value("available", "")
.first()
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
let mut model = model;
model.mmm_tracked = true;
Self { model, available }
}
}
impl Plugin for MemPlugin {
fn name(&self) -> &str {
"mem"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let meminfo = crate::collectors::read_meminfo();
let get = |key: &str| meminfo.get(key).copied().unwrap_or(0);
let total = get("MemTotal");
let free = get("MemFree");
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 active = get("Active");
let inactive = get("Inactive");
let mut available = get("MemAvailable");
if available == 0 {
// Kernel bug or old kernel: estimate like the "free" tool.
available = free + buffers + cached;
}
if available > total {
// Container with distorted values.
available = free;
}
let used = total.saturating_sub(available);
let percent = if total > 0 {
((used as f64 * 100.0 / total as f64) * 10.0).round() / 10.0
} else {
0.0
};
let mut stats = serde_json::Map::new();
stats.insert("total".to_string(), json!(total));
stats.insert("available".to_string(), json!(available));
stats.insert("percent".to_string(), json!(percent));
stats.insert("used".to_string(), json!(used));
stats.insert("free".to_string(), json!(free));
stats.insert("active".to_string(), json!(active));
stats.insert("inactive".to_string(), json!(inactive));
stats.insert("buffers".to_string(), json!(buffers));
stats.insert("cached".to_string(), json!(cached));
stats.insert("shared".to_string(), json!(shared));
Ok(Value::Object(stats))
}
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 decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration);
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,85 @@
//! Swap memory plugin.
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct MemSwapPlugin {
pub model: PluginModel,
}
impl MemSwapPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"total" => Field::new("Total swap memory.").unit("bytes").min_symbol("K"),
"used" => Field::new("Used swap memory.").unit("bytes").min_symbol("K"),
"free" => Field::new("Free swap memory.").unit("bytes").min_symbol("K"),
"percent" => Field::new("Used swap memory in percentage.").unit("percent"),
"sin" => Field::new("The number of bytes the system has swapped in from disk (cumulative).").unit("bytes").min_symbol("K"),
"sout" => Field::new("The number of bytes the system has swapped out from disk (cumulative).").unit("bytes").min_symbol("K"),
"time_since_update" => Field::new("Number of seconds since last update.").unit("seconds"),
};
let items_history = vec![HistoryItem::new("percent", "Swap memory usage")];
let model = PluginModel::new(
"memswap",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for MemSwapPlugin {
fn name(&self) -> &str {
"memswap"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let meminfo = crate::collectors::read_meminfo();
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 vmstat = crate::collectors::read_vmstat();
// pswpin/pswpout are expressed in 4 KiB pages.
let sin = vmstat.get("pswpin").copied().unwrap_or(0) * 4 * 1024;
let sout = vmstat.get("pswpout").copied().unwrap_or(0) * 4 * 1024;
let time_since_update = glances_core::timer::get_time_since_last_update("memswap");
let mut stats = serde_json::Map::new();
stats.insert("total".to_string(), json!(total));
stats.insert("used".to_string(), json!(used));
stats.insert("free".to_string(), json!(free));
stats.insert("percent".to_string(), json!(percent));
stats.insert("sin".to_string(), json!(sin));
stats.insert("sout".to_string(), json!(sout));
stats.insert("time_since_update".to_string(), json!(time_since_update));
Ok(Value::Object(stats))
}
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 decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration);
}
fn display_curse(&self) -> bool {
true
}
}
+46
View File
@@ -0,0 +1,46 @@
//! Plugin implementations.
//!
//! The extended plugins (containers, GPU, ports, ...) are added in a later
//! milestone.
pub mod core;
pub mod cpu;
pub mod diskio;
pub mod fs;
pub mod irq;
pub mod load;
pub mod mem;
pub mod memswap;
pub mod network;
pub mod now;
pub mod percpu;
pub mod processcount;
pub mod processlist;
pub mod programlist;
pub mod psutilversion;
pub mod quicklook;
pub mod sensors;
pub mod system;
pub mod uptime;
pub mod version;
pub use core::CorePlugin;
pub use cpu::CpuPlugin;
pub use diskio::DiskioPlugin;
pub use fs::FsPlugin;
pub use irq::IrqPlugin;
pub use load::LoadPlugin;
pub use mem::MemPlugin;
pub use memswap::MemSwapPlugin;
pub use network::NetworkPlugin;
pub use now::NowPlugin;
pub use percpu::PerCpuPlugin;
pub use processcount::ProcessCountPlugin;
pub use processlist::ProcessListPlugin;
pub use programlist::ProgramListPlugin;
pub use psutilversion::PsutilVersionPlugin;
pub use quicklook::QuicklookPlugin;
pub use sensors::SensorsPlugin;
pub use system::SystemPlugin;
pub use uptime::UptimePlugin;
pub use version::VersionPlugin;
@@ -0,0 +1,139 @@
//! Network plugin.
use crate::collectors;
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct NetworkPlugin {
pub model: PluginModel,
hide_no_up: bool,
hide_no_ip: bool,
}
impl NetworkPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"interface_name" => Field::new("Interface name."),
"alias" => Field::new("Interface alias name (optional)."),
"bytes_recv" => Field::new("Number of bytes received.").rate().unit("byte"),
"bytes_sent" => Field::new("Number of bytes sent.").rate().unit("byte"),
"bytes_all" => Field::new("Number of bytes received and sent.").rate().unit("byte"),
"speed" => Field::new("Maximum interface speed (in bit per second). Can return 0 on some operating-system.").unit("bitpersecond"),
"is_up" => Field::new("Is the interface up ?").unit("bool"),
};
let items_history = vec![
HistoryItem::new("bytes_recv_rate_per_sec", "Download rate per second"),
HistoryItem::new("bytes_sent_rate_per_sec", "Upload rate per second"),
];
let mut model = PluginModel::new(
"network",
fields,
items_history,
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("interface_name");
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 }
}
}
impl Plugin for NetworkPlugin {
fn name(&self) -> &str {
"network"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("interface_name")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let net_dev = collectors::read_net_dev();
let mut stats = Vec::new();
for (interface_name, counters) in net_dev {
if !self.model.is_display(&interface_name, "") {
continue;
}
let is_up = collectors::interface_flags(&interface_name) & libc::IFF_UP as u64 != 0;
if self.hide_no_up && !is_up {
continue;
}
if self.hide_no_ip && !collectors::interface_has_ip(&interface_name) {
continue;
}
let speed = collectors::interface_speed(&interface_name);
let alias = self.model.has_alias(&interface_name);
let mut item = serde_json::Map::new();
item.insert("bytes_sent".to_string(), json!(counters.bytes_sent));
item.insert("bytes_recv".to_string(), json!(counters.bytes_recv));
// Note: psutil's field is `isup`, which does not match the
// `is_up` fields description, so the Python implementation drops
// it from the stats (the value is only used to filter interfaces).
item.insert("speed".to_string(), json!(speed * 1048576));
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));
stats.push(Value::Object(item));
}
Ok(Value::Array(stats))
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let items: Vec<(String, f64, f64, f64, Option<String>)> = match &self.model.stats {
Value::Array(list) => list
.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 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)))
})
.collect(),
_ => Vec::new(),
};
for (interface_name, rx, tx, speed, action_key) in items {
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);
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);
}
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);
}
for field in ["bytes_recv", "bytes_recv_rate_per_sec"] {
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);
}
}
}
fn display_curse(&self) -> bool {
true
}
}
+70
View File
@@ -0,0 +1,70 @@
//! Current date/time plugin.
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct NowPlugin {
pub model: PluginModel,
}
impl NowPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"custom" => Field::new("Current date in custom format."),
"iso" => Field::new("Current date in ISO 8601 format."),
};
let model = PluginModel::new(
"now",
fields,
Vec::new(),
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for NowPlugin {
fn name(&self) -> &str {
"now"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let now = chrono::Local::now();
let iso = 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()
};
let custom = if !strftime.is_empty() {
now.format(&strftime).to_string()
} else {
// Choose the tz format depending on the timezone name length.
let tz_name = now.format("%Z").to_string();
if tz_name.len() > 6 {
now.format("%Y-%m-%d %H:%M:%S %z").to_string()
} else {
now.format("%Y-%m-%d %H:%M:%S %Z").to_string()
}
};
Ok(json!({"iso": iso, "custom": custom}))
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,67 @@
//! Per-CPU plugin.
use crate::cpu_percent::cpu_percent;
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::Value;
pub struct PerCpuPlugin {
pub model: PluginModel,
}
impl PerCpuPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"cpu_number" => Field::new("CPU number"),
"total" => Field::new("Sum of CPU percentages (except idle) for current CPU number.").unit("percent"),
"system" => Field::new("Percent time spent in kernel space. System CPU time is the time spent running code in the Operating System kernel.").unit("percent"),
"user" => Field::new("CPU percent time spent in user space. User CPU time is the time spent on the processor running your program's code (or code in libraries).").unit("percent"),
"iowait" => Field::new("*(Linux)*: percent time spent by the CPU waiting for I/O operations to complete.").unit("percent"),
"idle" => Field::new("percent of CPU used by any program. Every program or task that runs on a computer system occupies a certain amount of processing time on the CPU. If the CPU has completed all tasks it is idle.").unit("percent"),
"irq" => Field::new("*(Linux and BSD)*: percent time spent servicing/handling hardware/software interrupts. Time servicing interrupts (hardware + software).").unit("percent"),
"nice" => Field::new("*(Unix)*: percent time occupied by user level processes with a positive nice value. The time the CPU has spent running users' processes that are *niced*.").unit("percent"),
"steal" => Field::new("*(Linux)*: percentage of time a virtual CPU waits for a real CPU while the hypervisor is servicing another virtual processor.").unit("percent"),
"guest" => Field::new("*(Linux)*: percent of time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.").unit("percent"),
"guest_nice" => Field::new("*(Linux)*: percent of time spent running a niced guest (virtual CPU).").unit("percent"),
"softirq" => Field::new("*(Linux)*: percent of time spent handling software interrupts.").unit("percent"),
"dpc" => Field::new("*(Windows)*: percent of time spent handling deferred procedure calls.").unit("percent"),
"interrupt" => Field::new("*(Windows)*: percent of time spent handling software interrupts.").unit("percent"),
};
let mut model = PluginModel::new(
"percpu",
fields,
Vec::new(),
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("cpu_number");
Self { model }
}
}
impl Plugin for PerCpuPlugin {
fn name(&self) -> &str {
"percpu"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("cpu_number")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut state = cpu_percent().lock().unwrap();
Ok(Value::Array(state.get_percpu().to_vec()))
}
}
@@ -0,0 +1,72 @@
//! Process count plugin. It is also in charge of updating the shared
//! process list (port of `glances.plugins.processcount`).
use crate::fields;
use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::processes::processes;
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct ProcessCountPlugin {
pub model: PluginModel,
}
impl ProcessCountPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"total" => Field::new("Total number of processes").unit("number"),
"running" => Field::new("Total number of running processes").unit("number"),
"sleeping" => Field::new("Total number of sleeping processes").unit("number"),
"thread" => Field::new("Total number of threads").unit("number"),
"pid_max" => Field::new("Maximum PID number").unit("number"),
};
let items_history = vec![
HistoryItem::new("total", "Total number of processes"),
HistoryItem::new("running", "Total number of running processes"),
HistoryItem::new("sleeping", "Total number of sleeping processes"),
HistoryItem::new("thread", "Total number of threads"),
];
let model = PluginModel::new(
"processcount",
fields,
items_history,
json!({"total": 0, "running": 0, "sleeping": 0, "thread": 0, "pid_max": null}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for ProcessCountPlugin {
fn name(&self) -> &str {
"processcount"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut procs = processes().lock().unwrap();
procs.update(ctx.cursor_position, ctx.args);
let count = &procs.processcount;
Ok(json!({
"total": count.total,
"running": count.running,
"sleeping": count.sleeping,
"thread": count.thread,
"pid_max": count.pid_max,
}))
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,115 @@
//! Process list plugin. The stats are collected by the processcount plugin.
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::processes::processes;
use glances_core::{Args, Config};
use serde_json::Value;
pub struct ProcessListPlugin {
pub model: PluginModel,
}
impl ProcessListPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"pid" => Field::new("Process identifier (ID)").unit("number"),
"name" => Field::new("Process name").unit("string"),
"cmdline" => Field::new("Command line with arguments").unit("list"),
"username" => Field::new("Process owner").unit("string"),
"num_threads" => Field::new("Number of threads").unit("number"),
"cpu_percent" => Field::new("Process CPU consumption (returned value can be > 100.0 in case of a process running multiple threads on different CPU cores)").unit("percent"),
"memory_percent" => Field::new("Process memory consumption").unit("percent"),
"memory_info" => Field::new("Process memory information (dict with rss, vms, shared, text, lib, data, dirty keys)").unit("byte"),
"status" => Field::new("Process status").unit("string"),
"nice" => Field::new("Process nice value").unit("number"),
"cpu_times" => Field::new("Process CPU times (dict with user, system, iowait keys)").unit("second"),
"gids" => Field::new("Process group IDs (dict with real, effective, saved keys)").unit("number"),
"io_counters" => Field::new("Process IO counters (list with read_count, write_count, read_bytes, write_bytes, io_tag keys)").unit("byte"),
"cpu_num" => Field::new("CPU core number where the process is currently executing (0-based indexing)").unit("number"),
};
let mut model = PluginModel::new(
"processlist",
fields,
Vec::new(),
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
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", "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", "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 let Some(sort_key) = args.sort_processes_key {
processes().lock().unwrap().set_sort_key(sort_key.as_str(), false);
}
Self { model }
}
}
impl Plugin for ProcessListPlugin {
fn name(&self) -> &str {
"processlist"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("pid")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
// The list is updated by the processcount plugin.
let procs = processes().lock().unwrap();
Ok(Value::Array(procs.get_list(false).to_vec()))
}
fn update_views(&mut self) {
// The views are built lazily (see the Python implementation): the UI
// computes its own decorations.
self.model.reset_views();
}
fn export(&self) -> Value {
Value::Array(processes().lock().unwrap().get_export().to_vec())
}
fn display_curse(&self) -> bool {
true
}
fn align(&self) -> glances_core::Align {
glances_core::Align::Bottom
}
}
#[allow(dead_code)]
fn unused(_: Value) {}
@@ -0,0 +1,70 @@
//! Program list plugin (processes aggregated by program name).
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::processes::processes;
use glances_core::{Args, Config};
use serde_json::Value;
pub struct ProgramListPlugin {
pub model: PluginModel,
}
impl ProgramListPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"name" => Field::new("Process name").unit("string"),
"cmdline" => Field::new("Command line with arguments").unit("list"),
"username" => Field::new("Process owner").unit("string"),
"nprocs" => Field::new("Number of children processes").unit("number"),
"num_threads" => Field::new("Number of threads").unit("number"),
"cpu_percent" => Field::new("Process CPU consumption").unit("percent"),
"memory_percent" => Field::new("Process memory consumption").unit("percent"),
"memory_info" => Field::new("Process memory information (dict with rss, vms, shared, text, lib, data, dirty keys)").unit("byte"),
"status" => Field::new("Process status").unit("string"),
"nice" => Field::new("Process nice value").unit("number"),
"cpu_times" => Field::new("Process CPU times (dict with user, system, iowait keys)").unit("second"),
"gids" => Field::new("Process group IDs (dict with real, effective, saved keys)").unit("number"),
"io_counters" => Field::new("Process IO counters (list with read_count, write_count, read_bytes, write_bytes, io_tag keys)").unit("byte"),
};
let mut model = PluginModel::new(
"programlist",
fields,
Vec::new(),
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("name");
Self { model }
}
}
impl Plugin for ProgramListPlugin {
fn name(&self) -> &str {
"programlist"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("name")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut procs = processes().lock().unwrap();
Ok(Value::Array(procs.get_list_as_programs().to_vec()))
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,47 @@
//! Psutil version plugin.
//!
//! The Rust rewrite does not use psutil; the plugin is kept for drop-in
//! compatibility of the stats schema and reports "N/A".
use crate::fields;
use glances_core::plugin::{Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct PsutilVersionPlugin {
pub model: PluginModel,
}
impl PsutilVersionPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let model = PluginModel::new(
"psutilversion",
fields! {},
Vec::new(),
Value::Null,
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for PsutilVersionPlugin {
fn name(&self) -> &str {
"psutilversion"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
Ok(json!("N/A"))
}
}
@@ -0,0 +1,183 @@
//! Quicklook plugin (condensed stats displayed in the top-left corner).
use crate::cpu_percent::cpu_percent;
use crate::fields;
use glances_core::plugin::{Decoration, Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct QuicklookPlugin {
pub model: PluginModel,
pub stats_list: Vec<String>,
pub max_cpu_display: usize,
}
const AVAILABLE_STATS_LIST: [&str; 6] = ["cpu", "mem", "swap", "load", "gpu_mem", "gpu_proc"];
const DEFAULT_STATS_LIST: [&str; 3] = ["cpu", "mem", "load"];
impl QuicklookPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"cpu" => Field::new("CPU percent usage").unit("percent"),
"percpu" => Field::new("PERCPU percent usage").unit("percent"),
"mem" => Field::new("MEM percent usage").unit("percent"),
"swap" => Field::new("SWAP percent usage").unit("percent"),
"load" => Field::new("LOAD percent usage").unit("percent").alert(),
"cpu_log_core" => Field::new("Number of logical CPU core").unit("number"),
"cpu_phys_core" => Field::new("Number of physical CPU core").unit("number"),
"cpu_name" => Field::new("CPU name"),
"cpu_hz_current" => Field::new("CPU current frequency").unit("hertz"),
"cpu_hz" => Field::new("CPU max frequency").unit("hertz"),
"gpu_mem" => Field::new("Average GPU Memory consumption").unit("percent"),
"gpu_proc" => Field::new("Average GPU Processor consumption").unit("percent"),
};
let items_history = vec![
HistoryItem::new("cpu", "CPU percent usage"),
HistoryItem::new("percpu", "PERCPU percent usage"),
HistoryItem::new("mem", "MEM percent usage"),
HistoryItem::new("swap", "SWAP percent usage"),
HistoryItem::new("load", "LOAD percent usage"),
HistoryItem::new("gpu_mem", "GPU memory percent usage"),
HistoryItem::new("gpu_proc", "GPU processor percent usage"),
];
let model = PluginModel::new(
"quicklook",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
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())) {
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 }
}
fn mhz_to_hz(value: Option<f64>) -> Option<f64> {
value.map(|v| v * 1_000_000.0)
}
}
impl Plugin for QuicklookPlugin {
fn name(&self) -> &str {
"quicklook"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut stats = serde_json::Map::new();
let (info, cpu_value, percpu_values) = {
let mut state = cpu_percent().lock().unwrap();
let info = state.get_info();
let cpu = state.get_cpu();
let percpu = state.get_percpu().to_vec();
(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".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));
let meminfo = crate::collectors::read_meminfo();
let total = meminfo.get("MemTotal").copied().unwrap_or(0) as f64;
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 }),
);
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
} else {
0.0
}),
);
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()));
let load = crate::collectors::read_loadavg().map(|(_, _, min15)| {
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);
stats.insert("gpu_proc".to_string(), Value::Null);
Ok(Value::Object(stats))
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
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);
self.model.set_view_decoration(None, stat_key, decoration);
}
}
// Store the list of stats to display and the per-CPU decorations.
self.model
.views
.insert("list".to_string(), json!(stats_list));
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 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()));
}
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();
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()));
}
}
}
self.model.views.insert("percpu_decoration".to_string(), Value::Object(percpu_decoration));
}
fn display_curse(&self) -> bool {
true
}
}
/// Return the style for a CPU percentage without the get_alert side effects.
fn decoration_for_percent(model: &PluginModel, percent: f64) -> Decoration {
let stat_name = model.get_stat_name(Some("cpu"), None).to_lowercase();
let critical = model.get_limit("critical", &stat_name).unwrap_or(0.0);
let warning = model.get_limit("warning", &stat_name).unwrap_or(0.0);
let careful = model.get_limit("careful", &stat_name).unwrap_or(0.0);
if critical > 0.0 && percent >= critical {
Decoration::Critical
} else if warning > 0.0 && percent >= warning {
Decoration::Warning
} else if careful > 0.0 && percent >= careful {
Decoration::Careful
} else {
Decoration::Default
}
}
@@ -0,0 +1,225 @@
//! Sensors plugin (temperatures, fans, batteries).
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config, Decoration};
use serde_json::{Value, json};
pub struct SensorsPlugin {
pub model: PluginModel,
}
impl SensorsPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"label" => Field::new("Sensor label"),
"unit" => Field::new("Sensor unit"),
"value" => Field::new("Sensor value").unit("number"),
"warning" => Field::new("System warning threshold (regardless of the user configuration in the glances.conf file)").unit("number"),
"critical" => Field::new("System critical threshold (regardless of the user configuration in the glances.conf file)").unit("number"),
"type" => Field::new("Sensor type (one of battery, temperature_core, fan_speed)"),
};
let mut model = PluginModel::new(
"sensors",
fields,
Vec::new(),
Value::Array(Vec::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
model.set_item_key("label");
Self { model }
}
}
/// Read all hwmon sensors.
fn read_hwmon() -> Vec<Value> {
let mut stats = Vec::new();
let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") else {
return stats;
};
for entry in entries.flatten() {
let dir = entry.path();
let Ok(files) = std::fs::read_dir(&dir) else {
continue;
};
for file in files.flatten() {
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"));
}
}
// 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"));
}
}
}
}
stats
}
/// Read battery information from `/sys/class/power_supply`.
fn read_batteries() -> Vec<Value> {
let mut stats = Vec::new();
let Ok(entries) = std::fs::read_dir("/sys/class/power_supply") else {
return stats;
};
let mut batteries: Vec<_> = entries.flatten().collect();
batteries.sort_by_key(|e| e.file_name());
for entry in batteries {
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);
if !is_battery {
continue;
}
let Some(value) = read_value(&dir.join("capacity")) else {
continue;
};
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));
}
stats.push(item);
}
stats
}
fn sensor_item(label: &str, unit: &str, value: f64, warning: Option<f64>, critical: Option<f64>, r#type: &str) -> Value {
json!({
"label": label,
"unit": unit,
"value": value,
"warning": warning,
"critical": critical,
"type": r#type,
"key": "label",
})
}
fn read_string(path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok().map(|v| v.trim().to_string())
}
fn read_value(path: &std::path::Path) -> Option<f64> {
read_string(path).and_then(|v| v.parse().ok())
}
fn read_label_value(dir: &std::path::Path, file: &str) -> Option<f64> {
read_value(&dir.join(file))
}
fn read_label(dir: &std::path::Path, file: &str, default: &str) -> String {
read_string(&dir.join(file)).unwrap_or_else(|| default.to_string())
}
impl Plugin for SensorsPlugin {
fn name(&self) -> &str {
"sensors"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn get_key(&self) -> Option<&'static str> {
Some("label")
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut stats = read_hwmon();
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();
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()),
) {
if let Some(alias) = self.model.has_alias(&format!("{}_{}", label, r#type)) {
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));
}
}
}
// Sort by label.
stats.sort_by_key(|stat| stat.get("label").and_then(Value::as_str).unwrap_or("").to_lowercase());
Ok(Value::Array(stats))
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let items: Vec<(String, String, f64, Option<f64>, Option<f64>)> = match &self.model.stats {
Value::Array(list) => list
.iter()
.filter_map(|item| {
let label = item.get("label")?.as_str()?.to_string();
let r#type = item.get("type")?.as_str()?.to_string();
let value = item.get("value")?.as_f64()?;
let warning = item.get("warning").and_then(Value::as_f64);
let critical = item.get("critical").and_then(Value::as_f64);
Some((label, r#type, value, warning, critical))
})
.collect(),
_ => Vec::new(),
};
for (label, r#type, value, warning, critical) in items {
let decoration = if r#type == "temperature_core" {
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)
}
} else if r#type == "battery" {
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.set_view_decoration(Some(&label), "value", decoration);
}
}
fn display_curse(&self) -> bool {
true
}
}
fn system_thresholds(value: f64, warning: Option<f64>, critical: Option<f64>) -> Decoration {
match critical {
None => Decoration::Default,
Some(critical) if value >= critical => Decoration::Critical,
Some(_) => match warning {
None => Decoration::Default,
Some(warning) if value >= warning => Decoration::Warning,
Some(_) => Decoration::Ok,
},
}
}
@@ -0,0 +1,85 @@
//! Host/system information plugin.
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct SystemPlugin {
pub model: PluginModel,
}
impl SystemPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"os_name" => Field::new("Operating system name"),
"hostname" => Field::new("Hostname"),
"platform" => Field::new("Platform (32 or 64 bits)"),
"linux_distro" => Field::new("Linux distribution"),
"os_version" => Field::new("Operating system version"),
"hr_name" => Field::new("Human readable operating system name"),
};
let model = PluginModel::new(
"system",
fields,
Vec::new(),
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for SystemPlugin {
fn name(&self) -> &str {
"system"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let os_name = "Linux";
let hostname = crate::collectors::hostname();
let platform = format!("{}bit", std::mem::size_of::<usize>() * 8);
let os_version = std::fs::read_to_string("/proc/sys/kernel/osrelease")
.map(|v| v.trim().to_string())
.unwrap_or_default();
let (distro_name, distro_version) = crate::collectors::read_os_release();
let linux_distro = if distro_version.is_empty() {
distro_name.clone()
} else {
format!("{distro_name} {distro_version}")
};
let hr_name = format!("{linux_distro} {platform} / {os_name} {os_version}");
Ok(json!({
"os_name": os_name,
"hostname": hostname,
"platform": platform,
"linux_distro": linux_distro,
"os_version": os_version,
"hr_name": hr_name,
}))
}
fn display_curse(&self) -> bool {
true
}
fn align(&self) -> glances_core::Align {
glances_core::Align::Right
}
}
/// Logical core count helper shared by the load plugin.
pub fn log_core() -> usize {
crate::collectors::logical_core_count()
}
@@ -0,0 +1,70 @@
//! Uptime plugin.
use crate::fields;
use glances_core::plugin::{Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct UptimePlugin {
pub model: PluginModel,
}
impl UptimePlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let model = PluginModel::new(
"uptime",
fields! {},
Vec::new(),
Value::String(String::new()),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
/// Format a duration in seconds the way Python's `str(timedelta)` does.
pub fn format_uptime(seconds: f64) -> String {
let total = seconds.max(0.0) as i64;
let days = total / 86400;
let hours = (total % 86400) / 3600;
let minutes = (total % 3600) / 60;
let secs = total % 60;
if days > 0 {
format!(
"{} day{}, {}:{:02}:{:02}",
days,
if days > 1 { "s" } else { "" },
hours,
minutes,
secs
)
} else {
format!("{hours}:{minutes:02}:{secs:02}")
}
}
impl Plugin for UptimePlugin {
fn name(&self) -> &str {
"uptime"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let uptime = crate::collectors::uptime_seconds().unwrap_or(0.0);
Ok(json!(format_uptime(uptime)))
}
fn display_curse(&self) -> bool {
true
}
}
@@ -0,0 +1,48 @@
//! Glances version plugin.
use crate::fields;
use glances_core::plugin::{Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
/// Version reported to be compatible with the Python Glances implementation
/// this rewrite is based on.
pub const COMPAT_VERSION: &str = "4.5.7";
pub struct VersionPlugin {
pub model: PluginModel,
}
impl VersionPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let model = PluginModel::new(
"version",
fields! {},
Vec::new(),
Value::Null,
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
Self { model }
}
}
impl Plugin for VersionPlugin {
fn name(&self) -> &str {
"version"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
Ok(json!(COMPAT_VERSION))
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "glances-tui"
version = "0.1.0"
edition = "2024"
[dependencies]
crossterm = "0.29.0"
glances-core = { version = "0.1.0", path = "../glances-core" }
glances-plugins = { version = "0.1.0", path = "../glances-plugins" }
indexmap.workspace = true
ratatui = "0.30.2"
serde_json.workspace = true
tracing.workspace = true
+14
View File
@@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Glances (Python) stats oracle.
Prints one JSON line per iteration with the raw stats of the requested
plugins. It uses the Python Glances plugin code directly so that the
comparison harness can observe the same number of iterations as the Rust
implementation (`--stdout-json` exits after one line in Glances 4.5.7).
Usage:
tools/glances_oracle.py --iterations N --refresh T --plugins cpu,mem
"""
import argparse
import json
import sys
import time
parser = argparse.ArgumentParser()
parser.add_argument("--iterations", type=int, default=3)
parser.add_argument("--refresh", type=float, default=1.0)
parser.add_argument("--plugins", required=True)
parser.add_argument("--config", default=None)
args = parser.parse_args()
plugins = [p.strip() for p in args.plugins.split(",") if p.strip()]
sys.argv = ["glances", "-t", str(args.refresh), "--disable-plugin", "all", "--enable-plugin", args.plugins]
if args.config:
sys.argv += ["-C", args.config]
from glances.main import GlancesMain # noqa: E402
from glances.stats import GlancesStats # noqa: E402
core = GlancesMain()
stats = GlancesStats(config=core.get_config(), args=core.get_args())
for _ in range(args.iterations):
start = time.time()
stats.update()
line = {}
for name in plugins:
plugin = stats.get_plugin(name)
if plugin is not None:
line[name] = plugin.get_raw()
print(json.dumps(line), flush=True)
elapsed = time.time() - start
time.sleep(max(0.0, args.refresh - elapsed))
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Differential test harness: compares glances-rs stdout-json output with the
Python Glances implementation (the reference oracle).
Usage:
tools/parity.py [--rust BIN] [--python BIN] [--iterations N] [plugin ...]
With no plugin list, a default set of core plugins is tested.
"""
import argparse
import json
import os
import subprocess
import sys
DEFAULT_PLUGINS = [
"cpu",
"percpu",
"mem",
"memswap",
"load",
"fs",
"network",
"diskio",
"sensors",
"now",
"uptime",
"system",
"processcount",
"version",
"psutilversion",
"quicklook",
]
def flatten(value, prefix=""):
out = {}
if isinstance(value, dict):
for key, item in value.items():
out.update(flatten(item, f"{prefix}{key}."))
elif isinstance(value, list):
key_field = value[0].get("key") if value and isinstance(value[0], dict) else None
for index, item in enumerate(value):
identity = item.get(key_field) if key_field and isinstance(item, dict) else index
out.update(flatten(item, f"{prefix}{identity}."))
else:
out[prefix[:-1]] = value
return out
def type_name(value):
if value is None:
return "null"
if isinstance(value, bool):
return "bool"
if isinstance(value, (int, float)):
return "number"
if isinstance(value, str):
return "string"
if isinstance(value, list):
return "array"
if isinstance(value, dict):
return "object"
return type(value).__name__
def run(binary, plugins, iterations, extra):
plugins_arg = ",".join(plugins)
cmd = [
binary,
"--stdout-json",
plugins_arg,
"--disable-plugin",
"all",
"--enable-plugin",
plugins_arg,
"--stop-after",
str(iterations),
"-t",
"1",
"--disable-check-update",
] + extra
env = dict(os.environ)
env["PYTHONUNBUFFERED"] = "1"
result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120)
if result.returncode != 0:
return None, result.stderr.strip().splitlines()[-3:]
lines = [line for line in result.stdout.splitlines() if line.strip()]
if not lines:
return None, ["no output"]
return json.loads(lines[-1]), None
ORACLE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "glances_oracle.py")
def run_python(python, conf, plugins, iterations, extra):
plugins_arg = ",".join(plugins)
cmd = [
python,
ORACLE,
"--iterations",
str(iterations),
"--refresh",
"1",
"--plugins",
plugins_arg,
]
if conf:
cmd += ["--config", conf]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
return None, result.stderr.strip().splitlines()[-3:]
lines = [line for line in result.stdout.splitlines() if line.strip()]
if not lines:
return None, ["no output"]
return json.loads(lines[-1]), None
def compare(plugin, rust, python, errors):
rust_plugin = rust.get(plugin, {})
python_plugin = python.get(plugin, {})
rust_flat = flatten(rust_plugin)
python_flat = flatten(python_plugin)
if plugin not in rust:
errors.append(f"{plugin}: missing in Rust output")
return
if plugin not in python:
return
for key in sorted(set(rust_flat) | set(python_flat)):
if key not in python_flat:
errors.append(f"{plugin}.{key}: only in Rust ({rust_flat[key]!r})")
elif key not in rust_flat:
errors.append(f"{plugin}.{key}: only in Python ({python_flat[key]!r})")
else:
rust_type = type_name(rust_flat[key])
python_type = type_name(python_flat[key])
if rust_type != python_type and not (
rust_type == "null" or python_type == "null"
):
errors.append(
f"{plugin}.{key}: type mismatch Rust={rust_type} Python={python_type} "
f"({rust_flat[key]!r} vs {python_flat[key]!r})"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--rust", default="./target/debug/glances")
parser.add_argument("--python", default="/tmp/opencode/glances-py/bin/python")
parser.add_argument("--config", default="conf/glances.conf")
parser.add_argument("--iterations", type=int, default=3)
parser.add_argument("plugins", nargs="*")
args = parser.parse_args()
plugins = args.plugins or DEFAULT_PLUGINS
errors = []
for plugin in plugins:
extra = ["-C", args.config] if args.config else []
rust, rust_error = run(args.rust, [plugin], args.iterations, extra)
if rust_error is not None:
errors.append(f"{plugin}: Rust run failed: {rust_error}")
continue
python, python_error = run_python(args.python, args.config, [plugin], args.iterations, [])
if python_error is not None:
errors.append(f"{plugin}: Python run failed: {python_error}")
continue
compare(plugin, rust, python, errors)
status = "OK" if not any(e.startswith(f"{plugin}.") or e.startswith(f"{plugin}:") for e in errors) else "DIFF"
print(f" {plugin}: {status}")
if errors:
print("\nDifferences:")
for error in errors:
print(f" - {error}")
return 1
print("\nNo differences found")
return 0
if __name__ == "__main__":
sys.exit(main())