treewide: set up rustfmt and taplo with custom rules

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I794f9152bb02e3dd91c9738369b94fc66a6a6964
This commit is contained in:
raf 2026-02-19 01:42:28 +03:00
commit ffae695240
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
27 changed files with 1851 additions and 1618 deletions

28
.rustfmt.toml Normal file
View file

@ -0,0 +1,28 @@
# float_literal_trailing_zero = "Always" # TODO: Warning for some reason?
condense_wildcard_suffixes = true
doc_comment_code_block_width = 80
edition = "2024" # Keep in sync with Cargo.toml.
enum_discrim_align_threshold = 60
force_explicit_abi = false
force_multiline_blocks = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Upper"
imports_granularity = "Crate"
imports_layout = "Vertical"
inline_attribute_width = 60
match_block_trailing_comma = true
max_width = 80
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 60
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true

14
.taplo.toml Normal file
View file

@ -0,0 +1,14 @@
[formatting]
align_entries = true
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false

View file

@ -1,5 +1,4 @@
[workspace] [workspace]
resolver = "3"
members = [ members = [
"crates/*", "crates/*",
"scanners/scanner-system", "scanners/scanner-system",
@ -7,34 +6,35 @@ members = [
"scanners/scanner-power", "scanners/scanner-power",
"scanners/scanner-proc", "scanners/scanner-proc",
] ]
resolver = "3"
[workspace.package] [workspace.package]
version = "0.1.0" authors = [ "NotAShelf <raf@notashelf.dev>" ]
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
authors = ["NotAShelf <raf@notashelf.dev>"] version = "0.1.0"
[workspace.dependencies] [workspace.dependencies]
pscand-core = { path = "./crates/pscand-core" } pscand-core = { path = "./crates/pscand-core" }
pscand-macros = { path = "./crates/pscand-macros" } pscand-macros = { path = "./crates/pscand-macros" }
tokio = { version = "1.49.0", features = ["full"] } chrono = { features = [ "serde" ], version = "0.4.43" }
serde = { version = "1.0.228", features = ["derive"] } clap = { features = [ "derive" ], version = "4.5.59" }
serde_json = "1.0.149" dirs = "6.0.0"
toml = "1.0.0" env_logger = "0.11.9"
libloading = "0.9.0" libloading = "0.9.0"
chrono = { version = "0.4.43", features = ["serde"] } log = "0.4.29"
sysinfo = "0.38.2"
log = "0.4.29"
env_logger = "0.11.9"
thiserror = "2.0.18"
parking_lot = "0.12.5" parking_lot = "0.12.5"
ringbuf = "0.4.8" ringbuf = "0.4.8"
dirs = "6.0.0" serde = { features = [ "derive" ], version = "1.0.228" }
clap = { version = "4.5.59", features = ["derive"] } serde_json = "1.0.149"
sha2 = "0.10.9" sha2 = "0.10.9"
sysinfo = "0.38.2"
thiserror = "2.0.18"
tokio = { features = [ "full" ], version = "1.49.0" }
toml = "1.0.0"
[profile.release] [profile.release]
lto = true
opt-level = "z"
codegen-units = 1 codegen-units = 1
lto = true
opt-level = "z"

View file

@ -4,10 +4,10 @@
# Directories to load scanner plugins from # Directories to load scanner plugins from
# Set via PSCAND_SCANNER_DIRS environment variable or configure here # Set via PSCAND_SCANNER_DIRS environment variable or configure here
scanner_dirs = [ scanner_dirs = [
# Examples (uncomment and adjust for your system): # Examples (uncomment and adjust for your system):
# "/usr/lib/pscand/scanners", # "/usr/lib/pscand/scanners",
# "/var/lib/pscand/scanners", # "/var/lib/pscand/scanners",
"~/.local/share/pscand/scanners", "~/.local/share/pscand/scanners",
] ]
# Where to store log files # Where to store log files
@ -27,19 +27,19 @@ retention_days = 7
# Per-scanner configuration # Per-scanner configuration
[scanners.system] [scanners.system]
enabled = true enabled = true
interval_secs = 5 # Override default 1-second interval interval_secs = 5 # Override default 1-second interval
[scanners.sensor] [scanners.sensor]
enabled = true enabled = true
interval_secs = 10 # Sensors don't change as fast interval_secs = 10 # Sensors don't change as fast
[scanners.power] [scanners.power]
enabled = true enabled = true
interval_secs = 30 # Battery status changes slowly interval_secs = 30 # Battery status changes slowly
[scanners.proc] [scanners.proc]
enabled = true enabled = true
interval_secs = 5 interval_secs = 5
# Example: Custom scanner with extra parameters # Example: Custom scanner with extra parameters

View file

@ -1,27 +1,25 @@
log_dir = "/var/log/pscand" file_enabled = true
retention_days = 7 journal_enabled = true
log_dir = "/var/log/pscand"
retention_days = 7
ring_buffer_size = 60 ring_buffer_size = 60
journal_enabled = true
file_enabled = true
[scanner_dirs] [scanner_dirs]
# Directories to load scanner plugins from # Directories to load scanner plugins from
dirs = [ dirs = [ "~/.local/share/pscand/scanners" ]
"~/.local/share/pscand/scanners",
]
[scanners.system] [scanners.system]
enabled = true enabled = true
interval_secs = 1 interval_secs = 1
[scanners.sensor] [scanners.sensor]
enabled = true enabled = true
interval_secs = 2 interval_secs = 2
[scanners.power] [scanners.power]
enabled = true enabled = true
interval_secs = 2 interval_secs = 2
[scanners.proc] [scanners.proc]
enabled = true enabled = true
interval_secs = 5 interval_secs = 5

View file

@ -1,5 +1,5 @@
[package] [package]
name = "pscand-cli" name = "pscand-cli"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -10,20 +10,20 @@ name = "pscand"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
pscand-core.workspace = true chrono.workspace = true
clap.workspace = true
dirs.workspace = true
env_logger.workspace = true
libloading.workspace = true
log.workspace = true
parking_lot.workspace = true
pscand-core.workspace = true
pscand-macros.workspace = true pscand-macros.workspace = true
tokio.workspace = true ringbuf.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
toml.workspace = true sha2.workspace = true
libloading.workspace = true sysinfo.workspace = true
chrono.workspace = true thiserror.workspace = true
log.workspace = true tokio.workspace = true
env_logger.workspace = true toml.workspace = true
thiserror.workspace = true
parking_lot.workspace = true
ringbuf.workspace = true
dirs.workspace = true
sysinfo.workspace = true
clap.workspace = true
sha2.workspace = true

View file

@ -1,19 +1,45 @@
#![allow(improper_ctypes_definitions)] #![allow(improper_ctypes_definitions)]
use std::{
fs,
io::Read,
path::{
Path,
PathBuf,
},
sync::{
atomic::{
AtomicBool,
Ordering,
},
Arc,
},
time::{
Duration,
Instant,
SystemTime,
UNIX_EPOCH,
},
};
use clap::Parser; use clap::Parser;
use libloading::Library; use libloading::Library;
use pscand_core::Config as CoreConfig; use pscand_core::{
use pscand_core::logging::{LogLevel, RingBufferLogger}; logging::{
use pscand_core::scanner::Scanner; LogLevel,
use sha2::{Digest, Sha256}; RingBufferLogger,
use std::fs; },
use std::io::Read; scanner::Scanner,
use std::path::{Path, PathBuf}; Config as CoreConfig,
use std::sync::Arc; };
use std::sync::atomic::{AtomicBool, Ordering}; use sha2::{
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; Digest,
use tokio::sync::RwLock; Sha256,
use tokio::time::interval; };
use tokio::{
sync::RwLock,
time::interval,
};
type ScannerCreator = pscand_core::ScannerCreatorFfi; type ScannerCreator = pscand_core::ScannerCreatorFfi;
@ -79,22 +105,22 @@ fn verify_library(path: &Path) -> Result<(), String> {
} }
struct LoadedScanner { struct LoadedScanner {
name: String, name: String,
scanner: Arc<RwLock<Box<dyn Scanner>>>, scanner: Arc<RwLock<Box<dyn Scanner>>>,
interval: Duration, interval: Duration,
#[allow(dead_code)] #[allow(dead_code)]
library: Library, library: Library,
} }
#[derive(Clone)] #[derive(Clone)]
struct DaemonState { struct DaemonState {
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
shutdown_requested: Arc<AtomicBool>, shutdown_requested: Arc<AtomicBool>,
start_time: Arc<RwLock<SystemTime>>, start_time: Arc<RwLock<SystemTime>>,
last_collection: Arc<RwLock<SystemTime>>, last_collection: Arc<RwLock<SystemTime>>,
collection_count: Arc<RwLock<u64>>, collection_count: Arc<RwLock<u64>>,
error_count: Arc<RwLock<u64>>, error_count: Arc<RwLock<u64>>,
heartbeat_path: PathBuf, heartbeat_path: PathBuf,
} }
impl DaemonState { impl DaemonState {
@ -334,7 +360,8 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
" 1. Scanner plugins are installed in one of the configured directories" " 1. Scanner plugins are installed in one of the configured directories"
); );
log::error!( log::error!(
" 2. Scanner directories are correctly set in config file or PSCAND_SCANNER_DIRS env var" " 2. Scanner directories are correctly set in config file or \
PSCAND_SCANNER_DIRS env var"
); );
log::error!(" 3. Scanners are not disabled in the configuration"); log::error!(" 3. Scanners are not disabled in the configuration");
logger.log( logger.log(
@ -664,7 +691,8 @@ async fn list_scanners() -> Result<(), Box<dyn std::error::Error>> {
"\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)" "\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)"
); );
println!( println!(
" Default fallback: ~/.local/share/pscand/scanners/ or ~/.config/pscand/scanners/" " Default fallback: ~/.local/share/pscand/scanners/ or \
~/.config/pscand/scanners/"
); );
Ok(()) Ok(())
} }

