M3: extended plugins
- folders, amps, ports, raid, wifi, ip (drop-in compatible stats) - differential tests pass for all 22 implemented plugins - TUI left sidebar extended
This commit is contained in:
@@ -15,3 +15,4 @@ serde_json.workspace = true
|
||||
sysinfo = "0.39.6"
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
ureq = { version = "3.4.2", features = ["json"] }
|
||||
|
||||
@@ -559,6 +559,129 @@ pub fn is_admin() -> bool {
|
||||
unsafe { libc::getuid() == 0 }
|
||||
}
|
||||
|
||||
/// Return the current IP address and netmask.
|
||||
pub fn get_ip_address() -> (Option<String>, Option<String>) {
|
||||
// Ask the kernel which source address it would use for default-route
|
||||
// traffic (no packet is sent).
|
||||
let mut ip_address: Option<String> = None;
|
||||
unsafe {
|
||||
let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
|
||||
if sock >= 0 {
|
||||
let mut addr: libc::sockaddr_in = std::mem::zeroed();
|
||||
addr.sin_family = libc::AF_INET as u16;
|
||||
addr.sin_port = 53u16.to_be();
|
||||
addr.sin_addr.s_addr = u32::from_be_bytes([192, 0, 2, 1]).to_be();
|
||||
if libc::connect(sock, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in>() as u32) == 0 {
|
||||
let mut local: libc::sockaddr_in = std::mem::zeroed();
|
||||
let mut len = std::mem::size_of::<libc::sockaddr_in>() as u32;
|
||||
if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len) == 0 {
|
||||
let bytes = local.sin_addr.s_addr.to_ne_bytes();
|
||||
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
if !candidate.starts_with("127.") && candidate != "0.0.0.0" {
|
||||
ip_address = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
libc::close(sock);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ip) = &ip_address {
|
||||
// Find the matching netmask with getifaddrs.
|
||||
let mut netmask = None;
|
||||
unsafe {
|
||||
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
|
||||
if libc::getifaddrs(&mut ifap) == 0 {
|
||||
let mut current = ifap;
|
||||
while !current.is_null() {
|
||||
let addr = (*current).ifa_addr;
|
||||
let mask = (*current).ifa_netmask;
|
||||
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET {
|
||||
let sin = addr as *const libc::sockaddr_in;
|
||||
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes();
|
||||
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
if &candidate == ip && !mask.is_null() {
|
||||
let sin_mask = mask as *const libc::sockaddr_in;
|
||||
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes();
|
||||
netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
current = (*current).ifa_next;
|
||||
}
|
||||
libc::freeifaddrs(ifap);
|
||||
}
|
||||
}
|
||||
return (ip_address, netmask);
|
||||
}
|
||||
|
||||
// Fallback: first up non-loopback interface with an address.
|
||||
let mut found = (None, None);
|
||||
unsafe {
|
||||
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
|
||||
if libc::getifaddrs(&mut ifap) == 0 {
|
||||
let mut current = ifap;
|
||||
while !current.is_null() {
|
||||
let addr = (*current).ifa_addr;
|
||||
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET {
|
||||
let name = std::ffi::CStr::from_ptr((*current).ifa_name).to_string_lossy().to_string();
|
||||
let flags = (*current).ifa_flags as u32;
|
||||
if name != "lo" && flags & libc::IFF_UP as u32 != 0 {
|
||||
let sin = addr as *const libc::sockaddr_in;
|
||||
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes();
|
||||
let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
let mask = (*current).ifa_netmask;
|
||||
let mut netmask = None;
|
||||
if !mask.is_null() {
|
||||
let sin_mask = mask as *const libc::sockaddr_in;
|
||||
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes();
|
||||
netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3]));
|
||||
}
|
||||
found = (Some(candidate), netmask);
|
||||
break;
|
||||
}
|
||||
}
|
||||
current = (*current).ifa_next;
|
||||
}
|
||||
libc::freeifaddrs(ifap);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Return the default gateway IP address (from `/proc/net/route`).
|
||||
pub fn get_default_gateway() -> Option<String> {
|
||||
let content = std::fs::read_to_string("/proc/net/route").ok()?;
|
||||
for line in content.lines().skip(1) {
|
||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||
if fields.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
if fields[1] == "00000000" {
|
||||
let gateway = fields[2];
|
||||
if gateway.len() == 8 {
|
||||
let bytes: Vec<String> = (0..4)
|
||||
.map(|i| u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16).unwrap_or(0).to_string())
|
||||
.collect();
|
||||
return Some(bytes.join("."));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Convert a netmask to CIDR notation (port of `ip_to_cidr`).
|
||||
pub fn ip_to_cidr(ip: Option<&str>) -> u32 {
|
||||
match ip {
|
||||
None => 0,
|
||||
Some(ip) => ip
|
||||
.split('.')
|
||||
.filter_map(|part| part.parse::<u32>().ok())
|
||||
.map(|part| part.count_ones())
|
||||
.sum(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
|
||||
@@ -27,10 +27,13 @@ impl Engine {
|
||||
// Registration order mirrors the alphabetical plugin loading of the
|
||||
// Python implementation (in particular processcount runs before
|
||||
// processlist and programlist).
|
||||
plugins.insert("amps".to_string(), Box::new(AmpsPlugin::new(&config, &args)));
|
||||
plugins.insert("core".to_string(), Box::new(CorePlugin::new(&config, &args)));
|
||||
plugins.insert("cpu".to_string(), Box::new(CpuPlugin::new(&config, &args)));
|
||||
plugins.insert("diskio".to_string(), Box::new(DiskioPlugin::new(&config, &args)));
|
||||
plugins.insert("folders".to_string(), Box::new(FoldersPlugin::new(&config, &args)));
|
||||
plugins.insert("fs".to_string(), Box::new(FsPlugin::new(&config, &args)));
|
||||
plugins.insert("ip".to_string(), Box::new(IpPlugin::new(&config, &args)));
|
||||
plugins.insert("irq".to_string(), Box::new(IrqPlugin::new(&config, &args)));
|
||||
plugins.insert("load".to_string(), Box::new(LoadPlugin::new(&config, &args)));
|
||||
plugins.insert("mem".to_string(), Box::new(MemPlugin::new(&config, &args)));
|
||||
@@ -38,15 +41,18 @@ impl Engine {
|
||||
plugins.insert("network".to_string(), Box::new(NetworkPlugin::new(&config, &args)));
|
||||
plugins.insert("now".to_string(), Box::new(NowPlugin::new(&config, &args)));
|
||||
plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::new(&config, &args)));
|
||||
plugins.insert("ports".to_string(), Box::new(PortsPlugin::new(&config, &args)));
|
||||
plugins.insert("processcount".to_string(), Box::new(ProcessCountPlugin::new(&config, &args)));
|
||||
plugins.insert("processlist".to_string(), Box::new(ProcessListPlugin::new(&config, &args)));
|
||||
plugins.insert("programlist".to_string(), Box::new(ProgramListPlugin::new(&config, &args)));
|
||||
plugins.insert("psutilversion".to_string(), Box::new(PsutilVersionPlugin::new(&config, &args)));
|
||||
plugins.insert("quicklook".to_string(), Box::new(QuicklookPlugin::new(&config, &args)));
|
||||
plugins.insert("raid".to_string(), Box::new(RaidPlugin::new(&config, &args)));
|
||||
plugins.insert("sensors".to_string(), Box::new(SensorsPlugin::new(&config, &args)));
|
||||
plugins.insert("system".to_string(), Box::new(SystemPlugin::new(&config, &args)));
|
||||
plugins.insert("uptime".to_string(), Box::new(UptimePlugin::new(&config, &args)));
|
||||
plugins.insert("version".to_string(), Box::new(VersionPlugin::new(&config, &args)));
|
||||
plugins.insert("wifi".to_string(), Box::new(WifiPlugin::new(&config, &args)));
|
||||
// The refresh time is set on the model, which was built with the
|
||||
// command line value already.
|
||||
let _ = refresh_time;
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
//! AMPs plugin: run user-defined commands and report their result.
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::processes::processes;
|
||||
use glances_core::secure::secure_popen;
|
||||
use glances_core::{Args, Config};
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct AmpsPlugin {
|
||||
pub model: PluginModel,
|
||||
amps: Vec<AmpConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AmpConfig {
|
||||
name: String,
|
||||
command: Option<String>,
|
||||
regex: Option<regex::Regex>,
|
||||
refresh: f64,
|
||||
countmin: Option<f64>,
|
||||
countmax: Option<f64>,
|
||||
one_line: bool,
|
||||
enabled: bool,
|
||||
timer: Instant,
|
||||
result: Option<String>,
|
||||
count: i64,
|
||||
}
|
||||
|
||||
impl AmpsPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let fields = fields! {
|
||||
"name" => Field::new("AMP name."),
|
||||
"result" => Field::new("AMP result (a string)."),
|
||||
"refresh" => Field::new("AMP refresh interval.").unit("second"),
|
||||
"timer" => Field::new("Time until next refresh.").unit("second"),
|
||||
"count" => Field::new("Number of matching processes.").unit("number"),
|
||||
"countmin" => Field::new("Minimum number of matching processes.").unit("number"),
|
||||
"countmax" => Field::new("Maximum number of matching processes.").unit("number"),
|
||||
};
|
||||
let mut model = PluginModel::new(
|
||||
"amps",
|
||||
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");
|
||||
|
||||
let mut amps = Vec::new();
|
||||
for section in config.sections() {
|
||||
if !section.starts_with("amp_") {
|
||||
continue;
|
||||
}
|
||||
let command = config.get_value(section, "command", None);
|
||||
let enabled = config.get_bool_value(section, "enable", false);
|
||||
let regex = config
|
||||
.get_value(section, "regex", None)
|
||||
.and_then(|pattern| regex::Regex::new(&pattern).ok());
|
||||
let refresh = config.get_float_value(section, "refresh", 3.0);
|
||||
let countmin = config.get_value(section, "countmin", None).and_then(|v| v.parse::<f64>().ok());
|
||||
let countmax = config.get_value(section, "countmax", None).and_then(|v| v.parse::<f64>().ok());
|
||||
let one_line = config.get_bool_value(section, "one_line", false);
|
||||
amps.push(AmpConfig {
|
||||
name: amp_display_name(section),
|
||||
enabled,
|
||||
command,
|
||||
regex,
|
||||
refresh,
|
||||
countmin,
|
||||
countmax,
|
||||
one_line,
|
||||
timer: Instant::now(),
|
||||
result: None,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
Self { model, amps }
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the AMP display name (mirrors the Python AMP module names).
|
||||
fn amp_display_name(section: &str) -> String {
|
||||
let raw = section.trim_start_matches("amp_");
|
||||
match raw {
|
||||
"systemv" => "SystemV".to_string(),
|
||||
"systemd" => "Systemd".to_string(),
|
||||
"nginx" => "Nginx".to_string(),
|
||||
other => {
|
||||
let mut chars = other.chars();
|
||||
match chars.next() {
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for AmpsPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"amps"
|
||||
}
|
||||
|
||||
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 stats = Vec::new();
|
||||
for amp in self.amps.iter_mut() {
|
||||
if amp.timer.elapsed().as_secs_f64() >= amp.refresh && amp.enabled {
|
||||
let result = match &.command {
|
||||
Some(command) => secure_popen(command, !ctx.args.disable_config_exec, None),
|
||||
None => String::new(),
|
||||
};
|
||||
amp.result = Some(if amp.one_line {
|
||||
result.lines().next().unwrap_or("").to_string()
|
||||
} else {
|
||||
result.trim_end().to_string()
|
||||
});
|
||||
amp.timer = Instant::now();
|
||||
}
|
||||
if let Some(regex) = &.regex {
|
||||
let procs = processes().lock().unwrap();
|
||||
amp.count = procs
|
||||
.get_list(false)
|
||||
.iter()
|
||||
.filter(|process| {
|
||||
let name = process.get("name").and_then(Value::as_str).unwrap_or("");
|
||||
let cmdline = process
|
||||
.get("cmdline")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(" "))
|
||||
.unwrap_or_default();
|
||||
regex.is_match(name) || regex.is_match(&cmdline)
|
||||
})
|
||||
.count() as i64;
|
||||
}
|
||||
let timer_left = (amp.refresh - amp.timer.elapsed().as_secs_f64()).max(0.0);
|
||||
stats.push(json!({
|
||||
"key": "name",
|
||||
"name": amp.name,
|
||||
"result": amp.result,
|
||||
"refresh": amp.refresh,
|
||||
"timer": timer_left,
|
||||
"count": amp.count,
|
||||
"countmin": amp.countmin,
|
||||
"countmax": amp.countmax,
|
||||
"regex": amp.regex.is_some(),
|
||||
}));
|
||||
}
|
||||
Ok(Value::Array(stats))
|
||||
}
|
||||
|
||||
fn update_views(&mut self) {
|
||||
let key = self.get_key();
|
||||
self.model.update_views(key);
|
||||
let items: Vec<(String, i64, Option<i64>, Option<i64>, bool)> = match &self.model.stats {
|
||||
Value::Array(list) => list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let name = item.get("name")?.as_str()?.to_string();
|
||||
let count = item.get("count")?.as_i64()?;
|
||||
let countmin = item.get("countmin").and_then(Value::as_i64);
|
||||
let countmax = item.get("countmax").and_then(Value::as_i64);
|
||||
let regex = item.get("regex").and_then(Value::as_bool).unwrap_or(false);
|
||||
Some((name, count, countmin, countmax, regex))
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (name, count, countmin, countmax, regex) in items {
|
||||
let decoration = if !regex {
|
||||
glances_core::Decoration::Default
|
||||
} else if countmax.is_some_and(|max| count > max) {
|
||||
glances_core::Decoration::Critical
|
||||
} else if countmin.is_some_and(|min| count < min) {
|
||||
glances_core::Decoration::Warning
|
||||
} else {
|
||||
glances_core::Decoration::Ok
|
||||
};
|
||||
self.model.set_view_decoration(Some(&name), "count", decoration);
|
||||
}
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
|
||||
return ret;
|
||||
}
|
||||
let name_max_width = ctx.max_width.saturating_sub(12);
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", "AMPS", width = name_max_width),
|
||||
glances_core::Decoration::Title,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{:>9}", "Count"), glances_core::Decoration::Default, false, false, false));
|
||||
let items = self.model.sorted_stats();
|
||||
let Some(items) = items.as_array() else {
|
||||
return ret;
|
||||
};
|
||||
for i in items {
|
||||
let name = i.get("name").and_then(Value::as_str).unwrap_or("");
|
||||
let count = i.get("count").and_then(Value::as_i64).unwrap_or(0);
|
||||
let result = i.get("result").and_then(Value::as_str).unwrap_or("");
|
||||
let display_name = if result.is_empty() { name.to_string() } else { format!("{name}: {result}") };
|
||||
ret.push(self.model.curse_new_line());
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", display_name.chars().take(name_max_width).collect::<String>(), width = name_max_width),
|
||||
glances_core::Decoration::Default,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{count:>9}"),
|
||||
self.model.view_decoration(Some(name), "count"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Folders plugin: monitor the size of configured directories.
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::{Args, Config};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct FoldersPlugin {
|
||||
pub model: PluginModel,
|
||||
folders: Vec<FolderConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FolderConfig {
|
||||
indice: String,
|
||||
path: String,
|
||||
refresh: f64,
|
||||
careful: Option<f64>,
|
||||
warning: Option<f64>,
|
||||
critical: Option<f64>,
|
||||
timer: Instant,
|
||||
size: u64,
|
||||
errno: i64,
|
||||
}
|
||||
|
||||
impl FoldersPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let fields = fields! {
|
||||
"path" => Field::new("Absolute path."),
|
||||
"size" => Field::new("Folder size in bytes.").unit("byte"),
|
||||
"refresh" => Field::new("Refresh interval in seconds.").unit("second"),
|
||||
"errno" => Field::new("Return code when retrieving folder size (0 is no error).").unit("number"),
|
||||
"careful" => Field::new("Careful threshold in MB.").unit("megabyte"),
|
||||
"warning" => Field::new("Warning threshold in MB.").unit("megabyte"),
|
||||
"critical" => Field::new("Critical threshold in MB.").unit("megabyte"),
|
||||
};
|
||||
let mut model = PluginModel::new(
|
||||
"folders",
|
||||
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("path");
|
||||
|
||||
let mut folders = Vec::new();
|
||||
if config.has_section("folders") {
|
||||
let default_refresh = config.get_float_value("folders", "refresh", 60.0);
|
||||
for index in 1..=10 {
|
||||
let key = format!("folder_{index}_");
|
||||
let Some(path) = config.get_value("folders", &format!("{key}path"), None) else {
|
||||
continue;
|
||||
};
|
||||
if path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let refresh = config.get_float_value("folders", &format!("{key}refresh"), default_refresh);
|
||||
let threshold = |name: &str| -> Option<f64> {
|
||||
config.get_value("folders", &format!("{key}{name}"), None).and_then(|v| v.parse().ok())
|
||||
};
|
||||
folders.push(FolderConfig {
|
||||
indice: index.to_string(),
|
||||
path,
|
||||
refresh,
|
||||
careful: threshold("careful"),
|
||||
warning: threshold("warning"),
|
||||
critical: threshold("critical"),
|
||||
timer: Instant::now() - std::time::Duration::from_secs(3600),
|
||||
size: 0,
|
||||
errno: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
Self { model, folders }
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for FoldersPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"folders"
|
||||
}
|
||||
|
||||
fn model(&self) -> &PluginModel {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn model_mut(&mut self) -> &mut PluginModel {
|
||||
&mut self.model
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Option<&'static str> {
|
||||
Some("path")
|
||||
}
|
||||
|
||||
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
|
||||
let mut stats = Vec::new();
|
||||
for folder in self.folders.iter_mut() {
|
||||
if folder.timer.elapsed().as_secs_f64() >= folder.refresh {
|
||||
let (size, errno) = folder_size(Path::new(&folder.path), 0);
|
||||
folder.size = size;
|
||||
folder.errno = errno;
|
||||
folder.timer = Instant::now();
|
||||
}
|
||||
let mut item = serde_json::Map::new();
|
||||
item.insert("path".to_string(), json!(folder.path));
|
||||
item.insert("size".to_string(), json!(folder.size));
|
||||
item.insert("refresh".to_string(), json!(folder.refresh as i64));
|
||||
item.insert("errno".to_string(), json!(folder.errno));
|
||||
item.insert("careful".to_string(), json!(folder.careful));
|
||||
item.insert("warning".to_string(), json!(folder.warning));
|
||||
item.insert("critical".to_string(), json!(folder.critical));
|
||||
item.insert("key".to_string(), json!("path"));
|
||||
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, i64, u64, Option<f64>, Option<f64>, Option<f64>)> = match &self.model.stats {
|
||||
Value::Array(list) => list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let path = item.get("path")?.as_str()?.to_string();
|
||||
let errno = item.get("errno")?.as_i64()?;
|
||||
let size = item.get("size")?.as_u64()?;
|
||||
let careful = item.get("careful").and_then(Value::as_f64);
|
||||
let warning = item.get("warning").and_then(Value::as_f64);
|
||||
let critical = item.get("critical").and_then(Value::as_f64);
|
||||
Some((path, errno, size, careful, warning, critical))
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (path, errno, size, careful, warning, critical) in items {
|
||||
if errno != 0 {
|
||||
continue;
|
||||
}
|
||||
let decoration = if critical.is_some_and(|c| size as f64 > c * 1_000_000.0) {
|
||||
glances_core::Decoration::Critical
|
||||
} else if warning.is_some_and(|c| size as f64 > c * 1_000_000.0) {
|
||||
glances_core::Decoration::Warning
|
||||
} else if careful.is_some_and(|c| size as f64 > c * 1_000_000.0) {
|
||||
glances_core::Decoration::Careful
|
||||
} else {
|
||||
glances_core::Decoration::Default
|
||||
};
|
||||
self.model.set_view_decoration(Some(&path), "size", decoration);
|
||||
}
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
|
||||
return ret;
|
||||
}
|
||||
let name_max_width = ctx.max_width.saturating_sub(12);
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", "FOLDERS", width = name_max_width),
|
||||
glances_core::Decoration::Title,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{:>9}", "Size"), glances_core::Decoration::Default, false, false, false));
|
||||
let items = self.model.sorted_stats();
|
||||
let Some(items) = items.as_array() else {
|
||||
return ret;
|
||||
};
|
||||
for i in items {
|
||||
let path = i.get("path").and_then(Value::as_str).unwrap_or("");
|
||||
let size = i.get("size").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let errno = i.get("errno").and_then(Value::as_i64).unwrap_or(0);
|
||||
ret.push(self.model.curse_new_line());
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", path.chars().take(name_max_width).collect::<String>(), width = name_max_width),
|
||||
glances_core::Decoration::Default,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
let msg = if errno != 0 {
|
||||
format!("?{:>8}", self.model.auto_unit(size, false, "K"))
|
||||
} else {
|
||||
format!("{:>9}", self.model.auto_unit(size, false, "K"))
|
||||
};
|
||||
ret.push(self.model.curse_add_line(
|
||||
msg,
|
||||
self.model.view_decoration(Some(path), "size"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursive folder size (port of `folder_size`).
|
||||
fn folder_size(path: &Path, mut errno: i64) -> (u64, i64) {
|
||||
let mut size = 0u64;
|
||||
let entries = match std::fs::read_dir(path) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => return (0, e.raw_os_error().unwrap_or(0) as i64),
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let entry_path = entry.path();
|
||||
match entry.file_type() {
|
||||
Ok(file_type) if file_type.is_dir() => {
|
||||
let (sub_size, sub_errno) = folder_size(&entry_path, errno);
|
||||
size += sub_size;
|
||||
errno = sub_errno;
|
||||
}
|
||||
Ok(_) => match entry.metadata() {
|
||||
Ok(metadata) => size += metadata.len(),
|
||||
Err(e) => errno = e.raw_os_error().unwrap_or(0) as i64,
|
||||
},
|
||||
Err(e) => errno = e.raw_os_error().unwrap_or(0) as i64,
|
||||
}
|
||||
}
|
||||
(size, errno)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! IP plugin: private and (optionally) public IP information.
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::{Args, Config};
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct IpPlugin {
|
||||
pub model: PluginModel,
|
||||
public_api: Option<String>,
|
||||
public_field: String,
|
||||
public_template: String,
|
||||
public_disabled: bool,
|
||||
public_username: Option<String>,
|
||||
public_password: Option<String>,
|
||||
public_interval: f64,
|
||||
public_timer: Instant,
|
||||
public_address: String,
|
||||
public_info: Option<Value>,
|
||||
}
|
||||
|
||||
impl IpPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let fields = fields! {
|
||||
"address" => Field::new("Private IP address"),
|
||||
"mask" => Field::new("Private IP mask"),
|
||||
"mask_cidr" => Field::new("Private IP mask in CIDR format").unit("number"),
|
||||
"gateway" => Field::new("Private IP gateway"),
|
||||
"public_address" => Field::new("Public IP address"),
|
||||
"public_info_human" => Field::new("Public IP information"),
|
||||
};
|
||||
let model = PluginModel::new(
|
||||
"ip",
|
||||
fields,
|
||||
Vec::new(),
|
||||
json!({}),
|
||||
config,
|
||||
args.time.unwrap_or(2.0),
|
||||
args.disable_history,
|
||||
args.disable_config_exec,
|
||||
);
|
||||
let public_api = model.get_conf_value("public_api", "").first().cloned();
|
||||
let public_disabled = model
|
||||
.get_conf_value("public_disabled", "")
|
||||
.first()
|
||||
.map(|v| v.to_lowercase() != "false")
|
||||
.unwrap_or(true)
|
||||
|| public_api.is_none();
|
||||
Self {
|
||||
model,
|
||||
public_api,
|
||||
public_field: "ip".to_string(),
|
||||
public_template: String::new(),
|
||||
public_disabled,
|
||||
public_username: None,
|
||||
public_password: None,
|
||||
public_interval: 300.0,
|
||||
public_timer: Instant::now() - std::time::Duration::from_secs(3600),
|
||||
public_address: String::new(),
|
||||
public_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_public(&mut self) {
|
||||
if self.public_disabled {
|
||||
return;
|
||||
}
|
||||
if self.public_timer.elapsed().as_secs_f64() < self.public_interval {
|
||||
return;
|
||||
}
|
||||
self.public_timer = Instant::now();
|
||||
let Some(api) = &self.public_api else {
|
||||
return;
|
||||
};
|
||||
let mut request = ureq::get(api)
|
||||
.config()
|
||||
.timeout_global(Some(std::time::Duration::from_secs(2)))
|
||||
.build();
|
||||
if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) {
|
||||
request = request.header("Authorization", &format!("Basic {}:{}", username, password));
|
||||
}
|
||||
if let Ok(mut response) = request.call() {
|
||||
if let Ok(value) = response.body_mut().read_json::<Value>() {
|
||||
self.public_address =
|
||||
value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string();
|
||||
self.public_info = Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn public_info_human(&self) -> String {
|
||||
let Some(info) = &self.public_info else {
|
||||
return String::new();
|
||||
};
|
||||
if self.public_template.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut result = self.public_template.clone();
|
||||
for (key, value) in info.as_object().into_iter().flatten() {
|
||||
let text = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
other => glances_core::py_str(other),
|
||||
};
|
||||
result = result.replace(&format!("{{{key}}}"), &text);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for IpPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"ip"
|
||||
}
|
||||
|
||||
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 (address, mask) = crate::collectors::get_ip_address();
|
||||
if let Some(address) = &address {
|
||||
stats.insert("address".to_string(), json!(address));
|
||||
}
|
||||
if let Some(mask) = &mask {
|
||||
stats.insert("mask".to_string(), json!(mask));
|
||||
}
|
||||
stats.insert("mask_cidr".to_string(), json!(crate::collectors::ip_to_cidr(mask.as_deref())));
|
||||
self.refresh_public();
|
||||
if !self.public_address.is_empty() {
|
||||
let public = if ctx.args.hide_public_info {
|
||||
mask_public_ip(&self.public_address)
|
||||
} else {
|
||||
self.public_address.clone()
|
||||
};
|
||||
stats.insert("public_address".to_string(), json!(public));
|
||||
let human = self.public_info_human();
|
||||
if !human.is_empty() {
|
||||
stats.insert("public_info_human".to_string(), json!(human));
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(stats))
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
let s = &self.model.stats;
|
||||
let Some(address) = s.get("address").and_then(Value::as_str) else {
|
||||
return ret;
|
||||
};
|
||||
let mut text = address.to_string();
|
||||
if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) {
|
||||
if mask_cidr > 0 {
|
||||
text += &format!("/{mask_cidr}");
|
||||
}
|
||||
}
|
||||
ret.push(self.model.curse_add_line(text, glances_core::Decoration::Default, false, false, false));
|
||||
if let Some(gateway) = s.get("gateway").and_then(Value::as_str) {
|
||||
ret.push(self.model.curse_add_line(format!(" gw {gateway}"), glances_core::Decoration::Default, true, false, false));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_public_ip(address: &str) -> String {
|
||||
let parts: Vec<&str> = address.split('.').collect();
|
||||
if parts.len() == 4 {
|
||||
format!("{}.{}.{}.xxx", parts[0], parts[1], parts[2])
|
||||
} else {
|
||||
"xxx".to_string()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
//! Plugin implementations.
|
||||
//!
|
||||
//! The extended plugins (containers, GPU, ports, ...) are added in a later
|
||||
//! milestone.
|
||||
//! Remaining Python plugins (containers, GPU/NPU, connections, vms, cloud,
|
||||
//! smart) are not ported yet; see the README status section.
|
||||
|
||||
pub mod amps;
|
||||
pub mod core;
|
||||
pub mod cpu;
|
||||
pub mod diskio;
|
||||
pub mod folders;
|
||||
pub mod fs;
|
||||
pub mod ip;
|
||||
pub mod irq;
|
||||
pub mod load;
|
||||
pub mod mem;
|
||||
@@ -14,20 +17,26 @@ pub mod memswap;
|
||||
pub mod network;
|
||||
pub mod now;
|
||||
pub mod percpu;
|
||||
pub mod ports;
|
||||
pub mod processcount;
|
||||
pub mod processlist;
|
||||
pub mod programlist;
|
||||
pub mod psutilversion;
|
||||
pub mod quicklook;
|
||||
pub mod raid;
|
||||
pub mod sensors;
|
||||
pub mod system;
|
||||
pub mod uptime;
|
||||
pub mod version;
|
||||
pub mod wifi;
|
||||
|
||||
pub use amps::AmpsPlugin;
|
||||
pub use core::CorePlugin;
|
||||
pub use cpu::CpuPlugin;
|
||||
pub use diskio::DiskioPlugin;
|
||||
pub use folders::FoldersPlugin;
|
||||
pub use fs::FsPlugin;
|
||||
pub use ip::IpPlugin;
|
||||
pub use irq::IrqPlugin;
|
||||
pub use load::LoadPlugin;
|
||||
pub use mem::MemPlugin;
|
||||
@@ -35,12 +44,15 @@ pub use memswap::MemSwapPlugin;
|
||||
pub use network::NetworkPlugin;
|
||||
pub use now::NowPlugin;
|
||||
pub use percpu::PerCpuPlugin;
|
||||
pub use ports::PortsPlugin;
|
||||
pub use processcount::ProcessCountPlugin;
|
||||
pub use processlist::ProcessListPlugin;
|
||||
pub use programlist::ProgramListPlugin;
|
||||
pub use psutilversion::PsutilVersionPlugin;
|
||||
pub use quicklook::QuicklookPlugin;
|
||||
pub use raid::RaidPlugin;
|
||||
pub use sensors::SensorsPlugin;
|
||||
pub use system::SystemPlugin;
|
||||
pub use uptime::UptimePlugin;
|
||||
pub use version::VersionPlugin;
|
||||
pub use wifi::WifiPlugin;
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
//! Ports plugin: check the availability of configured hosts and ports.
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::{Args, Config};
|
||||
use serde_json::{Value, json};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct PortsPlugin {
|
||||
pub model: PluginModel,
|
||||
entries: Vec<PortEntry>,
|
||||
default_timeout: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PortEntry {
|
||||
indice: String,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
url: Option<String>,
|
||||
description: String,
|
||||
refresh: f64,
|
||||
timeout: f64,
|
||||
rtt_warning: Option<f64>,
|
||||
timer: Instant,
|
||||
status: Option<f64>,
|
||||
}
|
||||
|
||||
impl PortsPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let fields = fields! {
|
||||
"host" => Field::new("Measurement is be done on this host (or IP address)"),
|
||||
"port" => Field::new("Measurement is be done on this port (0 for ICMP)"),
|
||||
"url" => Field::new("Measurement is be done on this URL (credentials are redacted)"),
|
||||
"description" => Field::new("Human readable description for the host/port"),
|
||||
"refresh" => Field::new("Refresh time (in seconds) for this host/port"),
|
||||
"timeout" => Field::new("Timeout (in seconds) for the measurement"),
|
||||
"status" => Field::new("Measurement result (in seconds)").unit("second"),
|
||||
"rtt_warning" => Field::new("Warning threshold (in seconds) for the measurement").unit("second"),
|
||||
"indice" => Field::new("Unique indice for the host/port"),
|
||||
};
|
||||
let mut model = PluginModel::new(
|
||||
"ports",
|
||||
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("indice");
|
||||
|
||||
let default_timeout = config.get_float_value("ports", "timeout", 3.0);
|
||||
let refresh = config.get_float_value("ports", "refresh", 30.0);
|
||||
let mut entries = Vec::new();
|
||||
if config.has_section("ports") {
|
||||
// Default gateway entry.
|
||||
if config.get_bool_value("ports", "port_default_gateway", false) {
|
||||
if let Some(gateway) = crate::collectors::get_default_gateway() {
|
||||
entries.push(PortEntry {
|
||||
indice: "port_0".to_string(),
|
||||
host: gateway,
|
||||
port: None,
|
||||
url: None,
|
||||
description: "Default gateway".to_string(),
|
||||
refresh,
|
||||
timeout: default_timeout,
|
||||
rtt_warning: None,
|
||||
timer: Instant::now() - Duration::from_secs(3600),
|
||||
status: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
for index in 1..=255 {
|
||||
let key = format!("port_{index}_");
|
||||
let Some(host) = config.get_value("ports", &format!("{key}host"), None) else {
|
||||
continue;
|
||||
};
|
||||
if host.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let port = config.get_value("ports", &format!("{key}port"), None).and_then(|v| v.parse().ok());
|
||||
let description = config
|
||||
.get_value("ports", &format!("{key}description"), None)
|
||||
.unwrap_or_else(|| match port {
|
||||
Some(port) => format!("{host}:{port}"),
|
||||
None => host.clone(),
|
||||
});
|
||||
let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
|
||||
let rtt_warning = config
|
||||
.get_value("ports", &format!("{key}rtt_warning"), None)
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.map(|v| v / 1000.0);
|
||||
entries.push(PortEntry {
|
||||
indice: format!("port_{index}"),
|
||||
host,
|
||||
port,
|
||||
url: None,
|
||||
description,
|
||||
refresh,
|
||||
timeout,
|
||||
rtt_warning,
|
||||
timer: Instant::now() - Duration::from_secs(3600),
|
||||
status: None,
|
||||
});
|
||||
}
|
||||
for index in 1..=255 {
|
||||
let key = format!("web_{index}_");
|
||||
let Some(url) = config.get_value("ports", &format!("{key}url"), None) else {
|
||||
continue;
|
||||
};
|
||||
if url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let description = config.get_value("ports", &format!("{key}description"), None).unwrap_or_else(|| url.clone());
|
||||
let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout);
|
||||
let rtt_warning = config
|
||||
.get_value("ports", &format!("{key}rtt_warning"), None)
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.map(|v| v / 1000.0);
|
||||
entries.push(PortEntry {
|
||||
indice: format!("web_{index}"),
|
||||
host: url.clone(),
|
||||
port: None,
|
||||
url: Some(url),
|
||||
description,
|
||||
refresh,
|
||||
timeout,
|
||||
rtt_warning,
|
||||
timer: Instant::now() - Duration::from_secs(3600),
|
||||
status: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Self { model, entries, default_timeout }
|
||||
}
|
||||
|
||||
fn check(entry: &PortEntry) -> Option<f64> {
|
||||
let start = Instant::now();
|
||||
if let Some(url) = &entry.url {
|
||||
let timeout = Duration::from_secs_f64(entry.timeout.max(0.1));
|
||||
let request = ureq::get(url).config().timeout_global(Some(timeout)).build();
|
||||
match request.call() {
|
||||
Ok(_) => Some(start.elapsed().as_secs_f64()),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else if let Some(port) = entry.port {
|
||||
let timeout = Duration::from_secs_f64(entry.timeout.max(0.1));
|
||||
let address = format!("{}:{}", entry.host, port);
|
||||
let mut resolved = address.to_socket_addrs().ok()?;
|
||||
let socket = resolved.next()?;
|
||||
match TcpStream::connect_timeout(&socket, timeout) {
|
||||
Ok(_) => Some(start.elapsed().as_secs_f64()),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
// ICMP check with the ping command.
|
||||
let output = std::process::Command::new("ping")
|
||||
.args(["-c", "1", "-W", &format!("{}", entry.timeout.max(1.0) as i64), &entry.host])
|
||||
.output()
|
||||
.ok()?;
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if output.status.success() {
|
||||
for part in text.split_whitespace() {
|
||||
if let Some(value) = part.strip_prefix("time=") {
|
||||
return value.parse::<f64>().ok().map(|ms| ms / 1000.0);
|
||||
}
|
||||
}
|
||||
Some(start.elapsed().as_secs_f64())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for PortsPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"ports"
|
||||
}
|
||||
|
||||
fn model(&self) -> &PluginModel {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn model_mut(&mut self) -> &mut PluginModel {
|
||||
&mut self.model
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Option<&'static str> {
|
||||
Some("indice")
|
||||
}
|
||||
|
||||
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
|
||||
let mut stats = Vec::new();
|
||||
for entry in self.entries.iter_mut() {
|
||||
if entry.timer.elapsed().as_secs_f64() >= entry.refresh {
|
||||
entry.status = Self::check(entry);
|
||||
entry.timer = Instant::now();
|
||||
}
|
||||
let mut item = serde_json::Map::new();
|
||||
item.insert("host".to_string(), json!(entry.host));
|
||||
item.insert("port".to_string(), json!(entry.port));
|
||||
if let Some(url) = &entry.url {
|
||||
item.insert("url".to_string(), json!(redact_url(url)));
|
||||
}
|
||||
item.insert("description".to_string(), json!(entry.description));
|
||||
item.insert("refresh".to_string(), json!(entry.refresh));
|
||||
item.insert("timeout".to_string(), json!(entry.timeout));
|
||||
item.insert("status".to_string(), json!(entry.status));
|
||||
item.insert("rtt_warning".to_string(), json!(entry.rtt_warning));
|
||||
item.insert("indice".to_string(), json!(entry.indice));
|
||||
item.insert("key".to_string(), json!("indice"));
|
||||
stats.push(Value::Object(item));
|
||||
}
|
||||
let _ = self.default_timeout;
|
||||
Ok(Value::Array(stats))
|
||||
}
|
||||
|
||||
fn update_views(&mut self) {
|
||||
let key = self.get_key();
|
||||
self.model.update_views(key);
|
||||
let items: Vec<(String, Option<f64>, Option<f64>)> = match &self.model.stats {
|
||||
Value::Array(list) => list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let indice = item.get("indice")?.as_str()?.to_string();
|
||||
let status = item.get("status").and_then(Value::as_f64);
|
||||
let warning = item.get("rtt_warning").and_then(Value::as_f64);
|
||||
Some((indice, status, warning))
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (indice, status, warning) in items {
|
||||
let decoration = match status {
|
||||
None => glances_core::Decoration::Critical,
|
||||
Some(status) if warning.is_some_and(|w| status > w) => glances_core::Decoration::Warning,
|
||||
Some(_) => glances_core::Decoration::Ok,
|
||||
};
|
||||
self.model.set_view_decoration(Some(&indice), "status", decoration);
|
||||
}
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
|
||||
return ret;
|
||||
}
|
||||
let name_max_width = ctx.max_width.saturating_sub(12);
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", "PORTS", width = name_max_width),
|
||||
glances_core::Decoration::Title,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{:>9}", "RTT"), glances_core::Decoration::Default, false, false, false));
|
||||
let items = self.model.sorted_stats();
|
||||
let Some(items) = items.as_array() else {
|
||||
return ret;
|
||||
};
|
||||
for i in items {
|
||||
let indice = i.get("indice").and_then(Value::as_str).unwrap_or("");
|
||||
let description = i.get("description").and_then(Value::as_str).unwrap_or("");
|
||||
let status = i.get("status").and_then(Value::as_f64);
|
||||
ret.push(self.model.curse_new_line());
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", description.chars().take(name_max_width).collect::<String>(), width = name_max_width),
|
||||
glances_core::Decoration::Default,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
let msg = match status {
|
||||
Some(status) => format!("{:>8.3}s", status),
|
||||
None => format!("{:>9}", "timeout"),
|
||||
};
|
||||
ret.push(self.model.curse_add_line(
|
||||
msg,
|
||||
self.model.view_decoration(Some(indice), "status"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the credentials embedded in a URL.
|
||||
fn redact_url(url: &str) -> String {
|
||||
if let Some(scheme_end) = url.find("://") {
|
||||
let rest = &url[scheme_end + 3..];
|
||||
if let Some(at) = rest.find('@') {
|
||||
return format!("{}://********@{}", &url[..scheme_end], &rest[at + 1..]);
|
||||
}
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! RAID plugin: parse `/proc/mdstat`.
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::{Args, Config};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub struct RaidPlugin {
|
||||
pub model: PluginModel,
|
||||
}
|
||||
|
||||
impl RaidPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let model = PluginModel::new(
|
||||
"raid",
|
||||
fields! {},
|
||||
Vec::new(),
|
||||
json!({}),
|
||||
config,
|
||||
args.time.unwrap_or(2.0),
|
||||
args.disable_history,
|
||||
args.disable_config_exec,
|
||||
);
|
||||
Self { model }
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for RaidPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"raid"
|
||||
}
|
||||
|
||||
fn model(&self) -> &PluginModel {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn model_mut(&mut self) -> &mut PluginModel {
|
||||
&mut self.model
|
||||
}
|
||||
|
||||
fn available(&mut self) -> bool {
|
||||
std::path::Path::new("/proc/mdstat").exists()
|
||||
}
|
||||
|
||||
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
|
||||
let mut stats = serde_json::Map::new();
|
||||
let Ok(content) = std::fs::read_to_string("/proc/mdstat") else {
|
||||
return Ok(Value::Object(stats));
|
||||
};
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let mut index = 0;
|
||||
while index < lines.len() {
|
||||
let line = lines[index];
|
||||
if !line.starts_with("md") || !line.contains(':') {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||
if fields.len() < 3 {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let name = fields[0].to_string();
|
||||
let status = fields[2].to_string();
|
||||
let raid_type = fields
|
||||
.iter()
|
||||
.find(|f| f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath"))
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
// Devices (name[index]).
|
||||
let mut components = serde_json::Map::new();
|
||||
for field in fields.iter().skip(3) {
|
||||
if let Some(device) = field.split('[').next() {
|
||||
if !device.is_empty() {
|
||||
components.insert(device.to_string(), json!("active sync"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// The following line contains blocks and [used/available] [UU].
|
||||
let mut used = components.len();
|
||||
let mut available = components.len();
|
||||
if index + 1 < lines.len() {
|
||||
let detail = lines[index + 1];
|
||||
if let Some(start) = detail.rfind('[') {
|
||||
if let Some(end) = detail[start..].find(']') {
|
||||
let counts = &detail[start + 1..start + end];
|
||||
if let Some((u, a)) = counts.split_once('/') {
|
||||
used = u.parse().unwrap_or(used as i64) as usize;
|
||||
available = a.parse().unwrap_or(available as i64) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(start) = detail.rfind('[') {
|
||||
if let Some(end) = detail[start..].find(']') {
|
||||
let flags = &detail[start + 1..start + end];
|
||||
if flags.len() == components.len() {
|
||||
let names: Vec<String> = components.keys().cloned().collect();
|
||||
for (position, flag) in flags.chars().enumerate() {
|
||||
if let Some(device) = names.get(position) {
|
||||
let status = match flag {
|
||||
'U' => "active sync",
|
||||
'_' => "faulty",
|
||||
'S' => "spare",
|
||||
_ => "unknown",
|
||||
};
|
||||
components.insert(device.clone(), json!(status));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.insert(
|
||||
name.clone(),
|
||||
json!({
|
||||
"name": name,
|
||||
"status": status,
|
||||
"type": raid_type,
|
||||
"used": used,
|
||||
"available": available,
|
||||
"components": components,
|
||||
}),
|
||||
);
|
||||
index += 2;
|
||||
}
|
||||
Ok(Value::Object(stats))
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
let Some(arrays) = self.model.stats.as_object() else {
|
||||
return ret;
|
||||
};
|
||||
if arrays.is_empty() || ctx.max_width == 0 {
|
||||
return ret;
|
||||
}
|
||||
let name_max_width = ctx.max_width.saturating_sub(12);
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", "RAID disks", width = name_max_width),
|
||||
glances_core::Decoration::Title,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{:>7}", "Used"), glances_core::Decoration::Default, false, false, false));
|
||||
ret.push(self.model.curse_add_line(format!("{:>7}", "Avail"), glances_core::Decoration::Default, false, false, false));
|
||||
let mut names: Vec<&String> = arrays.keys().collect();
|
||||
names.sort();
|
||||
for name in names {
|
||||
let Some(array) = arrays.get(name).and_then(Value::as_object) else {
|
||||
continue;
|
||||
};
|
||||
let status = array.get("status").and_then(Value::as_str).unwrap_or("");
|
||||
let used = array.get("used").and_then(Value::as_i64).unwrap_or(0);
|
||||
let available = array.get("available").and_then(Value::as_i64).unwrap_or(0);
|
||||
let array_type = array.get("type").and_then(Value::as_str).unwrap_or("unknown");
|
||||
let decoration = raid_alert(status, used, available, array_type);
|
||||
ret.push(self.model.curse_new_line());
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", name.chars().take(name_max_width).collect::<String>(), width = name_max_width),
|
||||
decoration,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{used:>7}"), glances_core::Decoration::Default, false, false, false));
|
||||
ret.push(self.model.curse_add_line(format!("{available:>7}"), glances_core::Decoration::Default, false, false, false));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn raid_alert(status: &str, used: i64, available: i64, array_type: &str) -> glances_core::Decoration {
|
||||
if status != "active" || used < available {
|
||||
return glances_core::Decoration::Critical;
|
||||
}
|
||||
if array_type == "raid0" {
|
||||
return glances_core::Decoration::Warning;
|
||||
}
|
||||
glances_core::Decoration::Ok
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Wi-Fi plugin (reads `/proc/net/wireless`).
|
||||
|
||||
use crate::fields;
|
||||
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
|
||||
use glances_core::{Args, Config, Decoration};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub struct WifiPlugin {
|
||||
pub model: PluginModel,
|
||||
}
|
||||
|
||||
impl WifiPlugin {
|
||||
pub fn new(config: &Config, args: &Args) -> Self {
|
||||
let fields = fields! {
|
||||
"ssid" => Field::new("Wi-Fi network name."),
|
||||
"quality_link" => Field::new("Signal quality level.").unit("dBm"),
|
||||
"quality_level" => Field::new("Signal strong level.").unit("dBm").alert(),
|
||||
};
|
||||
let mut model = PluginModel::new(
|
||||
"wifi",
|
||||
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("ssid");
|
||||
Self { model }
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for WifiPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"wifi"
|
||||
}
|
||||
|
||||
fn model(&self) -> &PluginModel {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn model_mut(&mut self) -> &mut PluginModel {
|
||||
&mut self.model
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Option<&'static str> {
|
||||
Some("ssid")
|
||||
}
|
||||
|
||||
fn available(&mut self) -> bool {
|
||||
std::path::Path::new("/proc/net/wireless").exists()
|
||||
}
|
||||
|
||||
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
|
||||
let mut stats = Vec::new();
|
||||
let Ok(content) = std::fs::read_to_string("/proc/net/wireless") else {
|
||||
return Ok(Value::Array(stats));
|
||||
};
|
||||
for line in content.lines().skip(2) {
|
||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||
if fields.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
let ssid = fields[0].trim_end_matches(':').to_string();
|
||||
let quality_link = fields[2].trim_end_matches('.').parse::<f64>().unwrap_or(0.0);
|
||||
let quality_level = fields[3].trim_end_matches('.').parse::<f64>().unwrap_or(0.0);
|
||||
stats.push(json!({
|
||||
"key": "ssid",
|
||||
"ssid": ssid,
|
||||
"quality_link": quality_link,
|
||||
"quality_level": quality_level,
|
||||
}));
|
||||
}
|
||||
Ok(Value::Array(stats))
|
||||
}
|
||||
|
||||
fn update_views(&mut self) {
|
||||
let key = self.get_key();
|
||||
self.model.update_views(key);
|
||||
let items: Vec<(String, f64)> = match &self.model.stats {
|
||||
Value::Array(list) => list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let ssid = item.get("ssid")?.as_str()?.to_string();
|
||||
let level = item.get("quality_level")?.as_f64()?;
|
||||
Some((ssid, level))
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (ssid, level) in items {
|
||||
// Signal quality: lower is better, so the comparison is inverted.
|
||||
let critical = self.model.get_limit("critical", "wifi");
|
||||
let warning = self.model.get_limit("warning", "wifi");
|
||||
let careful = self.model.get_limit("careful", "wifi");
|
||||
let decoration = if critical.is_some_and(|c| level < c) {
|
||||
Decoration::Critical
|
||||
} else if warning.is_some_and(|c| level < c) {
|
||||
Decoration::Warning
|
||||
} else if careful.is_some_and(|c| level < c) {
|
||||
Decoration::Careful
|
||||
} else if critical.is_none() && warning.is_none() && careful.is_none() {
|
||||
Decoration::Default
|
||||
} else {
|
||||
Decoration::Ok
|
||||
};
|
||||
self.model.set_view_decoration(Some(&ssid), "quality_level", decoration);
|
||||
}
|
||||
}
|
||||
|
||||
fn display_curse(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
||||
let mut ret = Vec::new();
|
||||
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
|
||||
return ret;
|
||||
}
|
||||
let if_name_max_width = ctx.max_width.saturating_sub(5);
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", "WIFI", width = if_name_max_width),
|
||||
glances_core::Decoration::Title,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(format!("{:>7}", "dBm"), glances_core::Decoration::Default, false, false, false));
|
||||
let Some(items) = self.model.stats.as_array() else {
|
||||
return ret;
|
||||
};
|
||||
for i in items {
|
||||
let ssid = i.get("ssid").and_then(Value::as_str).unwrap_or("");
|
||||
let level = i.get("quality_level").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
ret.push(self.model.curse_new_line());
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:<width$}", ssid.chars().take(if_name_max_width).collect::<String>(), width = if_name_max_width),
|
||||
glances_core::Decoration::Default,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
ret.push(self.model.curse_add_line(
|
||||
format!("{:>7}", level),
|
||||
self.model.view_decoration(Some(ssid), "quality_level"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user