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,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")
|
||||
}
|
||||
Reference in New Issue
Block a user