View file

@ -1,22 +1,22 @@
[package] [package]
name = "pscand-core" name = "pscand-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
tokio.workspace = true chrono.workspace = true
serde.workspace = true dirs.workspace = true
serde_json.workspace = true log.workspace = true
toml.workspace = true
chrono.workspace = true
log.workspace = true
thiserror.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
dirs.workspace = true ringbuf.workspace = true
sysinfo.workspace = true serde.workspace = true
ringbuf.workspace = true serde_json.workspace = true
sysinfo.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
[lib] [lib]
name = "pscand_core" name = "pscand_core"

View file

@ -1,126 +1,135 @@
use serde::{Deserialize, Serialize}; use std::{
use std::collections::HashMap; collections::HashMap,
use std::path::{Path, PathBuf}; path::{
Path,
PathBuf,
},
};
use serde::{
Deserialize,
Serialize,
};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ConfigError { pub enum ConfigError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Parse error: {0}")] #[error("Parse error: {0}")]
Parse(#[from] toml::de::Error), Parse(#[from] toml::de::Error),
#[error("Scanner {0} not configured")] #[error("Scanner {0} not configured")]
ScannerNotConfigured(String), ScannerNotConfigured(String),
#[error("Invalid scanner name: {0}")] #[error("Invalid scanner name: {0}")]
InvalidScannerName(String), InvalidScannerName(String),
} }
pub type ConfigResult<T> = std::result::Result<T, ConfigError>; pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScannerConfig { pub struct ScannerConfig {
pub enabled: bool, pub enabled: bool,
pub interval_secs: Option<u64>, pub interval_secs: Option<u64>,
pub extra: HashMap<String, toml::Value>, pub extra: HashMap<String, toml::Value>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config { pub struct Config {
#[serde(default = "default_scanner_dirs")] #[serde(default = "default_scanner_dirs")]
pub scanner_dirs: Vec<PathBuf>, pub scanner_dirs: Vec<PathBuf>,
#[serde(default = "default_log_dir")] #[serde(default = "default_log_dir")]
pub log_dir: PathBuf, pub log_dir: PathBuf,
#[serde(default = "default_retention_days")] #[serde(default = "default_retention_days")]
pub retention_days: u32, pub retention_days: u32,
#[serde(default = "default_ring_buffer_size")] #[serde(default = "default_ring_buffer_size")]
pub ring_buffer_size: usize, pub ring_buffer_size: usize,
#[serde(default)] #[serde(default)]
pub scanners: HashMap<String, ScannerConfig>, pub scanners: HashMap<String, ScannerConfig>,
#[serde(default)] #[serde(default)]
pub journal_enabled: bool, pub journal_enabled: bool,
#[serde(default)] #[serde(default)]
pub file_enabled: bool, pub file_enabled: bool,
} }
fn default_scanner_dirs() -> Vec<PathBuf> { fn default_scanner_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new(); let mut dirs = Vec::new();
if let Ok(env_var) = std::env::var("PSCAND_SCANNER_DIRS") { if let Ok(env_var) = std::env::var("PSCAND_SCANNER_DIRS") {
for path in env_var.split(':') { for path in env_var.split(':') {
let path = PathBuf::from(path); let path = PathBuf::from(path);
if !path.as_os_str().is_empty() { if !path.as_os_str().is_empty() {
dirs.push(path); dirs.push(path);
} }
}
if !dirs.is_empty() {
return dirs;
}
} }
if !dirs.is_empty() {
if let Some(lib) = std::env::var_os("LIB_PSCAND") { return dirs;
dirs.push(PathBuf::from(lib));
} }
}
if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") { if let Some(lib) = std::env::var_os("LIB_PSCAND") {
dirs.push(PathBuf::from(lib_dir)); dirs.push(PathBuf::from(lib));
} }
if let Some(local) = dirs::data_local_dir() { if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") {
dirs.push(local.join("pscand/scanners")); dirs.push(PathBuf::from(lib_dir));
} }
if let Some(config) = dirs::config_dir() { if let Some(local) = dirs::data_local_dir() {
dirs.push(config.join("pscand/scanners")); dirs.push(local.join("pscand/scanners"));
} }
if dirs.is_empty() { if let Some(config) = dirs::config_dir() {
dirs.push(PathBuf::from(".pscand/scanners")); dirs.push(config.join("pscand/scanners"));
} }
dirs if dirs.is_empty() {
dirs.push(PathBuf::from(".pscand/scanners"));
}
dirs
} }
fn default_log_dir() -> PathBuf { fn default_log_dir() -> PathBuf {
dirs::data_local_dir() dirs::data_local_dir()
.map(|p| p.join("pscand/logs")) .map(|p| p.join("pscand/logs"))
.unwrap_or_else(|| PathBuf::from(".pscand/logs")) .unwrap_or_else(|| PathBuf::from(".pscand/logs"))
} }
fn default_retention_days() -> u32 { fn default_retention_days() -> u32 {
7 7
} }
fn default_ring_buffer_size() -> usize { fn default_ring_buffer_size() -> usize {
60 60
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
scanner_dirs: default_scanner_dirs(), scanner_dirs: default_scanner_dirs(),
log_dir: default_log_dir(), log_dir: default_log_dir(),
retention_days: default_retention_days(), retention_days: default_retention_days(),
ring_buffer_size: default_ring_buffer_size(), ring_buffer_size: default_ring_buffer_size(),
scanners: HashMap::new(), scanners: HashMap::new(),
journal_enabled: true, journal_enabled: true,
file_enabled: true, file_enabled: true,
}
} }
}
} }
impl Config { impl Config {
pub fn load(path: &Path) -> ConfigResult<Self> { pub fn load(path: &Path) -> ConfigResult<Self> {
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
let mut config: Config = toml::from_str(&content)?; let mut config: Config = toml::from_str(&content)?;
config.scanner_dirs.retain(|p| p.exists()); config.scanner_dirs.retain(|p| p.exists());
Ok(config) Ok(config)
} }
pub fn scanner_config(&self, name: &str) -> Option<&ScannerConfig> { pub fn scanner_config(&self, name: &str) -> Option<&ScannerConfig> {
self.scanners.get(name) self.scanners.get(name)
} }
pub fn is_scanner_enabled(&self, name: &str) -> bool { pub fn is_scanner_enabled(&self, name: &str) -> bool {
self.scanners.get(name).map(|c| c.enabled).unwrap_or(true) self.scanners.get(name).map(|c| c.enabled).unwrap_or(true)
} }
} }

View file

