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