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