@ -1,157 +1,160 @@
use std::collections::HashMap; use std::{
use std::fs; collections::HashMap,
use std::path::PathBuf; fs,
path::PathBuf,
};
pub struct PowerHelper; pub struct PowerHelper;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BatteryInfo { pub struct BatteryInfo {
pub name: String, pub name: String,
pub status: String, pub status: String,
pub capacity: u32, pub capacity: u32,
pub charge_percent: i32, pub charge_percent: i32,
pub voltage: f64, pub voltage: f64,
pub current_now: i64, pub current_now: i64,
pub power_now: i64, pub power_now: i64,
pub present: bool, pub present: bool,
} }
impl PowerHelper { impl PowerHelper {
pub fn battery_info() -> std::io::Result<Option<BatteryInfo>> { pub fn battery_info() -> std::io::Result<Option<BatteryInfo>> {
let battery_path = PathBuf::from("/sys/class/power_supply"); let battery_path = PathBuf::from("/sys/class/power_supply");
for entry in fs::read_dir(&battery_path)? { for entry in fs::read_dir(&battery_path)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
let type_path = path.join("type"); let type_path = path.join("type");
if type_path.exists() { if type_path.exists() {
let battery_type = fs::read_to_string(&type_path)?.trim().to_string(); let battery_type = fs::read_to_string(&type_path)?.trim().to_string();
if battery_type == "Battery" { if battery_type == "Battery" {
let present_path = path.join("present"); let present_path = path.join("present");
let present = fs::read_to_string(&present_path) let present = fs::read_to_string(&present_path)
.map(|s| s.trim() == "1") .map(|s| s.trim() == "1")
.unwrap_or(false); .unwrap_or(false);
if !present { if !present {
continue; continue;
} }
let name = fs::read_to_string(path.join("name")) let name = fs::read_to_string(path.join("name"))
.unwrap_or_else(|_| "Unknown".to_string()) .unwrap_or_else(|_| "Unknown".to_string())
.trim() .trim()
.to_string(); .to_string();
let status = fs::read_to_string(path.join("status")) let status = fs::read_to_string(path.join("status"))
.unwrap_or_else(|_| "Unknown".to_string()) .unwrap_or_else(|_| "Unknown".to_string())
.trim() .trim()
.to_string(); .to_string();
let capacity = fs::read_to_string(path.join("capacity")) let capacity = fs::read_to_string(path.join("capacity"))
.ok() .ok()
.and_then(|s| s.trim().parse::<u32>().ok()) .and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0); .unwrap_or(0);
let charge_now = fs::read_to_string(path.join("charge_now")) let charge_now = fs::read_to_string(path.join("charge_now"))
.or_else(|_| fs::read_to_string(path.join("energy_now"))) .or_else(|_| fs::read_to_string(path.join("energy_now")))
.ok() .ok()
.and_then(|s| s.trim().parse::<i64>().ok()) .and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(0); .unwrap_or(0);
let charge_full = fs::read_to_string(path.join("charge_full")) let charge_full = fs::read_to_string(path.join("charge_full"))
.or_else(|_| fs::read_to_string(path.join("energy_full"))) .or_else(|_| fs::read_to_string(path.join("energy_full")))
.ok() .ok()
.and_then(|s| s.trim().parse::<i64>().ok()) .and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(1); .unwrap_or(1);
let charge_percent = if charge_full > 0 { let charge_percent = if charge_full > 0 {
((charge_now as f64 / charge_full as f64) * 100.0) as i32 ((charge_now as f64 / charge_full as f64) * 100.0) as i32
} else { } else {
0 0
}; };
let voltage = fs::read_to_string(path.join("voltage_now")) let voltage = fs::read_to_string(path.join("voltage_now"))
.ok() .ok()
.and_then(|s| s.trim().parse::<f64>().ok()) .and_then(|s| s.trim().parse::<f64>().ok())
.unwrap_or(0.0) .unwrap_or(0.0)
/ 1_000_000.0; / 1_000_000.0;
let current_now = fs::read_to_string(path.join("current_now")) let current_now = fs::read_to_string(path.join("current_now"))
.ok() .ok()
.and_then(|s| s.trim().parse::<i64>().ok()) .and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(0); .unwrap_or(0);
let power_now = fs::read_to_string(path.join("power_now")) let power_now = fs::read_to_string(path.join("power_now"))
.ok() .ok()
.and_then(|s| s.trim().parse::<i64>().ok()) .and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(0); .unwrap_or(0);
return Ok(Some(BatteryInfo { return Ok(Some(BatteryInfo {
name, name,
status, status,
capacity, capacity,
charge_percent, charge_percent,
voltage, voltage,
current_now, current_now,
power_now, power_now,
present, present,
})); }));
}
}
} }
}
Ok(None)
} }
pub fn power_supplies() -> std::io::Result<HashMap<String, HashMap<String, String>>> { Ok(None)
let mut supplies = HashMap::new(); }
let power_supply_path = PathBuf::from("/sys/class/power_supply");
if !power_supply_path.exists() { pub fn power_supplies(
return Ok(supplies); ) -> std::io::Result<HashMap<String, HashMap<String, String>>> {
let mut supplies = HashMap::new();
let power_supply_path = PathBuf::from("/sys/class/power_supply");
if !power_supply_path.exists() {
return Ok(supplies);
}
for entry in fs::read_dir(&power_supply_path)? {
let entry = entry?;
let path = entry.path();
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let mut info = HashMap::new();
for attr in [
"type",
"status",
"capacity",
"voltage_now",
"power_now",
"online",
] {
let attr_path = path.join(attr);
if attr_path.exists() {
if let Ok(content) = fs::read_to_string(&attr_path) {
info.insert(attr.to_string(), content.trim().to_string());
}
} }
}
for entry in fs::read_dir(&power_supply_path)? { if !info.is_empty() {
let entry = entry?; supplies.insert(name, info);
let path = entry.path(); }
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let mut info = HashMap::new();
for attr in [
"type",
"status",
"capacity",
"voltage_now",
"power_now",
"online",
] {
let attr_path = path.join(attr);
if attr_path.exists() {
if let Ok(content) = fs::read_to_string(&attr_path) {
info.insert(attr.to_string(), content.trim().to_string());
}
}
}
if !info.is_empty() {
supplies.insert(name, info);
}
}
Ok(supplies)
} }
pub fn suspend_state() -> std::io::Result<String> { Ok(supplies)
let state_path = PathBuf::from("/sys/power/state"); }
fs::read_to_string(state_path).map(|s| s.trim().to_string())
}
pub fn mem_sleep_state() -> std::io::Result<String> { pub fn suspend_state() -> std::io::Result<String> {
let state_path = PathBuf::from("/sys/power/mem_sleep"); let state_path = PathBuf::from("/sys/power/state");
fs::read_to_string(state_path).map(|s| s.trim().to_string()) fs::read_to_string(state_path).map(|s| s.trim().to_string())
} }
pub fn mem_sleep_state() -> std::io::Result<String> {
let state_path = PathBuf::from("/sys/power/mem_sleep");
fs::read_to_string(state_path).map(|s| s.trim().to_string())
}
} }

View file

@ -1,136 +1,144 @@
use std::collections::HashMap; use std::{
use std::fs; collections::HashMap,
fs,
};
pub struct ProcessHelper; pub struct ProcessHelper;
#[derive(Debug)] #[derive(Debug)]
pub struct ProcessInfo { pub struct ProcessInfo {
pub pid: u32, pub pid: u32,
pub name: String, pub name: String,
pub state: String, pub state: String,
pub ppid: u32, pub ppid: u32,
pub memory_kb: u64, pub memory_kb: u64,
pub cpu_percent: f32, pub cpu_percent: f32,
} }
impl ProcessHelper { impl ProcessHelper {
pub fn list_processes() -> std::io::Result<Vec<ProcessInfo>> { pub fn list_processes() -> std::io::Result<Vec<ProcessInfo>> {
let mut processes = Vec::new(); let mut processes = Vec::new();
let proc_path = fs::read_dir("/proc")?; let proc_path = fs::read_dir("/proc")?;
for entry in proc_path.flatten() { for entry in proc_path.flatten() {
let path = entry.path(); let path = entry.path();
if !path.is_dir() { if !path.is_dir() {
continue; continue;
} }
let pid: u32 = match path.file_name() { let pid: u32 = match path.file_name() {
Some(name) => match name.to_str() { Some(name) => {
Some(s) => s.parse().ok(), match name.to_str() {
None => None, Some(s) => s.parse().ok(),
} None => None,
.ok_or(std::io::Error::new( }
std::io::ErrorKind::InvalidData, .ok_or(std::io::Error::new(
"invalid pid", std::io::ErrorKind::InvalidData,
))?, "invalid pid",
None => continue, ))?
}; },
None => continue,
};
if let Ok(info) = Self::process_info(pid) { if let Ok(info) = Self::process_info(pid) {
processes.push(info); processes.push(info);
} }
}
Ok(processes)
} }
pub fn process_info(pid: u32) -> std::io::Result<ProcessInfo> { Ok(processes)
let status_path = format!("/proc/{}/status", pid); }
let content = fs::read_to_string(status_path)?;
let mut name = String::new(); pub fn process_info(pid: u32) -> std::io::Result<ProcessInfo> {
let mut state = String::new(); let status_path = format!("/proc/{}/status", pid);
let mut ppid: u32 = 0; let content = fs::read_to_string(status_path)?;
let mut memory_kb: u64 = 0;
for line in content.lines() { let mut name = String::new();
if line.starts_with("Name:") { let mut state = String::new();
name = line let mut ppid: u32 = 0;
.split_whitespace() let mut memory_kb: u64 = 0;
.skip(1)
.collect::<Vec<_>>()
.join(" ");
} else if line.starts_with("State:") {
state = line.split_whitespace().nth(1).unwrap_or("").to_string();
} else if line.starts_with("PPid:") {
ppid = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
} else if line.starts_with("VmRSS:") {
memory_kb = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
}
Ok(ProcessInfo { for line in content.lines() {
pid, if line.starts_with("Name:") {
name, name = line
state, .split_whitespace()
ppid, .skip(1)
memory_kb, .collect::<Vec<_>>()
cpu_percent: 0.0, .join(" ");
}) } else if line.starts_with("State:") {
state = line.split_whitespace().nth(1).unwrap_or("").to_string();
} else if line.starts_with("PPid:") {
ppid = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
} else if line.starts_with("VmRSS:") {
memory_kb = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
} }
pub fn zombie_processes() -> std::io::Result<Vec<ProcessInfo>> { Ok(ProcessInfo {
Ok(Self::list_processes()? pid,
.into_iter() name,
.filter(|p| p.state.starts_with('Z')) state,
.collect()) ppid,
memory_kb,
cpu_percent: 0.0,
})
}
pub fn zombie_processes() -> std::io::Result<Vec<ProcessInfo>> {
Ok(
Self::list_processes()?
.into_iter()
.filter(|p| p.state.starts_with('Z'))
.collect(),
)
}
pub fn process_count() -> std::io::Result<HashMap<String, usize>> {
let mut counts = HashMap::new();
counts.insert("total".to_string(), 0);
counts.insert("running".to_string(), 0);
counts.insert("sleeping".to_string(), 0);
counts.insert("zombie".to_string(), 0);
for proc in Self::list_processes()? {
*counts.get_mut("total").unwrap() += 1;
let first_char = proc.state.chars().next().unwrap_or(' ');
match first_char {
'R' => *counts.get_mut("running").unwrap() += 1,
'S' | 'D' => *counts.get_mut("sleeping").unwrap() += 1,
'Z' => *counts.get_mut("zombie").unwrap() += 1,
_ => {},
}
} }
pub fn process_count() -> std::io::Result<HashMap<String, usize>> { Ok(counts)
let mut counts = HashMap::new(); }
counts.insert("total".to_string(), 0);
counts.insert("running".to_string(), 0);
counts.insert("sleeping".to_string(), 0);
counts.insert("zombie".to_string(), 0);
for proc in Self::list_processes()? { pub fn top_memory_processes(
*counts.get_mut("total").unwrap() += 1; count: usize,
) -> std::io::Result<Vec<ProcessInfo>> {
let mut processes = Self::list_processes()?;
processes.sort_by(|a, b| b.memory_kb.cmp(&a.memory_kb));
processes.truncate(count);
Ok(processes)
}
let first_char = proc.state.chars().next().unwrap_or(' '); pub fn top_cpu_processes(count: usize) -> std::io::Result<Vec<ProcessInfo>> {
match first_char { let mut processes = Self::list_processes()?;
'R' => *counts.get_mut("running").unwrap() += 1, processes.sort_by(|a, b| {
'S' | 'D' => *counts.get_mut("sleeping").unwrap() += 1, b.cpu_percent
'Z' => *counts.get_mut("zombie").unwrap() += 1, .partial_cmp(&a.cpu_percent)
_ => {} .unwrap_or(std::cmp::Ordering::Equal)
} });
} processes.truncate(count);
Ok(processes)
Ok(counts) }
}
pub fn top_memory_processes(count: usize) -> std::io::Result<Vec<ProcessInfo>> {
let mut processes = Self::list_processes()?;
processes.sort_by(|a, b| b.memory_kb.cmp(&a.memory_kb));
processes.truncate(count);
Ok(processes)
}
pub fn top_cpu_processes(count: usize) -> std::io::Result<Vec<ProcessInfo>> {
let mut processes = Self::list_processes()?;
processes.sort_by(|a, b| {
b.cpu_percent
.partial_cmp(&a.cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal)
});
processes.truncate(count);
Ok(processes)
}
} }

View file

@ -1,123 +1,129 @@
use std::collections::HashMap; use std::{
use std::fs; collections::HashMap,
fs,
};
pub struct ResourceHelper; pub struct ResourceHelper;
impl ResourceHelper { impl ResourceHelper {
pub fn cpu_usage() -> std::io::Result<HashMap<String, f64>> { pub fn cpu_usage() -> std::io::Result<HashMap<String, f64>> {
let content = fs::read_to_string("/proc/stat")?; let content = fs::read_to_string("/proc/stat")?;
let mut result = HashMap::new(); let mut result = HashMap::new();
for line in content.lines() { for line in content.lines() {
if line.starts_with("cpu") { if line.starts_with("cpu") {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 { if parts.len() >= 5 {
let cpu = parts[0]; let cpu = parts[0];
let values: Vec<u64> = parts[1..] let values: Vec<u64> = parts[1..]
.iter() .iter()
.take(7) .take(7)
.filter_map(|s| s.parse().ok()) .filter_map(|s| s.parse().ok())
.collect(); .collect();
if values.len() >= 4 { if values.len() >= 4 {
let user = values[0] as f64; let user = values[0] as f64;
let nice = values[1] as f64; let nice = values[1] as f64;
let system = values[2] as f64; let system = values[2] as f64;
let idle = values[3] as f64; let idle = values[3] as f64;
let iowait = values.get(4).copied().unwrap_or(0) as f64; let iowait = values.get(4).copied().unwrap_or(0) as f64;
let irq = values.get(5).copied().unwrap_or(0) as f64; let irq = values.get(5).copied().unwrap_or(0) as f64;
let softirq = values.get(6).copied().unwrap_or(0) as f64; let softirq = values.get(6).copied().unwrap_or(0) as f64;
let total = user + nice + system + idle + iowait + irq + softirq; let total = user + nice + system + idle + iowait + irq + softirq;
let active = user + nice + system + irq + softirq; let active = user + nice + system + irq + softirq;
if cpu == "cpu" { if cpu == "cpu" {
result.insert("total_user".to_string(), user); result.insert("total_user".to_string(), user);
result.insert("total_nice".to_string(), nice); result.insert("total_nice".to_string(), nice);
result.insert("total_system".to_string(), system); result.insert("total_system".to_string(), system);
result.insert("total_idle".to_string(), idle); result.insert("total_idle".to_string(), idle);
result.insert("total_iowait".to_string(), iowait); result.insert("total_iowait".to_string(), iowait);
result.insert( result.insert(
"total_usage_percent".to_string(), "total_usage_percent".to_string(),
(active / total) * 100.0, (active / total) * 100.0,
); );
} else { } else {
let core = cpu.replace("cpu", "core_"); let core = cpu.replace("cpu", "core_");
result.insert(format!("{}_user", core), user); result.insert(format!("{}_user", core), user);
result.insert( result.insert(
format!("{}_usage_percent", core), format!("{}_usage_percent", core),
(active / total) * 100.0, (active / total) * 100.0,
); );
}
}
}
} }
}
} }
Ok(result) }
} }
Ok(result)
}
pub fn memory_info() -> std::io::Result<HashMap<String, u64>> { pub fn memory_info() -> std::io::Result<HashMap<String, u64>> {
let content = fs::read_to_string("/proc/meminfo")?; let content = fs::read_to_string("/proc/meminfo")?;
let mut result = HashMap::new(); let mut result = HashMap::new();
for line in content.lines() { for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let key = parts[0].trim_end_matches(':'); let key = parts[0].trim_end_matches(':');
if let Ok(value) = parts[1].parse::<u64>() { if let Ok(value) = parts[1].parse::<u64>() {
result.insert(key.to_string(), value * 1024); result.insert(key.to_string(), value * 1024);
}
}
} }
Ok(result) }
} }
Ok(result)
}
pub fn disk_stats() -> std::io::Result<HashMap<String, HashMap<String, u64>>> { pub fn disk_stats() -> std::io::Result<HashMap<String, HashMap<String, u64>>>
let content = fs::read_to_string("/proc/diskstats")?; {
let mut result = HashMap::new(); let content = fs::read_to_string("/proc/diskstats")?;
let mut result = HashMap::new();
for line in content.lines() { for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 14 { if parts.len() >= 14 {
let device = parts[2].to_string(); let device = parts[2].to_string();
let mut stats = HashMap::new(); let mut stats = HashMap::new();
stats.insert("reads_completed".to_string(), parts[3].parse().unwrap_or(0)); stats
stats.insert("reads_merged".to_string(), parts[4].parse().unwrap_or(0)); .insert("reads_completed".to_string(), parts[3].parse().unwrap_or(0));
stats.insert("sectors_read".to_string(), parts[5].parse().unwrap_or(0)); stats.insert("reads_merged".to_string(), parts[4].parse().unwrap_or(0));
stats.insert("reads_ms".to_string(), parts[6].parse().unwrap_or(0)); stats.insert("sectors_read".to_string(), parts[5].parse().unwrap_or(0));
stats.insert( stats.insert("reads_ms".to_string(), parts[6].parse().unwrap_or(0));
"writes_completed".to_string(), stats.insert(
parts[7].parse().unwrap_or(0), "writes_completed".to_string(),
); parts[7].parse().unwrap_or(0),
stats.insert("writes_merged".to_string(), parts[8].parse().unwrap_or(0)); );
stats.insert("sectors_written".to_string(), parts[9].parse().unwrap_or(0)); stats
stats.insert("writes_ms".to_string(), parts[10].parse().unwrap_or(0)); .insert("writes_merged".to_string(), parts[8].parse().unwrap_or(0));
result.insert(device, stats); stats
} .insert("sectors_written".to_string(), parts[9].parse().unwrap_or(0));
} stats.insert("writes_ms".to_string(), parts[10].parse().unwrap_or(0));
Ok(result) result.insert(device, stats);
}
} }
Ok(result)
}
pub fn net_dev() -> std::io::Result<HashMap<String, HashMap<String, u64>>> { pub fn net_dev() -> std::io::Result<HashMap<String, HashMap<String, u64>>> {
let content = fs::read_to_string("/proc/net/dev")?; let content = fs::read_to_string("/proc/net/dev")?;
let mut result = HashMap::new(); let mut result = HashMap::new();
for line in content.lines().skip(2) { for line in content.lines().skip(2) {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 17 { if parts.len() >= 17 {
let iface = parts[0].trim_end_matches(':'); let iface = parts[0].trim_end_matches(':');
let mut stats = HashMap::new(); let mut stats = HashMap::new();
stats.insert("rx_bytes".to_string(), parts[1].parse().unwrap_or(0)); stats.insert("rx_bytes".to_string(), parts[1].parse().unwrap_or(0));
stats.insert("rx_packets".to_string(), parts[2].parse().unwrap_or(0)); stats.insert("rx_packets".to_string(), parts[2].parse().unwrap_or(0));
stats.insert("rx_errors".to_string(), parts[3].parse().unwrap_or(0)); stats.insert("rx_errors".to_string(), parts[3].parse().unwrap_or(0));
stats.insert("rx_dropped".to_string(), parts[4].parse().unwrap_or(0)); stats.insert("rx_dropped".to_string(), parts[4].parse().unwrap_or(0));
stats.insert("tx_bytes".to_string(), parts[9].parse().unwrap_or(0)); stats.insert("tx_bytes".to_string(), parts[9].parse().unwrap_or(0));
stats.insert("tx_packets".to_string(), parts[10].parse().unwrap_or(0)); stats.insert("tx_packets".to_string(), parts[10].parse().unwrap_or(0));
stats.insert("tx_errors".to_string(), parts[11].parse().unwrap_or(0)); stats.insert("tx_errors".to_string(), parts[11].parse().unwrap_or(0));
stats.insert("tx_dropped".to_string(), parts[12].parse().unwrap_or(0)); stats.insert("tx_dropped".to_string(), parts[12].parse().unwrap_or(0));
result.insert(iface.to_string(), stats); result.insert(iface.to_string(), stats);
} }
}
Ok(result)
} }
Ok(result)
}
} }

View file

@ -1,102 +1,117 @@
use std::collections::HashMap; use std::{
use std::fs; collections::HashMap,
use std::path::{Path, PathBuf}; fs,
path::{
Path,
PathBuf,
},
};
pub struct SensorHelper; pub struct SensorHelper;
impl SensorHelper { impl SensorHelper {
pub fn discover_hwmon() -> std::io::Result<Vec<PathBuf>> { pub fn discover_hwmon() -> std::io::Result<Vec<PathBuf>> {
let hwmon_path = PathBuf::from("/sys/class/hwmon"); let hwmon_path = PathBuf::from("/sys/class/hwmon");
let mut hwmons = Vec::new(); let mut hwmons = Vec::new();
if hwmon_path.exists() { if hwmon_path.exists() {
for entry in fs::read_dir(&hwmon_path)? { for entry in fs::read_dir(&hwmon_path)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
hwmons.push(path); hwmons.push(path);
}
}
} }
Ok(hwmons) }
}
Ok(hwmons)
}
pub fn read_hwmon_sensor(
hwmon_path: &Path,
sensor: &str,
) -> std::io::Result<Option<f64>> {
let sensor_path = hwmon_path.join(sensor);
if sensor_path.exists() {
let content = fs::read_to_string(sensor_path)?;
Ok(content.trim().parse::<f64>().ok())
} else {
Ok(None)
}
}
pub fn hwmon_info(
hwmon_path: &Path,
) -> std::io::Result<HashMap<String, String>> {
let mut info = HashMap::new();
let name_path = hwmon_path.join("name");
if name_path.exists() {
info.insert(
"name".to_string(),
fs::read_to_string(name_path)?.trim().to_string(),
);
} }
pub fn read_hwmon_sensor(hwmon_path: &Path, sensor: &str) -> std::io::Result<Option<f64>> { if let Ok(files) = fs::read_dir(hwmon_path) {
let sensor_path = hwmon_path.join(sensor); for file in files.flatten() {
if sensor_path.exists() { let filename = file.file_name().to_string_lossy().to_string();
let content = fs::read_to_string(sensor_path)?; if filename.starts_with("temp") && filename.ends_with("_input") {
Ok(content.trim().parse::<f64>().ok()) let id = filename
} else { .trim_start_matches("temp")
Ok(None) .trim_end_matches("_input");
} if let Some(t) = Self::read_hwmon_sensor(hwmon_path, &filename)
} .ok()
.flatten()
pub fn hwmon_info(hwmon_path: &Path) -> std::io::Result<HashMap<String, String>> { {
let mut info = HashMap::new();
let name_path = hwmon_path.join("name");
if name_path.exists() {
info.insert( info.insert(
"name".to_string(), format!("temp_{}_celsius", id),
fs::read_to_string(name_path)?.trim().to_string(), format!("{}", t / 1000.0),
); );
}
} }
if filename.starts_with("fan") && filename.ends_with("_input") {
if let Ok(files) = fs::read_dir(hwmon_path) { let id = filename
for file in files.flatten() { .trim_start_matches("fan")
let filename = file.file_name().to_string_lossy().to_string(); .trim_end_matches("_input");
if filename.starts_with("temp") && filename.ends_with("_input") { if let Some(f) = Self::read_hwmon_sensor(hwmon_path, &filename)
let id = filename .ok()
.trim_start_matches("temp") .flatten()
.trim_end_matches("_input"); {
if let Some(t) = Self::read_hwmon_sensor(hwmon_path, &filename) info.insert(format!("fan_{}_rpm", id), format!("{}", f));
.ok() }
.flatten()
{
info.insert(format!("temp_{}_celsius", id), format!("{}", t / 1000.0));
}
}
if filename.starts_with("fan") && filename.ends_with("_input") {
let id = filename
.trim_start_matches("fan")
.trim_end_matches("_input");
if let Some(f) = Self::read_hwmon_sensor(hwmon_path, &filename)
.ok()
.flatten()
{
info.insert(format!("fan_{}_rpm", id), format!("{}", f));
}
}
if filename.starts_with("in") && filename.ends_with("_input") {
let id = filename.trim_start_matches("in").trim_end_matches("_input");
if let Some(v) = Self::read_hwmon_sensor(hwmon_path, &filename)
.ok()
.flatten()
{
info.insert(format!("voltage_{}_mv", id), format!("{}", v / 1000.0));
}
}
}
} }
if filename.starts_with("in") && filename.ends_with("_input") {
Ok(info) let id = filename.trim_start_matches("in").trim_end_matches("_input");
if let Some(v) = Self::read_hwmon_sensor(hwmon_path, &filename)
.ok()
.flatten()
{
info
.insert(format!("voltage_{}_mv", id), format!("{}", v / 1000.0));
}
}
}
} }
pub fn all_sensors() -> std::io::Result<HashMap<String, HashMap<String, String>>> { Ok(info)
let mut all = HashMap::new(); }
for hwmon in Self::discover_hwmon()? { pub fn all_sensors(
if let Ok(info) = Self::hwmon_info(&hwmon) { ) -> std::io::Result<HashMap<String, HashMap<String, String>>> {
let name = info.get("name").cloned().unwrap_or_else(|| { let mut all = HashMap::new();
hwmon
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string())
});
all.insert(name, info);
}
}
Ok(all) for hwmon in Self::discover_hwmon()? {
if let Ok(info) = Self::hwmon_info(&hwmon) {
let name = info.get("name").cloned().unwrap_or_else(|| {
hwmon
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string())
});
all.insert(name, info);
}
} }
Ok(all)
}
} }

View file

@ -1,45 +1,50 @@
use std::fs; use std::{
use std::time::Duration; fs,
time::Duration,
};
pub struct SystemHelper; pub struct SystemHelper;
impl SystemHelper { impl SystemHelper {
pub fn uptime() -> std::io::Result<Duration> { pub fn uptime() -> std::io::Result<Duration> {
let uptime_secs = fs::read_to_string("/proc/uptime")? let uptime_secs = fs::read_to_string("/proc/uptime")?
.split_whitespace() .split_whitespace()
.next() .next()
.and_then(|s| s.parse::<f64>().ok()) .and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0); .unwrap_or(0.0);
Ok(Duration::from_secs_f64(uptime_secs)) Ok(Duration::from_secs_f64(uptime_secs))
} }
pub fn boot_id() -> std::io::Result<String> { pub fn boot_id() -> std::io::Result<String> {
fs::read_to_string("/proc/sys/kernel/random/boot_id").map(|s| s.trim().to_string()) fs::read_to_string("/proc/sys/kernel/random/boot_id")
} .map(|s| s.trim().to_string())
}
pub fn load_average() -> std::io::Result<(f64, f64, f64)> { pub fn load_average() -> std::io::Result<(f64, f64, f64)> {
let content = fs::read_to_string("/proc/loadavg")?; let content = fs::read_to_string("/proc/loadavg")?;
let mut parts = content.split_whitespace(); let mut parts = content.split_whitespace();
let load1 = parts let load1 = parts
.next() .next()
.and_then(|s| s.parse::<f64>().ok()) .and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0); .unwrap_or(0.0);
let load5 = parts let load5 = parts
.next() .next()
.and_then(|s| s.parse::<f64>().ok()) .and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0); .unwrap_or(0.0);
let load15 = parts let load15 = parts
.next() .next()
.and_then(|s| s.parse::<f64>().ok()) .and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0); .unwrap_or(0.0);
Ok((load1, load5, load15)) Ok((load1, load5, load15))
} }
pub fn hostname() -> std::io::Result<String> { pub fn hostname() -> std::io::Result<String> {
fs::read_to_string("/proc/sys/kernel/hostname").map(|s| s.trim().to_string()) fs::read_to_string("/proc/sys/kernel/hostname")
} .map(|s| s.trim().to_string())
}
pub fn kernel_version() -> std::io::Result<String> { pub fn kernel_version() -> std::io::Result<String> {
fs::read_to_string("/proc/sys/kernel/osrelease").map(|s| s.trim().to_string()) fs::read_to_string("/proc/sys/kernel/osrelease")
} .map(|s| s.trim().to_string())
}
} }

View file

@ -4,8 +4,17 @@ pub mod logging;
pub mod scanner; pub mod scanner;
pub use config::Config; pub use config::Config;
pub use logging::{DaemonLogEntry, LogLevel, RingBufferLogger}; pub use logging::{
DaemonLogEntry,
LogLevel,
RingBufferLogger,
};
pub use scanner::{ pub use scanner::{
get_scanner, register_scanner, MetricValue, Scanner, ScannerCreatorFfi, ScannerError, get_scanner,
register_scanner,
MetricValue,
Scanner,
ScannerCreatorFfi,
ScannerError,
}; };
pub type Result<T> = std::result::Result<T, ScannerError>; pub type Result<T> = std::result::Result<T, ScannerError>;

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,48 @@
use std::collections::HashMap; use std::{
use std::os::raw::c_void; collections::HashMap,
use std::sync::atomic::{AtomicUsize, Ordering}; os::raw::c_void,
use std::sync::LazyLock; sync::{
use std::sync::Mutex; atomic::{
use std::time::Duration; AtomicUsize,
Ordering,
},
LazyLock,
Mutex,
},
time::Duration,
};
use serde::{Deserialize, Serialize}; use serde::{
Deserialize,
Serialize,
};
use thiserror::Error; use thiserror::Error;
pub type ScannerBox = Box<dyn Scanner>; pub type ScannerBox = Box<dyn Scanner>;
pub type ScannerResult = Result<ScannerBox>; pub type ScannerResult = Result<ScannerBox>;
pub type ScannerMetrics = HashMap<String, MetricValue>; pub type ScannerMetrics = HashMap<String, MetricValue>;
pub type ScannerCollectionResult = Result<ScannerMetrics>; pub type ScannerCollectionResult = Result<ScannerMetrics>;
pub type ScannerCollectFn = Box<dyn Fn() -> ScannerCollectionResult + Send + Sync>; pub type ScannerCollectFn =
pub type ScannerInitFnMut = Mutex<Box<dyn FnMut(&toml::Value) -> Result<()> + Send>>; Box<dyn Fn() -> ScannerCollectionResult + Send + Sync>;
pub type ScannerInitFnMut =
Mutex<Box<dyn FnMut(&toml::Value) -> Result<()> + Send>>;
pub type ScannerCleanupFnMut = Mutex<Box<dyn FnMut() -> Result<()> + Send>>; pub type ScannerCleanupFnMut = Mutex<Box<dyn FnMut() -> Result<()> + Send>>;
pub type ScannerCreatorFfi = unsafe extern "C" fn() -> *mut c_void; pub type ScannerCreatorFfi = unsafe extern fn() -> *mut c_void;
pub type ScannerInitFn = unsafe extern "C" fn() -> *mut c_void; pub type ScannerInitFn = unsafe extern fn() -> *mut c_void;
pub type ScannerDropFn = unsafe extern "C" fn(*mut c_void); pub type ScannerDropFn = unsafe extern fn(*mut c_void);
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ScannerError { pub enum ScannerError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Parse error: {0}")] #[error("Parse error: {0}")]
Parse(String), Parse(String),
#[error("Configuration error: {0}")] #[error("Configuration error: {0}")]
Config(String), Config(String),
#[error("Scanner not initialized")] #[error("Scanner not initialized")]
NotInitialized, NotInitialized,
#[error("Scanner {0} not found")] #[error("Scanner {0} not found")]
NotFound(String), NotFound(String),
} }
pub type Result<T> = std::result::Result<T, ScannerError>; pub type Result<T> = std::result::Result<T, ScannerError>;
@ -38,40 +50,40 @@ pub type Result<T> = std::result::Result<T, ScannerError>;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum MetricValue { pub enum MetricValue {
Integer(i64), Integer(i64),
Float(f64), Float(f64),
Boolean(bool), Boolean(bool),
String(String), String(String),
} }
impl MetricValue { impl MetricValue {
pub fn from_i64(v: i64) -> Self { pub fn from_i64(v: i64) -> Self {
MetricValue::Integer(v) MetricValue::Integer(v)
} }
pub fn from_f64(v: f64) -> Self { pub fn from_f64(v: f64) -> Self {
MetricValue::Float(v) MetricValue::Float(v)
} }
pub fn from_bool(v: bool) -> Self { pub fn from_bool(v: bool) -> Self {
MetricValue::Boolean(v) MetricValue::Boolean(v)
} }
pub fn from_string(v: impl Into<String>) -> Self { pub fn from_string(v: impl Into<String>) -> Self {
MetricValue::String(v.into()) MetricValue::String(v.into())
} }
} }
pub trait Scanner: Send + Sync { pub trait Scanner: Send + Sync {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn interval(&self) -> Duration; fn interval(&self) -> Duration;
fn init(&mut self, config: &toml::Value) -> Result<()>; fn init(&mut self, config: &toml::Value) -> Result<()>;
fn collect(&self) -> ScannerCollectionResult; fn collect(&self) -> ScannerCollectionResult;
fn cleanup(&mut self) -> Result<()>; fn cleanup(&mut self) -> Result<()>;
} }
static SCANNER_HANDLES: LazyLock<Mutex<HashMap<usize, ScannerBox>>> = static SCANNER_HANDLES: LazyLock<Mutex<HashMap<usize, ScannerBox>>> =
LazyLock::new(|| Mutex::new(HashMap::new())); LazyLock::new(|| Mutex::new(HashMap::new()));
static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1); static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1);
#[inline] #[inline]
@ -79,9 +91,9 @@ static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1);
/// # Safety /// # Safety
/// The handle must only be used with `get_scanner` from the same process. /// The handle must only be used with `get_scanner` from the same process.
pub unsafe fn register_scanner(scanner: ScannerBox) -> usize { pub unsafe fn register_scanner(scanner: ScannerBox) -> usize {
let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst); let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst);
SCANNER_HANDLES.lock().unwrap().insert(handle, scanner); SCANNER_HANDLES.lock().unwrap().insert(handle, scanner);
handle handle
} }
#[inline] #[inline]
@ -90,65 +102,65 @@ pub unsafe fn register_scanner(scanner: ScannerBox) -> usize {
/// The handle must have been obtained from `register_scanner` in this process, /// The handle must have been obtained from `register_scanner` in this process,
/// and the returned Box must be properly dropped. /// and the returned Box must be properly dropped.
pub unsafe fn get_scanner(handle: *mut c_void) -> Option<ScannerBox> { pub unsafe fn get_scanner(handle: *mut c_void) -> Option<ScannerBox> {
let handle = handle as usize; let handle = handle as usize;
if handle == 0 { if handle == 0 {
None None
} else { } else {
SCANNER_HANDLES.lock().unwrap().remove(&handle) SCANNER_HANDLES.lock().unwrap().remove(&handle)
} }
} }
pub trait ScannerRegistry: Send + Sync { pub trait ScannerRegistry: Send + Sync {
fn list_scanners(&self) -> Vec<&'static str>; fn list_scanners(&self) -> Vec<&'static str>;
fn get_scanner(&self, name: &str) -> Option<ScannerBox>; fn get_scanner(&self, name: &str) -> Option<ScannerBox>;
} }
pub struct DynamicScanner { pub struct DynamicScanner {
name: &'static str, name: &'static str,
interval: Duration, interval: Duration,
collect_fn: ScannerCollectFn, collect_fn: ScannerCollectFn,
init_fn: ScannerInitFnMut, init_fn: ScannerInitFnMut,
cleanup_fn: ScannerCleanupFnMut, cleanup_fn: ScannerCleanupFnMut,
} }
impl DynamicScanner { impl DynamicScanner {
pub fn new( pub fn new(
name: &'static str, name: &'static str,
interval: Duration, interval: Duration,
collect_fn: impl Fn() -> ScannerCollectionResult + Send + Sync + 'static, collect_fn: impl Fn() -> ScannerCollectionResult + Send + Sync + 'static,
init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static, init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static,
cleanup_fn: impl FnMut() -> Result<()> + Send + 'static, cleanup_fn: impl FnMut() -> Result<()> + Send + 'static,
) -> Self { ) -> Self {
Self { Self {
name, name,
interval, interval,
collect_fn: Box::new(collect_fn), collect_fn: Box::new(collect_fn),
init_fn: Mutex::new(Box::new(init_fn)), init_fn: Mutex::new(Box::new(init_fn)),
cleanup_fn: Mutex::new(Box::new(cleanup_fn)), cleanup_fn: Mutex::new(Box::new(cleanup_fn)),
}
} }
}
} }
impl Scanner for DynamicScanner { impl Scanner for DynamicScanner {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
self.name self.name
} }
fn interval(&self) -> Duration { fn interval(&self) -> Duration {
self.interval self.interval
} }
fn init(&mut self, config: &toml::Value) -> Result<()> { fn init(&mut self, config: &toml::Value) -> Result<()> {
let mut init_fn = self.init_fn.lock().unwrap(); let mut init_fn = self.init_fn.lock().unwrap();
(init_fn)(config) (init_fn)(config)
} }
fn collect(&self) -> ScannerCollectionResult { fn collect(&self) -> ScannerCollectionResult {
(self.collect_fn)() (self.collect_fn)()
} }
fn cleanup(&mut self) -> Result<()> { fn cleanup(&mut self) -> Result<()> {
let mut cleanup_fn = self.cleanup_fn.lock().unwrap(); let mut cleanup_fn = self.cleanup_fn.lock().unwrap();
(cleanup_fn)() (cleanup_fn)()
} }
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "pscand-macros" name = "pscand-macros"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -10,5 +10,5 @@ proc-macro = true
[dependencies] [dependencies]
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2", features = ["full"] } syn = { features = [ "full" ], version = "2" }

View file

@ -1,64 +1,67 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, ItemFn}; use syn::{
parse_macro_input,
ItemFn,
};
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream { pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream {
let name_str = parse_macro_input!(name as syn::LitStr).value(); let name_str = parse_macro_input!(name as syn::LitStr).value();
let input = parse_macro_input!(input as ItemFn); let input = parse_macro_input!(input as ItemFn);
let _fn_name = input.sig.ident.clone(); let _fn_name = input.sig.ident.clone();
let body = &input.block; let body = &input.block;
let result = quote! { let result = quote! {
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
struct ScannerImpl; struct ScannerImpl;
impl pscand_core::Scanner for ScannerImpl { impl pscand_core::Scanner for ScannerImpl {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
#name_str #name_str
} }
fn interval(&self) -> std::time::Duration { fn interval(&self) -> std::time::Duration {
std::time::Duration::from_secs(1) std::time::Duration::from_secs(1)
} }
fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> {
Ok(()) Ok(())
} }
fn collect(&self) -> pscand_core::Result<std::collections::HashMap<String, pscand_core::MetricValue>> { fn collect(&self) -> pscand_core::Result<std::collections::HashMap<String, pscand_core::MetricValue>> {
#body #body
} }
fn cleanup(&mut self) -> pscand_core::Result<()> { fn cleanup(&mut self) -> pscand_core::Result<()> {
Ok(()) Ok(())
} }
} }
let handle = unsafe { pscand_core::register_scanner(Box::new(ScannerImpl)) }; let handle = unsafe { pscand_core::register_scanner(Box::new(ScannerImpl)) };
handle as *mut std::os::raw::c_void handle as *mut std::os::raw::c_void
} }
}; };
result.into() result.into()
} }
#[proc_macro] #[proc_macro]
pub fn register_scanner(input: TokenStream) -> TokenStream { pub fn register_scanner(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemFn); let input = parse_macro_input!(input as ItemFn);
let fn_name = input.sig.ident.clone(); let fn_name = input.sig.ident.clone();
let result = quote! { let result = quote! {
#input #input
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
let handle = unsafe { pscand_core::register_scanner(Box::new(#fn_name())) }; let handle = unsafe { pscand_core::register_scanner(Box::new(#fn_name())) };
handle as *mut std::os::raw::c_void handle as *mut std::os::raw::c_void
} }
}; };
result.into() result.into()
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "scanner-power" name = "scanner-power"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -7,9 +7,9 @@ authors.workspace = true
[dependencies] [dependencies]
pscand-core.workspace = true pscand-core.workspace = true
toml.workspace = true toml.workspace = true
[lib] [lib]
name = "scanner_power" crate-type = [ "cdylib" ]
path = "src/lib.rs" name = "scanner_power"
crate-type = ["cdylib"] path = "src/lib.rs"

View file

@ -1,96 +1,104 @@
use pscand_core::helpers::PowerHelper; use std::{
use pscand_core::scanner::{MetricValue, Scanner}; collections::HashMap,
use std::collections::HashMap; time::Duration,
use std::time::Duration; };
use pscand_core::{
helpers::PowerHelper,
scanner::{
MetricValue,
Scanner,
},
};
struct PowerScanner; struct PowerScanner;
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::into_raw(Box::new(PowerScanner)) as *mut std::os::raw::c_void Box::into_raw(Box::new(PowerScanner)) as *mut std::os::raw::c_void
} }
impl Default for PowerScanner { impl Default for PowerScanner {
fn default() -> Self { fn default() -> Self {
Self Self
} }
} }
impl Scanner for PowerScanner { impl Scanner for PowerScanner {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"power" "power"
}
fn interval(&self) -> Duration {
Duration::from_secs(2)
}
fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> {
Ok(())
}
fn collect(&self) -> pscand_core::Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new();
if let Ok(Some(battery)) = PowerHelper::battery_info() {
metrics.insert(
"battery_present".to_string(),
MetricValue::from_bool(battery.present),
);
metrics.insert(
"battery_charge_percent".to_string(),
MetricValue::Integer(battery.charge_percent as i64),
);
metrics.insert(
"battery_voltage_v".to_string(),
MetricValue::from_f64(battery.voltage),
);
metrics.insert(
"battery_power_now_mw".to_string(),
MetricValue::Integer(battery.power_now),
);
metrics.insert(
"battery_status".to_string(),
MetricValue::from_string(&battery.status),
);
} }
fn interval(&self) -> Duration { if let Ok(supplies) = PowerHelper::power_supplies() {
Duration::from_secs(2) for (name, info) in supplies {
} if let Some(status) = info.get("status") {
metrics.insert(
fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { format!("supply_{}_status", name),
Ok(()) MetricValue::from_string(status),
} );
fn collect(&self) -> pscand_core::Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new();
if let Ok(Some(battery)) = PowerHelper::battery_info() {
metrics.insert(
"battery_present".to_string(),
MetricValue::from_bool(battery.present),
);
metrics.insert(
"battery_charge_percent".to_string(),
MetricValue::Integer(battery.charge_percent as i64),
);
metrics.insert(
"battery_voltage_v".to_string(),
MetricValue::from_f64(battery.voltage),
);
metrics.insert(
"battery_power_now_mw".to_string(),
MetricValue::Integer(battery.power_now),
);
metrics.insert(
"battery_status".to_string(),
MetricValue::from_string(&battery.status),
);
} }
if let Some(online) = info.get("online") {
if let Ok(supplies) = PowerHelper::power_supplies() { metrics.insert(
for (name, info) in supplies { format!("supply_{}_online", name),
if let Some(status) = info.get("status") { MetricValue::from_bool(online == "1"),
metrics.insert( );
format!("supply_{}_status", name),
MetricValue::from_string(status),
);
}
if let Some(online) = info.get("online") {
metrics.insert(
format!("supply_{}_online", name),
MetricValue::from_bool(online == "1"),
);
}
if let Some(capacity) = info.get("capacity") {
if let Ok(cap) = capacity.parse::<u32>() {
metrics.insert(
format!("supply_{}_capacity", name),
MetricValue::Integer(cap as i64),
);
}
}
}
} }
if let Some(capacity) = info.get("capacity") {
if let Ok(state) = PowerHelper::suspend_state() { if let Ok(cap) = capacity.parse::<u32>() {
metrics.insert( metrics.insert(
"suspend_state".to_string(), format!("supply_{}_capacity", name),
MetricValue::from_string(&state), MetricValue::Integer(cap as i64),
); );
}
} }
}
Ok(metrics)
} }
fn cleanup(&mut self) -> pscand_core::Result<()> { if let Ok(state) = PowerHelper::suspend_state() {
Ok(()) metrics.insert(
"suspend_state".to_string(),
MetricValue::from_string(&state),
);
} }
Ok(metrics)
}
fn cleanup(&mut self) -> pscand_core::Result<()> {
Ok(())
}
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "scanner-proc" name = "scanner-proc"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -7,9 +7,9 @@ authors.workspace = true
[dependencies] [dependencies]
pscand-core.workspace = true pscand-core.workspace = true
toml.workspace = true toml.workspace = true
[lib] [lib]
name = "scanner_proc" crate-type = [ "cdylib" ]
path = "src/lib.rs" name = "scanner_proc"
crate-type = ["cdylib"] path = "src/lib.rs"

View file

@ -1,99 +1,107 @@
use pscand_core::helpers::ProcessHelper; use std::{
use pscand_core::scanner::{MetricValue, Scanner}; collections::HashMap,
use std::collections::HashMap; time::Duration,
use std::time::Duration; };
use pscand_core::{
helpers::ProcessHelper,
scanner::{
MetricValue,
Scanner,
},
};
struct ProcScanner; struct ProcScanner;
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::into_raw(Box::new(ProcScanner)) as *mut std::os::raw::c_void Box::into_raw(Box::new(ProcScanner)) as *mut std::os::raw::c_void
} }
impl Default for ProcScanner { impl Default for ProcScanner {
fn default() -> Self { fn default() -> Self {
Self Self
} }
} }
impl Scanner for ProcScanner { impl Scanner for ProcScanner {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"proc" "proc"
}
fn interval(&self) -> Duration {
Duration::from_secs(5)
}
fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> {
Ok(())
}
fn collect(&self) -> pscand_core::Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new();
if let Ok(counts) = ProcessHelper::process_count() {
if let Some(total) = counts.get("total") {
metrics.insert(
"process_total".to_string(),
MetricValue::Integer(*total as i64),
);
}
if let Some(running) = counts.get("running") {
metrics.insert(
"process_running".to_string(),
MetricValue::Integer(*running as i64),
);
}
if let Some(sleeping) = counts.get("sleeping") {
metrics.insert(
"process_sleeping".to_string(),
MetricValue::Integer(*sleeping as i64),
);
}
if let Some(zombie) = counts.get("zombie") {
metrics.insert(
"process_zombie".to_string(),
MetricValue::Integer(*zombie as i64),
);
}
} }
fn interval(&self) -> Duration { if let Ok(zombies) = ProcessHelper::zombie_processes() {
Duration::from_secs(5) metrics.insert(
} "zombie_count".to_string(),
MetricValue::Integer(zombies.len() as i64),
);
fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { if !zombies.is_empty() {
Ok(()) let mut zombie_info = Vec::new();
} for z in zombies.iter().take(5) {
zombie_info.push(format!("{}({})", z.name, z.pid));
fn collect(&self) -> pscand_core::Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new();
if let Ok(counts) = ProcessHelper::process_count() {
if let Some(total) = counts.get("total") {
metrics.insert(
"process_total".to_string(),
MetricValue::Integer(*total as i64),
);
}
if let Some(running) = counts.get("running") {
metrics.insert(
"process_running".to_string(),
MetricValue::Integer(*running as i64),
);
}
if let Some(sleeping) = counts.get("sleeping") {
metrics.insert(
"process_sleeping".to_string(),
MetricValue::Integer(*sleeping as i64),
);
}
if let Some(zombie) = counts.get("zombie") {
metrics.insert(
"process_zombie".to_string(),
MetricValue::Integer(*zombie as i64),
);
}
} }
metrics.insert(
if let Ok(zombies) = ProcessHelper::zombie_processes() { "zombie_processes".to_string(),
metrics.insert( MetricValue::from_string(zombie_info.join(",")),
"zombie_count".to_string(), );
MetricValue::Integer(zombies.len() as i64), }
);
if !zombies.is_empty() {
let mut zombie_info = Vec::new();
for z in zombies.iter().take(5) {
zombie_info.push(format!("{}({})", z.name, z.pid));
}
metrics.insert(
"zombie_processes".to_string(),
MetricValue::from_string(zombie_info.join(",")),
);
}
}
if let Ok(top_mem) = ProcessHelper::top_memory_processes(3) {
for (i, proc) in top_mem.iter().enumerate() {
metrics.insert(
format!("top_mem_{}_name", i + 1),
MetricValue::from_string(&proc.name),
);
metrics.insert(
format!("top_mem_{}_mb", i + 1),
MetricValue::Integer((proc.memory_kb / 1024) as i64),
);
}
}
Ok(metrics)
} }
fn cleanup(&mut self) -> pscand_core::Result<()> { if let Ok(top_mem) = ProcessHelper::top_memory_processes(3) {
Ok(()) for (i, proc) in top_mem.iter().enumerate() {
metrics.insert(
format!("top_mem_{}_name", i + 1),
MetricValue::from_string(&proc.name),
);
metrics.insert(
format!("top_mem_{}_mb", i + 1),
MetricValue::Integer((proc.memory_kb / 1024) as i64),
);
}
} }
Ok(metrics)
}
fn cleanup(&mut self) -> pscand_core::Result<()> {
Ok(())
}
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "scanner-sensor" name = "scanner-sensor"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -7,9 +7,9 @@ authors.workspace = true
[dependencies] [dependencies]
pscand-core.workspace = true pscand-core.workspace = true
toml.workspace = true toml.workspace = true
[lib] [lib]
name = "scanner_sensor" crate-type = [ "cdylib" ]
path = "src/lib.rs" name = "scanner_sensor"
crate-type = ["cdylib"] path = "src/lib.rs"

View file

@ -1,79 +1,92 @@
use pscand_core::helpers::SensorHelper; use std::{
use pscand_core::scanner::{MetricValue, Scanner}; collections::HashMap,
use pscand_core::Result; time::Duration,
use std::collections::HashMap; };
use std::time::Duration;
use pscand_core::{
helpers::SensorHelper,
scanner::{
MetricValue,
Scanner,
},
Result,
};
struct SensorScanner; struct SensorScanner;
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::into_raw(Box::new(SensorScanner)) as *mut std::os::raw::c_void Box::into_raw(Box::new(SensorScanner)) as *mut std::os::raw::c_void
} }
impl Default for SensorScanner { impl Default for SensorScanner {
fn default() -> Self { fn default() -> Self {
Self Self
} }
} }
impl Scanner for SensorScanner { impl Scanner for SensorScanner {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"sensor" "sensor"
} }
fn interval(&self) -> Duration { fn interval(&self) -> Duration {
Duration::from_secs(2) Duration::from_secs(2)
} }
fn init(&mut self, _config: &toml::Value) -> Result<()> { fn init(&mut self, _config: &toml::Value) -> Result<()> {
Ok(()) Ok(())
} }
fn collect(&self) -> Result<HashMap<String, MetricValue>> { fn collect(&self) -> Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new(); let mut metrics = HashMap::new();
if let Ok(sensors) = SensorHelper::all_sensors() { if let Ok(sensors) = SensorHelper::all_sensors() {
let mut temp_count = 0; let mut temp_count = 0;
let mut fan_count = 0; let mut fan_count = 0;
for (hwmon, values) in sensors { for (hwmon, values) in sensors {
for (key, value) in values { for (key, value) in values {
if key.starts_with("temp_") && key.ends_with("_celsius") { if key.starts_with("temp_") && key.ends_with("_celsius") {
if let Ok(v) = value.parse::<f64>() { if let Ok(v) = value.parse::<f64>() {
metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); metrics
temp_count += 1; .insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
if temp_count <= 3 { temp_count += 1;
metrics.insert( if temp_count <= 3 {
format!("temp_{}", temp_count), metrics.insert(
MetricValue::from_f64(v), format!("temp_{}", temp_count),
); MetricValue::from_f64(v),
} );
} }
}
if key.starts_with("fan_") && key.ends_with("_rpm") {
if let Ok(v) = value.parse::<f64>() {
metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
fan_count += 1;
if fan_count <= 2 {
metrics
.insert(format!("fan_{}", fan_count), MetricValue::from_f64(v));
}
}
}
if key.starts_with("voltage_") {
if let Ok(v) = value.parse::<f64>() {
metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
}
}
}
} }
}
if key.starts_with("fan_") && key.ends_with("_rpm") {
if let Ok(v) = value.parse::<f64>() {
metrics
.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
fan_count += 1;
if fan_count <= 2 {
metrics.insert(
format!("fan_{}", fan_count),
MetricValue::from_f64(v),
);
}
}
}
if key.starts_with("voltage_") {
if let Ok(v) = value.parse::<f64>() {
metrics
.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
}
}
} }
}
Ok(metrics)
} }
fn cleanup(&mut self) -> Result<()> { Ok(metrics)
Ok(()) }
}
fn cleanup(&mut self) -> Result<()> {
Ok(())
}
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "scanner-system" name = "scanner-system"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -7,9 +7,9 @@ authors.workspace = true
[dependencies] [dependencies]
pscand-core.workspace = true pscand-core.workspace = true
toml.workspace = true toml.workspace = true
[lib] [lib]
name = "scanner_system" crate-type = [ "cdylib" ]
path = "src/lib.rs" name = "scanner_system"
crate-type = ["cdylib"] path = "src/lib.rs"

View file

@ -1,80 +1,94 @@
use pscand_core::helpers::{ResourceHelper, SystemHelper}; use std::{
use pscand_core::scanner::{MetricValue, Scanner}; collections::HashMap,
use pscand_core::Result; time::Duration,
use std::collections::HashMap; };
use std::time::Duration;
use pscand_core::{
helpers::{
ResourceHelper,
SystemHelper,
},
scanner::{
MetricValue,
Scanner,
},
Result,
};
#[derive(Default)] #[derive(Default)]
struct SystemScanner { struct SystemScanner {
_prev_cpu: Option<HashMap<String, f64>>, _prev_cpu: Option<HashMap<String, f64>>,
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { pub extern fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::into_raw(Box::new(SystemScanner::default())) as *mut std::os::raw::c_void Box::into_raw(Box::new(SystemScanner::default())) as *mut std::os::raw::c_void
} }
impl Scanner for SystemScanner { impl Scanner for SystemScanner {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"system" "system"
}
fn interval(&self) -> Duration {
Duration::from_secs(1)
}
fn init(&mut self, _config: &toml::Value) -> Result<()> {
Ok(())
}
fn collect(&self) -> Result<HashMap<String, MetricValue>> {
let mut metrics = HashMap::new();
if let Ok(uptime) = SystemHelper::uptime() {
metrics.insert(
"uptime_secs".to_string(),
MetricValue::from_f64(uptime.as_secs_f64()),
);
} }
fn interval(&self) -> Duration { if let Ok((load1, load5, load15)) = SystemHelper::load_average() {
Duration::from_secs(1) metrics.insert("load_1m".to_string(), MetricValue::from_f64(load1));
metrics.insert("load_5m".to_string(), MetricValue::from_f64(load5));
metrics.insert("load_15m".to_string(), MetricValue::from_f64(load15));
} }
fn init(&mut self, _config: &toml::Value) -> Result<()> { if let Ok(cpu) = ResourceHelper::cpu_usage() {
Ok(()) if let Some(total) = cpu.get("total_usage_percent") {
metrics
.insert("cpu_percent".to_string(), MetricValue::from_f64(*total));
}
} }
fn collect(&self) -> Result<HashMap<String, MetricValue>> { if let Ok(mem) = ResourceHelper::memory_info() {
let mut metrics = HashMap::new(); if let Some(total) = mem.get("MemTotal") {
metrics.insert(
if let Ok(uptime) = SystemHelper::uptime() { "mem_total_bytes".to_string(),
metrics.insert( MetricValue::Integer(*total as i64),
"uptime_secs".to_string(), );
MetricValue::from_f64(uptime.as_secs_f64()), }
); if let Some(available) = mem.get("MemAvailable") {
} metrics.insert(
"mem_available_bytes".to_string(),
if let Ok((load1, load5, load15)) = SystemHelper::load_average() { MetricValue::Integer(*available as i64),
metrics.insert("load_1m".to_string(), MetricValue::from_f64(load1)); );
metrics.insert("load_5m".to_string(), MetricValue::from_f64(load5)); }
metrics.insert("load_15m".to_string(), MetricValue::from_f64(load15)); if let (Some(used), Some(total)) =
} (mem.get("MemAvailable"), mem.get("MemTotal"))
{
if let Ok(cpu) = ResourceHelper::cpu_usage() { let used_mem = total.saturating_sub(*used);
if let Some(total) = cpu.get("total_usage_percent") { metrics.insert(
metrics.insert("cpu_percent".to_string(), MetricValue::from_f64(*total)); "mem_used_bytes".to_string(),
} MetricValue::Integer(used_mem as i64),
} );
}
if let Ok(mem) = ResourceHelper::memory_info() {
if let Some(total) = mem.get("MemTotal") {
metrics.insert(
"mem_total_bytes".to_string(),
MetricValue::Integer(*total as i64),
);
}
if let Some(available) = mem.get("MemAvailable") {
metrics.insert(
"mem_available_bytes".to_string(),
MetricValue::Integer(*available as i64),
);
}
if let (Some(used), Some(total)) = (mem.get("MemAvailable"), mem.get("MemTotal")) {
let used_mem = total.saturating_sub(*used);
metrics.insert(
"mem_used_bytes".to_string(),
MetricValue::Integer(used_mem as i64),
);
}
}
Ok(metrics)
} }
fn cleanup(&mut self) -> Result<()> { Ok(metrics)
Ok(()) }
}
fn cleanup(&mut self) -> Result<()> {
Ok(())
}
} }