diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..b887d71 --- /dev/null +++ b/.rustfmt.toml @@ -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 + diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..68b8a84 --- /dev/null +++ b/.taplo.toml @@ -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 + diff --git a/Cargo.toml b/Cargo.toml index 26dfdc5..8f5867e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,4 @@ [workspace] -resolver = "3" members = [ "crates/*", "scanners/scanner-system", @@ -7,34 +6,35 @@ members = [ "scanners/scanner-power", "scanners/scanner-proc", ] +resolver = "3" [workspace.package] -version = "0.1.0" +authors = [ "NotAShelf " ] edition = "2021" license = "MPL-2.0" -authors = ["NotAShelf "] +version = "0.1.0" [workspace.dependencies] -pscand-core = { path = "./crates/pscand-core" } +pscand-core = { path = "./crates/pscand-core" } pscand-macros = { path = "./crates/pscand-macros" } -tokio = { version = "1.49.0", features = ["full"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" -toml = "1.0.0" -libloading = "0.9.0" -chrono = { version = "0.4.43", features = ["serde"] } -sysinfo = "0.38.2" -log = "0.4.29" -env_logger = "0.11.9" -thiserror = "2.0.18" +chrono = { features = [ "serde" ], version = "0.4.43" } +clap = { features = [ "derive" ], version = "4.5.59" } +dirs = "6.0.0" +env_logger = "0.11.9" +libloading = "0.9.0" +log = "0.4.29" parking_lot = "0.12.5" -ringbuf = "0.4.8" -dirs = "6.0.0" -clap = { version = "4.5.59", features = ["derive"] } -sha2 = "0.10.9" +ringbuf = "0.4.8" +serde = { features = [ "derive" ], version = "1.0.228" } +serde_json = "1.0.149" +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] -lto = true -opt-level = "z" codegen-units = 1 +lto = true +opt-level = "z" diff --git a/config/pscand.toml b/config/pscand.toml index 950d28f..67f8ed9 100644 --- a/config/pscand.toml +++ b/config/pscand.toml @@ -4,10 +4,10 @@ # Directories to load scanner plugins from # Set via PSCAND_SCANNER_DIRS environment variable or configure here scanner_dirs = [ - # Examples (uncomment and adjust for your system): - # "/usr/lib/pscand/scanners", - # "/var/lib/pscand/scanners", - "~/.local/share/pscand/scanners", + # Examples (uncomment and adjust for your system): + # "/usr/lib/pscand/scanners", + # "/var/lib/pscand/scanners", + "~/.local/share/pscand/scanners", ] # Where to store log files @@ -27,19 +27,19 @@ retention_days = 7 # Per-scanner configuration [scanners.system] -enabled = true -interval_secs = 5 # Override default 1-second interval +enabled = true +interval_secs = 5 # Override default 1-second interval [scanners.sensor] -enabled = true -interval_secs = 10 # Sensors don't change as fast +enabled = true +interval_secs = 10 # Sensors don't change as fast [scanners.power] -enabled = true -interval_secs = 30 # Battery status changes slowly +enabled = true +interval_secs = 30 # Battery status changes slowly [scanners.proc] -enabled = true +enabled = true interval_secs = 5 # Example: Custom scanner with extra parameters diff --git a/contrib/pscand.example.toml b/contrib/pscand.example.toml index b032bb8..6f3d6d0 100644 --- a/contrib/pscand.example.toml +++ b/contrib/pscand.example.toml @@ -1,27 +1,25 @@ -log_dir = "/var/log/pscand" -retention_days = 7 +file_enabled = true +journal_enabled = true +log_dir = "/var/log/pscand" +retention_days = 7 ring_buffer_size = 60 -journal_enabled = true -file_enabled = true [scanner_dirs] # Directories to load scanner plugins from -dirs = [ - "~/.local/share/pscand/scanners", -] +dirs = [ "~/.local/share/pscand/scanners" ] [scanners.system] -enabled = true +enabled = true interval_secs = 1 [scanners.sensor] -enabled = true +enabled = true interval_secs = 2 [scanners.power] -enabled = true +enabled = true interval_secs = 2 [scanners.proc] -enabled = true +enabled = true interval_secs = 5 diff --git a/crates/pscand-cli/Cargo.toml b/crates/pscand-cli/Cargo.toml index 24406ca..36d98c8 100644 --- a/crates/pscand-cli/Cargo.toml +++ b/crates/pscand-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pscand-cli" +name = "pscand-cli" version.workspace = true edition.workspace = true license.workspace = true @@ -10,20 +10,20 @@ name = "pscand" path = "src/main.rs" [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 -tokio.workspace = true -serde.workspace = true -serde_json.workspace = true -toml.workspace = true -libloading.workspace = true -chrono.workspace = true -log.workspace = true -env_logger.workspace = true -thiserror.workspace = true -parking_lot.workspace = true -ringbuf.workspace = true -dirs.workspace = true -sysinfo.workspace = true -clap.workspace = true -sha2.workspace = true +ringbuf.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +sysinfo.workspace = true +thiserror.workspace = true +tokio.workspace = true +toml.workspace = true diff --git a/crates/pscand-cli/src/main.rs b/crates/pscand-cli/src/main.rs index b32fe8c..478e882 100644 --- a/crates/pscand-cli/src/main.rs +++ b/crates/pscand-cli/src/main.rs @@ -1,19 +1,45 @@ #![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 libloading::Library; -use pscand_core::Config as CoreConfig; -use pscand_core::logging::{LogLevel, RingBufferLogger}; -use pscand_core::scanner::Scanner; -use sha2::{Digest, Sha256}; -use std::fs; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::RwLock; -use tokio::time::interval; +use pscand_core::{ + logging::{ + LogLevel, + RingBufferLogger, + }, + scanner::Scanner, + Config as CoreConfig, +}; +use sha2::{ + Digest, + Sha256, +}; +use tokio::{ + sync::RwLock, + time::interval, +}; type ScannerCreator = pscand_core::ScannerCreatorFfi; @@ -79,22 +105,22 @@ fn verify_library(path: &Path) -> Result<(), String> { } struct LoadedScanner { - name: String, - scanner: Arc>>, + name: String, + scanner: Arc>>, interval: Duration, #[allow(dead_code)] - library: Library, + library: Library, } #[derive(Clone)] struct DaemonState { - running: Arc, + running: Arc, shutdown_requested: Arc, - start_time: Arc>, - last_collection: Arc>, - collection_count: Arc>, - error_count: Arc>, - heartbeat_path: PathBuf, + start_time: Arc>, + last_collection: Arc>, + collection_count: Arc>, + error_count: Arc>, + heartbeat_path: PathBuf, } impl DaemonState { @@ -334,7 +360,8 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box> { " 1. Scanner plugins are installed in one of the configured directories" ); 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"); logger.log( @@ -664,7 +691,8 @@ async fn list_scanners() -> Result<(), Box> { "\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)" ); println!( - " Default fallback: ~/.local/share/pscand/scanners/ or ~/.config/pscand/scanners/" + " Default fallback: ~/.local/share/pscand/scanners/ or \ + ~/.config/pscand/scanners/" ); Ok(()) } diff --git a/crates/pscand-core/Cargo.toml b/crates/pscand-core/Cargo.toml index a711562..16d216c 100644 --- a/crates/pscand-core/Cargo.toml +++ b/crates/pscand-core/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "pscand-core" +name = "pscand-core" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -tokio.workspace = true -serde.workspace = true -serde_json.workspace = true -toml.workspace = true -chrono.workspace = true -log.workspace = true -thiserror.workspace = true +chrono.workspace = true +dirs.workspace = true +log.workspace = true parking_lot.workspace = true -dirs.workspace = true -sysinfo.workspace = true -ringbuf.workspace = true +ringbuf.workspace = true +serde.workspace = true +serde_json.workspace = true +sysinfo.workspace = true +thiserror.workspace = true +tokio.workspace = true +toml.workspace = true [lib] name = "pscand_core" diff --git a/crates/pscand-core/src/config.rs b/crates/pscand-core/src/config.rs index f800719..22d551c 100644 --- a/crates/pscand-core/src/config.rs +++ b/crates/pscand-core/src/config.rs @@ -1,126 +1,135 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{ + Path, + PathBuf, + }, +}; + +use serde::{ + Deserialize, + Serialize, +}; use thiserror::Error; #[derive(Error, Debug)] pub enum ConfigError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("Parse error: {0}")] - Parse(#[from] toml::de::Error), - #[error("Scanner {0} not configured")] - ScannerNotConfigured(String), - #[error("Invalid scanner name: {0}")] - InvalidScannerName(String), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Parse error: {0}")] + Parse(#[from] toml::de::Error), + #[error("Scanner {0} not configured")] + ScannerNotConfigured(String), + #[error("Invalid scanner name: {0}")] + InvalidScannerName(String), } pub type ConfigResult = std::result::Result; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScannerConfig { - pub enabled: bool, - pub interval_secs: Option, - pub extra: HashMap, + pub enabled: bool, + pub interval_secs: Option, + pub extra: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - #[serde(default = "default_scanner_dirs")] - pub scanner_dirs: Vec, - #[serde(default = "default_log_dir")] - pub log_dir: PathBuf, - #[serde(default = "default_retention_days")] - pub retention_days: u32, - #[serde(default = "default_ring_buffer_size")] - pub ring_buffer_size: usize, - #[serde(default)] - pub scanners: HashMap, - #[serde(default)] - pub journal_enabled: bool, - #[serde(default)] - pub file_enabled: bool, + #[serde(default = "default_scanner_dirs")] + pub scanner_dirs: Vec, + #[serde(default = "default_log_dir")] + pub log_dir: PathBuf, + #[serde(default = "default_retention_days")] + pub retention_days: u32, + #[serde(default = "default_ring_buffer_size")] + pub ring_buffer_size: usize, + #[serde(default)] + pub scanners: HashMap, + #[serde(default)] + pub journal_enabled: bool, + #[serde(default)] + pub file_enabled: bool, } fn default_scanner_dirs() -> Vec { - let mut dirs = Vec::new(); + let mut dirs = Vec::new(); - if let Ok(env_var) = std::env::var("PSCAND_SCANNER_DIRS") { - for path in env_var.split(':') { - let path = PathBuf::from(path); - if !path.as_os_str().is_empty() { - dirs.push(path); - } - } - if !dirs.is_empty() { - return dirs; - } + if let Ok(env_var) = std::env::var("PSCAND_SCANNER_DIRS") { + for path in env_var.split(':') { + let path = PathBuf::from(path); + if !path.as_os_str().is_empty() { + dirs.push(path); + } } - - if let Some(lib) = std::env::var_os("LIB_PSCAND") { - dirs.push(PathBuf::from(lib)); + if !dirs.is_empty() { + return dirs; } + } - if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") { - dirs.push(PathBuf::from(lib_dir)); - } + if let Some(lib) = std::env::var_os("LIB_PSCAND") { + dirs.push(PathBuf::from(lib)); + } - if let Some(local) = dirs::data_local_dir() { - dirs.push(local.join("pscand/scanners")); - } + if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") { + dirs.push(PathBuf::from(lib_dir)); + } - if let Some(config) = dirs::config_dir() { - dirs.push(config.join("pscand/scanners")); - } + if let Some(local) = dirs::data_local_dir() { + dirs.push(local.join("pscand/scanners")); + } - if dirs.is_empty() { - dirs.push(PathBuf::from(".pscand/scanners")); - } + if let Some(config) = dirs::config_dir() { + dirs.push(config.join("pscand/scanners")); + } - dirs + if dirs.is_empty() { + dirs.push(PathBuf::from(".pscand/scanners")); + } + + dirs } fn default_log_dir() -> PathBuf { - dirs::data_local_dir() - .map(|p| p.join("pscand/logs")) - .unwrap_or_else(|| PathBuf::from(".pscand/logs")) + dirs::data_local_dir() + .map(|p| p.join("pscand/logs")) + .unwrap_or_else(|| PathBuf::from(".pscand/logs")) } fn default_retention_days() -> u32 { - 7 + 7 } fn default_ring_buffer_size() -> usize { - 60 + 60 } impl Default for Config { - fn default() -> Self { - Self { - scanner_dirs: default_scanner_dirs(), - log_dir: default_log_dir(), - retention_days: default_retention_days(), - ring_buffer_size: default_ring_buffer_size(), - scanners: HashMap::new(), - journal_enabled: true, - file_enabled: true, - } + fn default() -> Self { + Self { + scanner_dirs: default_scanner_dirs(), + log_dir: default_log_dir(), + retention_days: default_retention_days(), + ring_buffer_size: default_ring_buffer_size(), + scanners: HashMap::new(), + journal_enabled: true, + file_enabled: true, } + } } impl Config { - pub fn load(path: &Path) -> ConfigResult { - let content = std::fs::read_to_string(path)?; - let mut config: Config = toml::from_str(&content)?; - config.scanner_dirs.retain(|p| p.exists()); - Ok(config) - } + pub fn load(path: &Path) -> ConfigResult { + let content = std::fs::read_to_string(path)?; + let mut config: Config = toml::from_str(&content)?; + config.scanner_dirs.retain(|p| p.exists()); + Ok(config) + } - pub fn scanner_config(&self, name: &str) -> Option<&ScannerConfig> { - self.scanners.get(name) - } + pub fn scanner_config(&self, name: &str) -> Option<&ScannerConfig> { + self.scanners.get(name) + } - pub fn is_scanner_enabled(&self, name: &str) -> bool { - self.scanners.get(name).map(|c| c.enabled).unwrap_or(true) - } + pub fn is_scanner_enabled(&self, name: &str) -> bool { + self.scanners.get(name).map(|c| c.enabled).unwrap_or(true) + } } diff --git a/crates/pscand-core/src/helpers/power.rs b/crates/pscand-core/src/helpers/power.rs index 0277f07..0077eff 100644 --- a/crates/pscand-core/src/helpers/power.rs +++ b/crates/pscand-core/src/helpers/power.rs @@ -1,157 +1,160 @@ -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; +use std::{ + collections::HashMap, + fs, + path::PathBuf, +}; pub struct PowerHelper; #[derive(Debug, Clone)] pub struct BatteryInfo { - pub name: String, - pub status: String, - pub capacity: u32, - pub charge_percent: i32, - pub voltage: f64, - pub current_now: i64, - pub power_now: i64, - pub present: bool, + pub name: String, + pub status: String, + pub capacity: u32, + pub charge_percent: i32, + pub voltage: f64, + pub current_now: i64, + pub power_now: i64, + pub present: bool, } impl PowerHelper { - pub fn battery_info() -> std::io::Result> { - let battery_path = PathBuf::from("/sys/class/power_supply"); + pub fn battery_info() -> std::io::Result> { + let battery_path = PathBuf::from("/sys/class/power_supply"); - for entry in fs::read_dir(&battery_path)? { - let entry = entry?; - let path = entry.path(); - let type_path = path.join("type"); + for entry in fs::read_dir(&battery_path)? { + let entry = entry?; + let path = entry.path(); + let type_path = path.join("type"); - if type_path.exists() { - let battery_type = fs::read_to_string(&type_path)?.trim().to_string(); - if battery_type == "Battery" { - let present_path = path.join("present"); - let present = fs::read_to_string(&present_path) - .map(|s| s.trim() == "1") - .unwrap_or(false); + if type_path.exists() { + let battery_type = fs::read_to_string(&type_path)?.trim().to_string(); + if battery_type == "Battery" { + let present_path = path.join("present"); + let present = fs::read_to_string(&present_path) + .map(|s| s.trim() == "1") + .unwrap_or(false); - if !present { - continue; - } + if !present { + continue; + } - let name = fs::read_to_string(path.join("name")) - .unwrap_or_else(|_| "Unknown".to_string()) - .trim() - .to_string(); + let name = fs::read_to_string(path.join("name")) + .unwrap_or_else(|_| "Unknown".to_string()) + .trim() + .to_string(); - let status = fs::read_to_string(path.join("status")) - .unwrap_or_else(|_| "Unknown".to_string()) - .trim() - .to_string(); + let status = fs::read_to_string(path.join("status")) + .unwrap_or_else(|_| "Unknown".to_string()) + .trim() + .to_string(); - let capacity = fs::read_to_string(path.join("capacity")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0); + let capacity = fs::read_to_string(path.join("capacity")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); - let charge_now = fs::read_to_string(path.join("charge_now")) - .or_else(|_| fs::read_to_string(path.join("energy_now"))) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0); + let charge_now = fs::read_to_string(path.join("charge_now")) + .or_else(|_| fs::read_to_string(path.join("energy_now"))) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); - let charge_full = fs::read_to_string(path.join("charge_full")) - .or_else(|_| fs::read_to_string(path.join("energy_full"))) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(1); + let charge_full = fs::read_to_string(path.join("charge_full")) + .or_else(|_| fs::read_to_string(path.join("energy_full"))) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(1); - let charge_percent = if charge_full > 0 { - ((charge_now as f64 / charge_full as f64) * 100.0) as i32 - } else { - 0 - }; + let charge_percent = if charge_full > 0 { + ((charge_now as f64 / charge_full as f64) * 100.0) as i32 + } else { + 0 + }; - let voltage = fs::read_to_string(path.join("voltage_now")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0.0) - / 1_000_000.0; + let voltage = fs::read_to_string(path.join("voltage_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0.0) + / 1_000_000.0; - let current_now = fs::read_to_string(path.join("current_now")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0); + let current_now = fs::read_to_string(path.join("current_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); - let power_now = fs::read_to_string(path.join("power_now")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0); + let power_now = fs::read_to_string(path.join("power_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); - return Ok(Some(BatteryInfo { - name, - status, - capacity, - charge_percent, - voltage, - current_now, - power_now, - present, - })); - } - } + return Ok(Some(BatteryInfo { + name, + status, + capacity, + charge_percent, + voltage, + current_now, + power_now, + present, + })); } - - Ok(None) + } } - pub fn power_supplies() -> std::io::Result>> { - let mut supplies = HashMap::new(); - let power_supply_path = PathBuf::from("/sys/class/power_supply"); + Ok(None) + } - if !power_supply_path.exists() { - return Ok(supplies); + pub fn power_supplies( + ) -> std::io::Result>> { + 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)? { - 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()); - } - } - } - - if !info.is_empty() { - supplies.insert(name, info); - } - } - - Ok(supplies) + if !info.is_empty() { + supplies.insert(name, info); + } } - pub fn suspend_state() -> std::io::Result { - let state_path = PathBuf::from("/sys/power/state"); - fs::read_to_string(state_path).map(|s| s.trim().to_string()) - } + Ok(supplies) + } - pub fn mem_sleep_state() -> std::io::Result { - let state_path = PathBuf::from("/sys/power/mem_sleep"); - fs::read_to_string(state_path).map(|s| s.trim().to_string()) - } + pub fn suspend_state() -> std::io::Result { + 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 { + let state_path = PathBuf::from("/sys/power/mem_sleep"); + fs::read_to_string(state_path).map(|s| s.trim().to_string()) + } } diff --git a/crates/pscand-core/src/helpers/process.rs b/crates/pscand-core/src/helpers/process.rs index d471ca5..66797cc 100644 --- a/crates/pscand-core/src/helpers/process.rs +++ b/crates/pscand-core/src/helpers/process.rs @@ -1,136 +1,144 @@ -use std::collections::HashMap; -use std::fs; +use std::{ + collections::HashMap, + fs, +}; pub struct ProcessHelper; #[derive(Debug)] pub struct ProcessInfo { - pub pid: u32, - pub name: String, - pub state: String, - pub ppid: u32, - pub memory_kb: u64, - pub cpu_percent: f32, + pub pid: u32, + pub name: String, + pub state: String, + pub ppid: u32, + pub memory_kb: u64, + pub cpu_percent: f32, } impl ProcessHelper { - pub fn list_processes() -> std::io::Result> { - let mut processes = Vec::new(); - let proc_path = fs::read_dir("/proc")?; + pub fn list_processes() -> std::io::Result> { + let mut processes = Vec::new(); + let proc_path = fs::read_dir("/proc")?; - for entry in proc_path.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } + for entry in proc_path.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } - let pid: u32 = match path.file_name() { - Some(name) => match name.to_str() { - Some(s) => s.parse().ok(), - None => None, - } - .ok_or(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid pid", - ))?, - None => continue, - }; + let pid: u32 = match path.file_name() { + Some(name) => { + match name.to_str() { + Some(s) => s.parse().ok(), + None => None, + } + .ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid pid", + ))? + }, + None => continue, + }; - if let Ok(info) = Self::process_info(pid) { - processes.push(info); - } - } - - Ok(processes) + if let Ok(info) = Self::process_info(pid) { + processes.push(info); + } } - pub fn process_info(pid: u32) -> std::io::Result { - let status_path = format!("/proc/{}/status", pid); - let content = fs::read_to_string(status_path)?; + Ok(processes) + } - let mut name = String::new(); - let mut state = String::new(); - let mut ppid: u32 = 0; - let mut memory_kb: u64 = 0; + pub fn process_info(pid: u32) -> std::io::Result { + let status_path = format!("/proc/{}/status", pid); + let content = fs::read_to_string(status_path)?; - for line in content.lines() { - if line.starts_with("Name:") { - name = line - .split_whitespace() - .skip(1) - .collect::>() - .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); - } - } + let mut name = String::new(); + let mut state = String::new(); + let mut ppid: u32 = 0; + let mut memory_kb: u64 = 0; - Ok(ProcessInfo { - pid, - name, - state, - ppid, - memory_kb, - cpu_percent: 0.0, - }) + for line in content.lines() { + if line.starts_with("Name:") { + name = line + .split_whitespace() + .skip(1) + .collect::>() + .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> { - Ok(Self::list_processes()? - .into_iter() - .filter(|p| p.state.starts_with('Z')) - .collect()) + Ok(ProcessInfo { + pid, + name, + state, + ppid, + memory_kb, + cpu_percent: 0.0, + }) + } + + pub fn zombie_processes() -> std::io::Result> { + Ok( + Self::list_processes()? + .into_iter() + .filter(|p| p.state.starts_with('Z')) + .collect(), + ) + } + + pub fn process_count() -> std::io::Result> { + 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> { - 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); + Ok(counts) + } - for proc in Self::list_processes()? { - *counts.get_mut("total").unwrap() += 1; + pub fn top_memory_processes( + count: usize, + ) -> std::io::Result> { + 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(' '); - 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, - _ => {} - } - } - - Ok(counts) - } - - pub fn top_memory_processes(count: usize) -> std::io::Result> { - 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> { - 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) - } + pub fn top_cpu_processes(count: usize) -> std::io::Result> { + 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) + } } diff --git a/crates/pscand-core/src/helpers/resource.rs b/crates/pscand-core/src/helpers/resource.rs index 8522523..0d32d26 100644 --- a/crates/pscand-core/src/helpers/resource.rs +++ b/crates/pscand-core/src/helpers/resource.rs @@ -1,123 +1,129 @@ -use std::collections::HashMap; -use std::fs; +use std::{ + collections::HashMap, + fs, +}; pub struct ResourceHelper; impl ResourceHelper { - pub fn cpu_usage() -> std::io::Result> { - let content = fs::read_to_string("/proc/stat")?; - let mut result = HashMap::new(); + pub fn cpu_usage() -> std::io::Result> { + let content = fs::read_to_string("/proc/stat")?; + let mut result = HashMap::new(); - for line in content.lines() { - if line.starts_with("cpu") { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 5 { - let cpu = parts[0]; - let values: Vec = parts[1..] - .iter() - .take(7) - .filter_map(|s| s.parse().ok()) - .collect(); + for line in content.lines() { + if line.starts_with("cpu") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 5 { + let cpu = parts[0]; + let values: Vec = parts[1..] + .iter() + .take(7) + .filter_map(|s| s.parse().ok()) + .collect(); - if values.len() >= 4 { - let user = values[0] as f64; - let nice = values[1] as f64; - let system = values[2] as f64; - let idle = values[3] 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 softirq = values.get(6).copied().unwrap_or(0) as f64; + if values.len() >= 4 { + let user = values[0] as f64; + let nice = values[1] as f64; + let system = values[2] as f64; + let idle = values[3] 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 softirq = values.get(6).copied().unwrap_or(0) as f64; - let total = user + nice + system + idle + iowait + irq + softirq; - let active = user + nice + system + irq + softirq; + let total = user + nice + system + idle + iowait + irq + softirq; + let active = user + nice + system + irq + softirq; - if cpu == "cpu" { - result.insert("total_user".to_string(), user); - result.insert("total_nice".to_string(), nice); - result.insert("total_system".to_string(), system); - result.insert("total_idle".to_string(), idle); - result.insert("total_iowait".to_string(), iowait); - result.insert( - "total_usage_percent".to_string(), - (active / total) * 100.0, - ); - } else { - let core = cpu.replace("cpu", "core_"); - result.insert(format!("{}_user", core), user); - result.insert( - format!("{}_usage_percent", core), - (active / total) * 100.0, - ); - } - } - } + if cpu == "cpu" { + result.insert("total_user".to_string(), user); + result.insert("total_nice".to_string(), nice); + result.insert("total_system".to_string(), system); + result.insert("total_idle".to_string(), idle); + result.insert("total_iowait".to_string(), iowait); + result.insert( + "total_usage_percent".to_string(), + (active / total) * 100.0, + ); + } else { + let core = cpu.replace("cpu", "core_"); + result.insert(format!("{}_user", core), user); + result.insert( + format!("{}_usage_percent", core), + (active / total) * 100.0, + ); } + } } - Ok(result) + } } + Ok(result) + } - pub fn memory_info() -> std::io::Result> { - let content = fs::read_to_string("/proc/meminfo")?; - let mut result = HashMap::new(); + pub fn memory_info() -> std::io::Result> { + let content = fs::read_to_string("/proc/meminfo")?; + let mut result = HashMap::new(); - for line in content.lines() { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 2 { - let key = parts[0].trim_end_matches(':'); - if let Ok(value) = parts[1].parse::() { - result.insert(key.to_string(), value * 1024); - } - } + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let key = parts[0].trim_end_matches(':'); + if let Ok(value) = parts[1].parse::() { + result.insert(key.to_string(), value * 1024); } - Ok(result) + } } + Ok(result) + } - pub fn disk_stats() -> std::io::Result>> { - let content = fs::read_to_string("/proc/diskstats")?; - let mut result = HashMap::new(); + pub fn disk_stats() -> std::io::Result>> + { + let content = fs::read_to_string("/proc/diskstats")?; + let mut result = HashMap::new(); - for line in content.lines() { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 14 { - let device = parts[2].to_string(); - let mut stats = HashMap::new(); - stats.insert("reads_completed".to_string(), parts[3].parse().unwrap_or(0)); - stats.insert("reads_merged".to_string(), parts[4].parse().unwrap_or(0)); - stats.insert("sectors_read".to_string(), parts[5].parse().unwrap_or(0)); - stats.insert("reads_ms".to_string(), parts[6].parse().unwrap_or(0)); - stats.insert( - "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.insert("writes_ms".to_string(), parts[10].parse().unwrap_or(0)); - result.insert(device, stats); - } - } - Ok(result) + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 14 { + let device = parts[2].to_string(); + let mut stats = HashMap::new(); + stats + .insert("reads_completed".to_string(), parts[3].parse().unwrap_or(0)); + stats.insert("reads_merged".to_string(), parts[4].parse().unwrap_or(0)); + stats.insert("sectors_read".to_string(), parts[5].parse().unwrap_or(0)); + stats.insert("reads_ms".to_string(), parts[6].parse().unwrap_or(0)); + stats.insert( + "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.insert("writes_ms".to_string(), parts[10].parse().unwrap_or(0)); + result.insert(device, stats); + } } + Ok(result) + } - pub fn net_dev() -> std::io::Result>> { - let content = fs::read_to_string("/proc/net/dev")?; - let mut result = HashMap::new(); + pub fn net_dev() -> std::io::Result>> { + let content = fs::read_to_string("/proc/net/dev")?; + let mut result = HashMap::new(); - for line in content.lines().skip(2) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 17 { - let iface = parts[0].trim_end_matches(':'); - let mut stats = HashMap::new(); - 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_errors".to_string(), parts[3].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_packets".to_string(), parts[10].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)); - result.insert(iface.to_string(), stats); - } - } - Ok(result) + for line in content.lines().skip(2) { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 17 { + let iface = parts[0].trim_end_matches(':'); + let mut stats = HashMap::new(); + 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_errors".to_string(), parts[3].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_packets".to_string(), parts[10].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)); + result.insert(iface.to_string(), stats); + } } + Ok(result) + } } diff --git a/crates/pscand-core/src/helpers/sensor.rs b/crates/pscand-core/src/helpers/sensor.rs index e8032dc..08a88de 100644 --- a/crates/pscand-core/src/helpers/sensor.rs +++ b/crates/pscand-core/src/helpers/sensor.rs @@ -1,102 +1,117 @@ -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + fs, + path::{ + Path, + PathBuf, + }, +}; pub struct SensorHelper; impl SensorHelper { - pub fn discover_hwmon() -> std::io::Result> { - let hwmon_path = PathBuf::from("/sys/class/hwmon"); - let mut hwmons = Vec::new(); + pub fn discover_hwmon() -> std::io::Result> { + let hwmon_path = PathBuf::from("/sys/class/hwmon"); + let mut hwmons = Vec::new(); - if hwmon_path.exists() { - for entry in fs::read_dir(&hwmon_path)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - hwmons.push(path); - } - } + if hwmon_path.exists() { + for entry in fs::read_dir(&hwmon_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + hwmons.push(path); } - Ok(hwmons) + } + } + Ok(hwmons) + } + + pub fn read_hwmon_sensor( + hwmon_path: &Path, + sensor: &str, + ) -> std::io::Result> { + let sensor_path = hwmon_path.join(sensor); + if sensor_path.exists() { + let content = fs::read_to_string(sensor_path)?; + Ok(content.trim().parse::().ok()) + } else { + Ok(None) + } + } + + pub fn hwmon_info( + hwmon_path: &Path, + ) -> std::io::Result> { + 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> { - let sensor_path = hwmon_path.join(sensor); - if sensor_path.exists() { - let content = fs::read_to_string(sensor_path)?; - Ok(content.trim().parse::().ok()) - } else { - Ok(None) - } - } - - pub fn hwmon_info(hwmon_path: &Path) -> std::io::Result> { - let mut info = HashMap::new(); - - let name_path = hwmon_path.join("name"); - if name_path.exists() { + if let Ok(files) = fs::read_dir(hwmon_path) { + for file in files.flatten() { + let filename = file.file_name().to_string_lossy().to_string(); + if filename.starts_with("temp") && filename.ends_with("_input") { + let id = filename + .trim_start_matches("temp") + .trim_end_matches("_input"); + if let Some(t) = Self::read_hwmon_sensor(hwmon_path, &filename) + .ok() + .flatten() + { info.insert( - "name".to_string(), - fs::read_to_string(name_path)?.trim().to_string(), + format!("temp_{}_celsius", id), + format!("{}", t / 1000.0), ); + } } - - if let Ok(files) = fs::read_dir(hwmon_path) { - for file in files.flatten() { - let filename = file.file_name().to_string_lossy().to_string(); - if filename.starts_with("temp") && filename.ends_with("_input") { - let id = filename - .trim_start_matches("temp") - .trim_end_matches("_input"); - if let Some(t) = Self::read_hwmon_sensor(hwmon_path, &filename) - .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("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)); + } } - - Ok(info) + 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)); + } + } + } } - pub fn all_sensors() -> std::io::Result>> { - let mut all = HashMap::new(); + Ok(info) + } - 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); - } - } + pub fn all_sensors( + ) -> std::io::Result>> { + let mut all = HashMap::new(); - 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) + } } diff --git a/crates/pscand-core/src/helpers/system.rs b/crates/pscand-core/src/helpers/system.rs index c274111..12d7595 100644 --- a/crates/pscand-core/src/helpers/system.rs +++ b/crates/pscand-core/src/helpers/system.rs @@ -1,45 +1,50 @@ -use std::fs; -use std::time::Duration; +use std::{ + fs, + time::Duration, +}; pub struct SystemHelper; impl SystemHelper { - pub fn uptime() -> std::io::Result { - let uptime_secs = fs::read_to_string("/proc/uptime")? - .split_whitespace() - .next() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0.0); - Ok(Duration::from_secs_f64(uptime_secs)) - } + pub fn uptime() -> std::io::Result { + let uptime_secs = fs::read_to_string("/proc/uptime")? + .split_whitespace() + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + Ok(Duration::from_secs_f64(uptime_secs)) + } - pub fn boot_id() -> std::io::Result { - fs::read_to_string("/proc/sys/kernel/random/boot_id").map(|s| s.trim().to_string()) - } + pub fn boot_id() -> std::io::Result { + 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)> { - let content = fs::read_to_string("/proc/loadavg")?; - let mut parts = content.split_whitespace(); - let load1 = parts - .next() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0.0); - let load5 = parts - .next() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0.0); - let load15 = parts - .next() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0.0); - Ok((load1, load5, load15)) - } + pub fn load_average() -> std::io::Result<(f64, f64, f64)> { + let content = fs::read_to_string("/proc/loadavg")?; + let mut parts = content.split_whitespace(); + let load1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + let load5 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + let load15 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + Ok((load1, load5, load15)) + } - pub fn hostname() -> std::io::Result { - fs::read_to_string("/proc/sys/kernel/hostname").map(|s| s.trim().to_string()) - } + pub fn hostname() -> std::io::Result { + fs::read_to_string("/proc/sys/kernel/hostname") + .map(|s| s.trim().to_string()) + } - pub fn kernel_version() -> std::io::Result { - fs::read_to_string("/proc/sys/kernel/osrelease").map(|s| s.trim().to_string()) - } + pub fn kernel_version() -> std::io::Result { + fs::read_to_string("/proc/sys/kernel/osrelease") + .map(|s| s.trim().to_string()) + } } diff --git a/crates/pscand-core/src/lib.rs b/crates/pscand-core/src/lib.rs index efc05e6..86f1b40 100644 --- a/crates/pscand-core/src/lib.rs +++ b/crates/pscand-core/src/lib.rs @@ -4,8 +4,17 @@ pub mod logging; pub mod scanner; pub use config::Config; -pub use logging::{DaemonLogEntry, LogLevel, RingBufferLogger}; +pub use logging::{ + DaemonLogEntry, + LogLevel, + RingBufferLogger, +}; pub use scanner::{ - get_scanner, register_scanner, MetricValue, Scanner, ScannerCreatorFfi, ScannerError, + get_scanner, + register_scanner, + MetricValue, + Scanner, + ScannerCreatorFfi, + ScannerError, }; pub type Result = std::result::Result; diff --git a/crates/pscand-core/src/logging.rs b/crates/pscand-core/src/logging.rs index edfef0f..cdaa801 100644 --- a/crates/pscand-core/src/logging.rs +++ b/crates/pscand-core/src/logging.rs @@ -1,147 +1,178 @@ -use chrono::{DateTime, Utc}; +use std::{ + collections::HashMap, + fs::{ + self, + OpenOptions, + }, + io::Write, + path::{ + Path, + PathBuf, + }, + sync::{ + atomic::{ + AtomicU64, + Ordering, + }, + Arc, + }, + time::Instant, +}; + +use chrono::{ + DateTime, + Utc, +}; use parking_lot::Mutex; use ringbuf::{ - storage::Heap, - traits::*, - wrap::caching::{CachingCons, CachingProd}, - SharedRb, + storage::Heap, + traits::*, + wrap::caching::{ + CachingCons, + CachingProd, + }, + SharedRb, +}; +use serde::{ + Deserialize, + Serialize, }; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, OpenOptions}; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::Instant; use crate::scanner::MetricValue; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum LogLevel { - Debug, - Info, - Warn, - Error, - Critical, + Debug, + Info, + Warn, + Error, + Critical, } impl LogLevel { - pub fn parse(s: &str) -> Self { - match s.to_lowercase().as_str() { - "debug" => LogLevel::Debug, - "info" => LogLevel::Info, - "warn" | "warning" => LogLevel::Warn, - "error" | "err" => LogLevel::Error, - "critical" | "crit" | "fatal" => LogLevel::Critical, - _ => LogLevel::Info, - } + pub fn parse(s: &str) -> Self { + match s.to_lowercase().as_str() { + "debug" => LogLevel::Debug, + "info" => LogLevel::Info, + "warn" | "warning" => LogLevel::Warn, + "error" | "err" => LogLevel::Error, + "critical" | "crit" | "fatal" => LogLevel::Critical, + _ => LogLevel::Info, } + } - pub fn as_str(&self) -> &'static str { - match self { - LogLevel::Debug => "DEBUG", - LogLevel::Info => "INFO", - LogLevel::Warn => "WARN", - LogLevel::Error => "ERROR", - LogLevel::Critical => "CRITICAL", - } + pub fn as_str(&self) -> &'static str { + match self { + LogLevel::Debug => "DEBUG", + LogLevel::Info => "INFO", + LogLevel::Warn => "WARN", + LogLevel::Error => "ERROR", + LogLevel::Critical => "CRITICAL", } + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { - pub timestamp: DateTime, - pub scanner: String, - pub level: LogLevel, - pub message: Option, - pub metrics: HashMap, - pub collection_time_ms: Option, - pub error_count: Option, + pub timestamp: DateTime, + pub scanner: String, + pub level: LogLevel, + pub message: Option, + pub metrics: HashMap, + pub collection_time_ms: Option, + pub error_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DaemonLogEntry { - pub timestamp: DateTime, - pub level: LogLevel, - pub source: String, - pub event: String, - pub message: String, + pub timestamp: DateTime, + pub level: LogLevel, + pub source: String, + pub event: String, + pub message: String, } impl DaemonLogEntry { - pub fn new(source: impl Into, event: impl Into, message: String) -> Self { - Self { - timestamp: Utc::now(), - level: LogLevel::Info, - source: source.into(), - event: event.into(), - message, - } + pub fn new( + source: impl Into, + event: impl Into, + message: String, + ) -> Self { + Self { + timestamp: Utc::now(), + level: LogLevel::Info, + source: source.into(), + event: event.into(), + message, } + } - pub fn with_level(mut self, level: LogLevel) -> Self { - self.level = level; - self - } + pub fn with_level(mut self, level: LogLevel) -> Self { + self.level = level; + self + } - pub fn to_json(&self) -> String { - serde_json::to_string(self).unwrap_or_default() - } + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } } impl LogEntry { - pub fn new(scanner: impl Into, metrics: HashMap) -> Self { - Self { - timestamp: Utc::now(), - scanner: scanner.into(), - level: LogLevel::Info, - message: None, - metrics, - collection_time_ms: None, - error_count: None, - } + pub fn new( + scanner: impl Into, + metrics: HashMap, + ) -> Self { + Self { + timestamp: Utc::now(), + scanner: scanner.into(), + level: LogLevel::Info, + message: None, + metrics, + collection_time_ms: None, + error_count: None, } + } - pub fn with_level(mut self, level: LogLevel) -> Self { - self.level = level; - self - } + pub fn with_level(mut self, level: LogLevel) -> Self { + self.level = level; + self + } - pub fn with_message(mut self, message: impl Into) -> Self { - self.message = Some(message.into()); - self - } + pub fn with_message(mut self, message: impl Into) -> Self { + self.message = Some(message.into()); + self + } - pub fn with_timing(mut self, duration: Instant) -> Self { - self.collection_time_ms = Some(duration.elapsed().as_millis() as u64); - self - } + pub fn with_timing(mut self, duration: Instant) -> Self { + self.collection_time_ms = Some(duration.elapsed().as_millis() as u64); + self + } - pub fn with_error_count(mut self, count: u64) -> Self { - self.error_count = Some(count); - self - } + pub fn with_error_count(mut self, count: u64) -> Self { + self.error_count = Some(count); + self + } - pub fn to_json(&self) -> String { - serde_json::to_string(self).unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e)) - } + pub fn to_json(&self) -> String { + serde_json::to_string(self) + .unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e)) + } - pub fn to_journal(&self) -> String { - let metrics_json = serde_json::to_string(&self.metrics).unwrap_or_default(); - let level_str = self.level.as_str(); - if let Some(ref msg) = self.message { - format!( - "PSCAND_SCANNER={} PSCAND_LEVEL={} PSCAND_MSG={} PSCAND_METRICS={}", - self.scanner, level_str, msg, metrics_json - ) - } else { - format!( - "PSCAND_SCANNER={} PSCAND_LEVEL={} PSCAND_METRICS={}", - self.scanner, level_str, metrics_json - ) - } + pub fn to_journal(&self) -> String { + let metrics_json = serde_json::to_string(&self.metrics).unwrap_or_default(); + let level_str = self.level.as_str(); + if let Some(ref msg) = self.message { + format!( + "PSCAND_SCANNER={} PSCAND_LEVEL={} PSCAND_MSG={} PSCAND_METRICS={}", + self.scanner, level_str, msg, metrics_json + ) + } else { + format!( + "PSCAND_SCANNER={} PSCAND_LEVEL={} PSCAND_METRICS={}", + self.scanner, level_str, metrics_json + ) } + } } type RbStorage = Heap; @@ -150,460 +181,481 @@ type SharedRbLog = SharedRb; type SharedRbLogDaemon = SharedRb; struct RingBufferHandles { - prod: CachingProd>, - cons: CachingCons>, + prod: CachingProd>, + cons: CachingCons>, } struct DaemonBufferHandles { - prod: CachingProd>, - cons: CachingCons>, + prod: CachingProd>, + cons: CachingCons>, } pub struct RingBufferLogger { - buffer: Arc>, - daemon_buffer: Arc>, - file_path: Option, - journal_enabled: bool, - file_enabled: bool, + buffer: Arc>, + daemon_buffer: Arc>, + file_path: Option, + journal_enabled: bool, + file_enabled: bool, } impl RingBufferLogger { - pub fn new( - capacity: usize, - file_path: Option, - journal_enabled: bool, - file_enabled: bool, - ) -> Self { - let rb = SharedRb::::new(capacity); - let (prod, cons) = rb.split(); - let handles = RingBufferHandles { prod, cons }; + pub fn new( + capacity: usize, + file_path: Option, + journal_enabled: bool, + file_enabled: bool, + ) -> Self { + let rb = SharedRb::::new(capacity); + let (prod, cons) = rb.split(); + let handles = RingBufferHandles { prod, cons }; - let daemon_rb = SharedRb::::new(capacity); - let (daemon_prod, daemon_cons) = daemon_rb.split(); - let daemon_handles = DaemonBufferHandles { - prod: daemon_prod, - cons: daemon_cons, - }; + let daemon_rb = SharedRb::::new(capacity); + let (daemon_prod, daemon_cons) = daemon_rb.split(); + let daemon_handles = DaemonBufferHandles { + prod: daemon_prod, + cons: daemon_cons, + }; - Self { - buffer: Arc::new(Mutex::new(handles)), - daemon_buffer: Arc::new(Mutex::new(daemon_handles)), - file_path, - journal_enabled, - file_enabled, - } + Self { + buffer: Arc::new(Mutex::new(handles)), + daemon_buffer: Arc::new(Mutex::new(daemon_handles)), + file_path, + journal_enabled, + file_enabled, + } + } + + pub fn push(&self, entry: LogEntry) { + { + let mut handles = self.buffer.lock(); + if handles.prod.is_full() { + let _ = handles.cons.try_pop(); + } + let _ = handles.prod.try_push(entry.clone()); } - pub fn push(&self, entry: LogEntry) { - { - let mut handles = self.buffer.lock(); - if handles.prod.is_full() { - let _ = handles.cons.try_pop(); - } - let _ = handles.prod.try_push(entry.clone()); - } + if self.journal_enabled { + self.write_to_journal(&entry); + } + if self.file_enabled { + self.write_to_file(&entry); + } + } - if self.journal_enabled { - self.write_to_journal(&entry); - } - if self.file_enabled { - self.write_to_file(&entry); - } + pub fn log( + &self, + level: LogLevel, + source: &str, + event: &str, + message: String, + ) { + let entry = DaemonLogEntry { + timestamp: Utc::now(), + level, + source: source.to_string(), + event: event.to_string(), + message, + }; + + { + let mut daemon_handles = self.daemon_buffer.lock(); + if daemon_handles.prod.is_full() { + let _ = daemon_handles.cons.try_pop(); + } + let _ = daemon_handles.prod.try_push(entry.clone()); } - pub fn log(&self, level: LogLevel, source: &str, event: &str, message: String) { - let entry = DaemonLogEntry { - timestamp: Utc::now(), - level, - source: source.to_string(), - event: event.to_string(), - message, - }; - - { - let mut daemon_handles = self.daemon_buffer.lock(); - if daemon_handles.prod.is_full() { - let _ = daemon_handles.cons.try_pop(); - } - let _ = daemon_handles.prod.try_push(entry.clone()); - } - - if self.journal_enabled { - self.write_daemon_to_journal(&entry); - } - if self.file_enabled { - self.write_daemon_to_file(&entry); - } + if self.journal_enabled { + self.write_daemon_to_journal(&entry); } - - fn write_to_journal(&self, entry: &LogEntry) { - let priority = match entry.level { - LogLevel::Debug => 7, - LogLevel::Info => 6, - LogLevel::Warn => 4, - LogLevel::Error => 3, - LogLevel::Critical => 2, - }; - - let msg = entry - .message - .clone() - .unwrap_or_else(|| "collection completed".to_string()); - let scanner = entry.scanner.clone(); - - // Write directly to systemd journal socket - std::thread::spawn(move || { - use std::os::unix::net::UnixDatagram; - - let journal_msg = format!( - "PRIORITY={}\nSYSLOG_IDENTIFIER=pscand\nPSCAND_SCANNER={}\nMESSAGE={}\n", - priority, scanner, msg - ); - - if let Ok(sock) = UnixDatagram::unbound() { - let _ = sock.send_to(journal_msg.as_bytes(), "/run/systemd/journal/socket"); - } - }); + if self.file_enabled { + self.write_daemon_to_file(&entry); } + } - fn write_to_file(&self, entry: &LogEntry) { - if let Some(ref path) = self.file_path { - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { - let _ = writeln!(file, "{}", entry.to_json()); - } - } + fn write_to_journal(&self, entry: &LogEntry) { + let priority = match entry.level { + LogLevel::Debug => 7, + LogLevel::Info => 6, + LogLevel::Warn => 4, + LogLevel::Error => 3, + LogLevel::Critical => 2, + }; + + let msg = entry + .message + .clone() + .unwrap_or_else(|| "collection completed".to_string()); + let scanner = entry.scanner.clone(); + + // Write directly to systemd journal socket + std::thread::spawn(move || { + use std::os::unix::net::UnixDatagram; + + let journal_msg = format!( + "PRIORITY={}\nSYSLOG_IDENTIFIER=pscand\nPSCAND_SCANNER={}\nMESSAGE={}\\ + n", + priority, scanner, msg + ); + + if let Ok(sock) = UnixDatagram::unbound() { + let _ = + sock.send_to(journal_msg.as_bytes(), "/run/systemd/journal/socket"); + } + }); + } + + fn write_to_file(&self, entry: &LogEntry) { + if let Some(ref path) = self.file_path { + if let Ok(mut file) = + OpenOptions::new().create(true).append(true).open(path) + { + let _ = writeln!(file, "{}", entry.to_json()); + } } + } - fn write_daemon_to_journal(&self, entry: &DaemonLogEntry) { - let priority = match entry.level { - LogLevel::Debug => 7, - LogLevel::Info => 6, - LogLevel::Warn => 4, - LogLevel::Error => 3, - LogLevel::Critical => 2, - }; + fn write_daemon_to_journal(&self, entry: &DaemonLogEntry) { + let priority = match entry.level { + LogLevel::Debug => 7, + LogLevel::Info => 6, + LogLevel::Warn => 4, + LogLevel::Error => 3, + LogLevel::Critical => 2, + }; - let source = entry.source.clone(); - let event = entry.event.clone(); - let msg = entry.message.clone(); + let source = entry.source.clone(); + let event = entry.event.clone(); + let msg = entry.message.clone(); - // Write directly to systemd journal socket - std::thread::spawn(move || { - use std::os::unix::net::UnixDatagram; + // Write directly to systemd journal socket + std::thread::spawn(move || { + use std::os::unix::net::UnixDatagram; - let journal_msg = format!( - "PRIORITY={}\nSYSLOG_IDENTIFIER=pscand\nPSCAND_SOURCE={}\nPSCAND_EVENT={}\nMESSAGE={}\n", - priority, source, event, msg - ); + let journal_msg = format!( + "PRIORITY={}\nSYSLOG_IDENTIFIER=pscand\nPSCAND_SOURCE={}\\ + nPSCAND_EVENT={}\nMESSAGE={}\n", + priority, source, event, msg + ); - if let Ok(sock) = UnixDatagram::unbound() { - let _ = sock.send_to(journal_msg.as_bytes(), "/run/systemd/journal/socket"); - } - }); + if let Ok(sock) = UnixDatagram::unbound() { + let _ = + sock.send_to(journal_msg.as_bytes(), "/run/systemd/journal/socket"); + } + }); + } + + fn write_daemon_to_file(&self, entry: &DaemonLogEntry) { + if let Some(ref path) = self.file_path { + if let Ok(mut file) = + OpenOptions::new().create(true).append(true).open(path) + { + let _ = writeln!(file, "{}", entry.to_json()); + } } + } - fn write_daemon_to_file(&self, entry: &DaemonLogEntry) { - if let Some(ref path) = self.file_path { - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { - let _ = writeln!(file, "{}", entry.to_json()); - } - } - } + pub fn get_daemon_recent(&self, count: usize) -> Vec { + let handles = self.daemon_buffer.lock(); + handles.cons.iter().rev().take(count).cloned().collect() + } - pub fn get_daemon_recent(&self, count: usize) -> Vec { - let handles = self.daemon_buffer.lock(); - handles.cons.iter().rev().take(count).cloned().collect() - } + pub fn get_recent(&self, count: usize) -> Vec { + let handles = self.buffer.lock(); + handles.cons.iter().rev().take(count).cloned().collect() + } - pub fn get_recent(&self, count: usize) -> Vec { - let handles = self.buffer.lock(); - handles.cons.iter().rev().take(count).cloned().collect() - } - - pub fn flush_to_file(&self, path: &Path) -> std::io::Result<()> { - let entries = self.get_recent(usize::MAX); - let mut file = fs::File::create(path)?; - for entry in entries { - writeln!(file, "{}", entry.to_json())?; - } - Ok(()) + pub fn flush_to_file(&self, path: &Path) -> std::io::Result<()> { + let entries = self.get_recent(usize::MAX); + let mut file = fs::File::create(path)?; + for entry in entries { + writeln!(file, "{}", entry.to_json())?; } + Ok(()) + } } impl Default for RingBufferLogger { - fn default() -> Self { - Self::new(60, None, true, false) - } + fn default() -> Self { + Self::new(60, None, true, false) + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuntimeStats { - pub uptime_secs: u64, - pub total_collections: u64, - pub total_errors: u64, - pub last_collection_time_ms: u64, - pub avg_collection_time_ms: f64, - pub scanner_stats: HashMap, + pub uptime_secs: u64, + pub total_collections: u64, + pub total_errors: u64, + pub last_collection_time_ms: u64, + pub avg_collection_time_ms: f64, + pub scanner_stats: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScannerStats { - pub collections: u64, - pub errors: u64, - pub last_error: Option, - pub last_collection_time_ms: u64, - pub avg_collection_time_ms: f64, + pub collections: u64, + pub errors: u64, + pub last_error: Option, + pub last_collection_time_ms: u64, + pub avg_collection_time_ms: f64, } pub struct RuntimeMonitor { - start_time: Instant, - total_collections: AtomicU64, - total_errors: AtomicU64, - last_collection_time: AtomicU64, - collection_time_sum: AtomicU64, - scanner_collections: parking_lot::Mutex>, - scanner_errors: parking_lot::Mutex>, - scanner_last_error: parking_lot::Mutex>, - scanner_last_time: parking_lot::Mutex>, - scanner_time_sum: parking_lot::Mutex>, + start_time: Instant, + total_collections: AtomicU64, + total_errors: AtomicU64, + last_collection_time: AtomicU64, + collection_time_sum: AtomicU64, + scanner_collections: parking_lot::Mutex>, + scanner_errors: parking_lot::Mutex>, + scanner_last_error: parking_lot::Mutex>, + scanner_last_time: parking_lot::Mutex>, + scanner_time_sum: parking_lot::Mutex>, } impl RuntimeMonitor { - pub fn new() -> Self { - Self { - start_time: Instant::now(), - total_collections: AtomicU64::new(0), - total_errors: AtomicU64::new(0), - last_collection_time: AtomicU64::new(0), - collection_time_sum: AtomicU64::new(0), - scanner_collections: parking_lot::Mutex::new(HashMap::new()), - scanner_errors: parking_lot::Mutex::new(HashMap::new()), - scanner_last_error: parking_lot::Mutex::new(HashMap::new()), - scanner_last_time: parking_lot::Mutex::new(HashMap::new()), - scanner_time_sum: parking_lot::Mutex::new(HashMap::new()), - } + pub fn new() -> Self { + Self { + start_time: Instant::now(), + total_collections: AtomicU64::new(0), + total_errors: AtomicU64::new(0), + last_collection_time: AtomicU64::new(0), + collection_time_sum: AtomicU64::new(0), + scanner_collections: parking_lot::Mutex::new(HashMap::new()), + scanner_errors: parking_lot::Mutex::new(HashMap::new()), + scanner_last_error: parking_lot::Mutex::new(HashMap::new()), + scanner_last_time: parking_lot::Mutex::new(HashMap::new()), + scanner_time_sum: parking_lot::Mutex::new(HashMap::new()), + } + } + + pub fn record_collection( + &self, + scanner: &str, + time_ms: u64, + error: Option<&str>, + ) { + self.total_collections.fetch_add(1, Ordering::Relaxed); + self.last_collection_time.store(time_ms, Ordering::Relaxed); + self + .collection_time_sum + .fetch_add(time_ms, Ordering::Relaxed); + + let mut collections = self.scanner_collections.lock(); + let collections = collections + .entry(scanner.to_string()) + .or_insert_with(|| AtomicU64::new(0)); + collections.fetch_add(1, Ordering::Relaxed); + + let mut last_time = self.scanner_last_time.lock(); + let last_time = last_time + .entry(scanner.to_string()) + .or_insert_with(|| AtomicU64::new(0)); + last_time.store(time_ms, Ordering::Relaxed); + + let mut time_sum = self.scanner_time_sum.lock(); + let time_sum = time_sum + .entry(scanner.to_string()) + .or_insert_with(|| AtomicU64::new(0)); + time_sum.fetch_add(time_ms, Ordering::Relaxed); + + if let Some(err) = error { + self.total_errors.fetch_add(1, Ordering::Relaxed); + + let mut errors = self.scanner_errors.lock(); + let errors = errors + .entry(scanner.to_string()) + .or_insert_with(|| AtomicU64::new(0)); + errors.fetch_add(1, Ordering::Relaxed); + + let mut last_error = self.scanner_last_error.lock(); + last_error.insert(scanner.to_string(), err.to_string()); + } + } + + pub fn get_stats(&self) -> RuntimeStats { + let uptime = self.start_time.elapsed().as_secs(); + let total = self.total_collections.load(Ordering::Relaxed); + let errors = self.total_errors.load(Ordering::Relaxed); + let last_time = self.last_collection_time.load(Ordering::Relaxed); + let sum_time = self.collection_time_sum.load(Ordering::Relaxed); + let avg = if total > 0 { + sum_time as f64 / total as f64 + } else { + 0.0 + }; + + let mut scanner_stats = HashMap::new(); + + let collections = self.scanner_collections.lock(); + let errors_map = self.scanner_errors.lock(); + let last_error_map = self.scanner_last_error.lock(); + let last_time_map = self.scanner_last_time.lock(); + let time_sum_map = self.scanner_time_sum.lock(); + + for (name, coll) in collections.iter() { + let coll_count = coll.load(Ordering::Relaxed); + let err_count = errors_map + .get(name) + .map(|e| e.load(Ordering::Relaxed)) + .unwrap_or(0); + let last_err = last_error_map.get(name).cloned(); + let last_t = last_time_map + .get(name) + .map(|t| t.load(Ordering::Relaxed)) + .unwrap_or(0); + let sum_t = time_sum_map + .get(name) + .map(|s| s.load(Ordering::Relaxed)) + .unwrap_or(0); + let avg_t = if coll_count > 0 { + sum_t as f64 / coll_count as f64 + } else { + 0.0 + }; + + scanner_stats.insert(name.clone(), ScannerStats { + collections: coll_count, + errors: err_count, + last_error: last_err, + last_collection_time_ms: last_t, + avg_collection_time_ms: avg_t, + }); } - pub fn record_collection(&self, scanner: &str, time_ms: u64, error: Option<&str>) { - self.total_collections.fetch_add(1, Ordering::Relaxed); - self.last_collection_time.store(time_ms, Ordering::Relaxed); - self.collection_time_sum - .fetch_add(time_ms, Ordering::Relaxed); - - let mut collections = self.scanner_collections.lock(); - let collections = collections - .entry(scanner.to_string()) - .or_insert_with(|| AtomicU64::new(0)); - collections.fetch_add(1, Ordering::Relaxed); - - let mut last_time = self.scanner_last_time.lock(); - let last_time = last_time - .entry(scanner.to_string()) - .or_insert_with(|| AtomicU64::new(0)); - last_time.store(time_ms, Ordering::Relaxed); - - let mut time_sum = self.scanner_time_sum.lock(); - let time_sum = time_sum - .entry(scanner.to_string()) - .or_insert_with(|| AtomicU64::new(0)); - time_sum.fetch_add(time_ms, Ordering::Relaxed); - - if let Some(err) = error { - self.total_errors.fetch_add(1, Ordering::Relaxed); - - let mut errors = self.scanner_errors.lock(); - let errors = errors - .entry(scanner.to_string()) - .or_insert_with(|| AtomicU64::new(0)); - errors.fetch_add(1, Ordering::Relaxed); - - let mut last_error = self.scanner_last_error.lock(); - last_error.insert(scanner.to_string(), err.to_string()); - } - } - - pub fn get_stats(&self) -> RuntimeStats { - let uptime = self.start_time.elapsed().as_secs(); - let total = self.total_collections.load(Ordering::Relaxed); - let errors = self.total_errors.load(Ordering::Relaxed); - let last_time = self.last_collection_time.load(Ordering::Relaxed); - let sum_time = self.collection_time_sum.load(Ordering::Relaxed); - let avg = if total > 0 { - sum_time as f64 / total as f64 - } else { - 0.0 - }; - - let mut scanner_stats = HashMap::new(); - - let collections = self.scanner_collections.lock(); - let errors_map = self.scanner_errors.lock(); - let last_error_map = self.scanner_last_error.lock(); - let last_time_map = self.scanner_last_time.lock(); - let time_sum_map = self.scanner_time_sum.lock(); - - for (name, coll) in collections.iter() { - let coll_count = coll.load(Ordering::Relaxed); - let err_count = errors_map - .get(name) - .map(|e| e.load(Ordering::Relaxed)) - .unwrap_or(0); - let last_err = last_error_map.get(name).cloned(); - let last_t = last_time_map - .get(name) - .map(|t| t.load(Ordering::Relaxed)) - .unwrap_or(0); - let sum_t = time_sum_map - .get(name) - .map(|s| s.load(Ordering::Relaxed)) - .unwrap_or(0); - let avg_t = if coll_count > 0 { - sum_t as f64 / coll_count as f64 - } else { - 0.0 - }; - - scanner_stats.insert( - name.clone(), - ScannerStats { - collections: coll_count, - errors: err_count, - last_error: last_err, - last_collection_time_ms: last_t, - avg_collection_time_ms: avg_t, - }, - ); - } - - RuntimeStats { - uptime_secs: uptime, - total_collections: total, - total_errors: errors, - last_collection_time_ms: last_time, - avg_collection_time_ms: avg, - scanner_stats, - } + RuntimeStats { + uptime_secs: uptime, + total_collections: total, + total_errors: errors, + last_collection_time_ms: last_time, + avg_collection_time_ms: avg, + scanner_stats, } + } } impl Default for RuntimeMonitor { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } pub struct Heartbeat { - path: PathBuf, - interval_secs: u64, - last_update: parking_lot::Mutex, + path: PathBuf, + interval_secs: u64, + last_update: parking_lot::Mutex, } impl Heartbeat { - pub fn new(path: PathBuf, interval_secs: u64) -> Self { - Self { - path, - interval_secs, - last_update: parking_lot::Mutex::new(Instant::now()), - } + pub fn new(path: PathBuf, interval_secs: u64) -> Self { + Self { + path, + interval_secs, + last_update: parking_lot::Mutex::new(Instant::now()), + } + } + + pub fn touch(&self) -> std::io::Result<()> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + if let Some(parent) = self.path.parent() { + fs::create_dir_all(parent)? } - pub fn touch(&self) -> std::io::Result<()> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); + let mut file = fs::File::create(&self.path)?; + writeln!(file, "{}", now)?; + *self.last_update.lock() = Instant::now(); + Ok(()) + } - if let Some(parent) = self.path.parent() { - fs::create_dir_all(parent)? - } - - let mut file = fs::File::create(&self.path)?; - writeln!(file, "{}", now)?; - *self.last_update.lock() = Instant::now(); - Ok(()) - } - - pub fn is_stale(&self) -> bool { - self.last_update.lock().elapsed().as_secs() > self.interval_secs * 3 - } + pub fn is_stale(&self) -> bool { + self.last_update.lock().elapsed().as_secs() > self.interval_secs * 3 + } } pub struct CrashDetector { - heartbeat: Heartbeat, - state_file: PathBuf, + heartbeat: Heartbeat, + state_file: PathBuf, } impl CrashDetector { - pub fn new(state_dir: PathBuf) -> Self { - let heartbeat = Heartbeat::new(state_dir.join("heartbeat"), 5); - let state_file = state_dir.join("state"); - Self { - heartbeat, - state_file, - } + pub fn new(state_dir: PathBuf) -> Self { + let heartbeat = Heartbeat::new(state_dir.join("heartbeat"), 5); + let state_file = state_dir.join("state"); + Self { + heartbeat, + state_file, + } + } + + pub fn new_with_interval( + state_dir: PathBuf, + heartbeat_interval_secs: u64, + ) -> Self { + let heartbeat = + Heartbeat::new(state_dir.join("heartbeat"), heartbeat_interval_secs); + let state_file = state_dir.join("state"); + Self { + heartbeat, + state_file, + } + } + + pub fn write_state(&self, stats: &RuntimeStats) -> std::io::Result<()> { + if let Some(parent) = self.state_file.parent() { + fs::create_dir_all(parent)? + }; + let json = serde_json::to_string_pretty(stats).unwrap_or_default(); + fs::write(&self.state_file, json) + } + + pub fn mark_running(&self) -> std::io::Result<()> { + self.heartbeat.touch() + } + + pub fn mark_stopped(&self) -> std::io::Result<()> { + if let Some(parent) = self.state_file.parent() { + fs::create_dir_all(parent)?; + } + let json = serde_json::json!({ + "status": "stopped", + "timestamp": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + }); + fs::write( + &self.state_file, + serde_json::to_string_pretty(&json).unwrap_or_default(), + ) + } + + pub fn is_healthy(&self) -> bool { + if self.heartbeat.is_stale() { + return false; } - pub fn new_with_interval(state_dir: PathBuf, heartbeat_interval_secs: u64) -> Self { - let heartbeat = Heartbeat::new(state_dir.join("heartbeat"), heartbeat_interval_secs); - let state_file = state_dir.join("state"); - Self { - heartbeat, - state_file, - } + if let Some(content) = fs::read_to_string(&self.state_file) + .and_then(|c| { + serde_json::from_str::(&c) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + }) + .ok() + .and_then(|state| { + state + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + { + if content == "stopped" { + return false; + } } - pub fn write_state(&self, stats: &RuntimeStats) -> std::io::Result<()> { - if let Some(parent) = self.state_file.parent() { - fs::create_dir_all(parent)? - }; - let json = serde_json::to_string_pretty(stats).unwrap_or_default(); - fs::write(&self.state_file, json) - } - - pub fn mark_running(&self) -> std::io::Result<()> { - self.heartbeat.touch() - } - - pub fn mark_stopped(&self) -> std::io::Result<()> { - if let Some(parent) = self.state_file.parent() { - fs::create_dir_all(parent)?; - } - let json = serde_json::json!({ - "status": "stopped", - "timestamp": std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0) - }); - fs::write( - &self.state_file, - serde_json::to_string_pretty(&json).unwrap_or_default(), - ) - } - - pub fn is_healthy(&self) -> bool { - if self.heartbeat.is_stale() { - return false; - } - - if let Some(content) = fs::read_to_string(&self.state_file) - .and_then(|c| { - serde_json::from_str::(&c) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) - }) - .ok() - .and_then(|state| { - state - .get("status") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }) - { - if content == "stopped" { - return false; - } - } - - true - } + true + } } diff --git a/crates/pscand-core/src/scanner.rs b/crates/pscand-core/src/scanner.rs index 96f0000..8b55aa2 100644 --- a/crates/pscand-core/src/scanner.rs +++ b/crates/pscand-core/src/scanner.rs @@ -1,36 +1,48 @@ -use std::collections::HashMap; -use std::os::raw::c_void; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::LazyLock; -use std::sync::Mutex; -use std::time::Duration; +use std::{ + collections::HashMap, + os::raw::c_void, + sync::{ + atomic::{ + AtomicUsize, + Ordering, + }, + LazyLock, + Mutex, + }, + time::Duration, +}; -use serde::{Deserialize, Serialize}; +use serde::{ + Deserialize, + Serialize, +}; use thiserror::Error; pub type ScannerBox = Box; pub type ScannerResult = Result; pub type ScannerMetrics = HashMap; pub type ScannerCollectionResult = Result; -pub type ScannerCollectFn = Box ScannerCollectionResult + Send + Sync>; -pub type ScannerInitFnMut = Mutex Result<()> + Send>>; +pub type ScannerCollectFn = + Box ScannerCollectionResult + Send + Sync>; +pub type ScannerInitFnMut = + Mutex Result<()> + Send>>; pub type ScannerCleanupFnMut = Mutex Result<()> + Send>>; -pub type ScannerCreatorFfi = unsafe extern "C" fn() -> *mut c_void; -pub type ScannerInitFn = unsafe extern "C" fn() -> *mut c_void; -pub type ScannerDropFn = unsafe extern "C" fn(*mut c_void); +pub type ScannerCreatorFfi = unsafe extern fn() -> *mut c_void; +pub type ScannerInitFn = unsafe extern fn() -> *mut c_void; +pub type ScannerDropFn = unsafe extern fn(*mut c_void); #[derive(Error, Debug)] pub enum ScannerError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("Parse error: {0}")] - Parse(String), - #[error("Configuration error: {0}")] - Config(String), - #[error("Scanner not initialized")] - NotInitialized, - #[error("Scanner {0} not found")] - NotFound(String), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Parse error: {0}")] + Parse(String), + #[error("Configuration error: {0}")] + Config(String), + #[error("Scanner not initialized")] + NotInitialized, + #[error("Scanner {0} not found")] + NotFound(String), } pub type Result = std::result::Result; @@ -38,40 +50,40 @@ pub type Result = std::result::Result; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MetricValue { - Integer(i64), - Float(f64), - Boolean(bool), - String(String), + Integer(i64), + Float(f64), + Boolean(bool), + String(String), } impl MetricValue { - pub fn from_i64(v: i64) -> Self { - MetricValue::Integer(v) - } + pub fn from_i64(v: i64) -> Self { + MetricValue::Integer(v) + } - pub fn from_f64(v: f64) -> Self { - MetricValue::Float(v) - } + pub fn from_f64(v: f64) -> Self { + MetricValue::Float(v) + } - pub fn from_bool(v: bool) -> Self { - MetricValue::Boolean(v) - } + pub fn from_bool(v: bool) -> Self { + MetricValue::Boolean(v) + } - pub fn from_string(v: impl Into) -> Self { - MetricValue::String(v.into()) - } + pub fn from_string(v: impl Into) -> Self { + MetricValue::String(v.into()) + } } pub trait Scanner: Send + Sync { - fn name(&self) -> &'static str; - fn interval(&self) -> Duration; - fn init(&mut self, config: &toml::Value) -> Result<()>; - fn collect(&self) -> ScannerCollectionResult; - fn cleanup(&mut self) -> Result<()>; + fn name(&self) -> &'static str; + fn interval(&self) -> Duration; + fn init(&mut self, config: &toml::Value) -> Result<()>; + fn collect(&self) -> ScannerCollectionResult; + fn cleanup(&mut self) -> Result<()>; } static SCANNER_HANDLES: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); + LazyLock::new(|| Mutex::new(HashMap::new())); static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1); #[inline] @@ -79,9 +91,9 @@ static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1); /// # Safety /// The handle must only be used with `get_scanner` from the same process. pub unsafe fn register_scanner(scanner: ScannerBox) -> usize { - let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst); - SCANNER_HANDLES.lock().unwrap().insert(handle, scanner); - handle + let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst); + SCANNER_HANDLES.lock().unwrap().insert(handle, scanner); + handle } #[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, /// and the returned Box must be properly dropped. pub unsafe fn get_scanner(handle: *mut c_void) -> Option { - let handle = handle as usize; - if handle == 0 { - None - } else { - SCANNER_HANDLES.lock().unwrap().remove(&handle) - } + let handle = handle as usize; + if handle == 0 { + None + } else { + SCANNER_HANDLES.lock().unwrap().remove(&handle) + } } pub trait ScannerRegistry: Send + Sync { - fn list_scanners(&self) -> Vec<&'static str>; - fn get_scanner(&self, name: &str) -> Option; + fn list_scanners(&self) -> Vec<&'static str>; + fn get_scanner(&self, name: &str) -> Option; } pub struct DynamicScanner { - name: &'static str, - interval: Duration, - collect_fn: ScannerCollectFn, - init_fn: ScannerInitFnMut, - cleanup_fn: ScannerCleanupFnMut, + name: &'static str, + interval: Duration, + collect_fn: ScannerCollectFn, + init_fn: ScannerInitFnMut, + cleanup_fn: ScannerCleanupFnMut, } impl DynamicScanner { - pub fn new( - name: &'static str, - interval: Duration, - collect_fn: impl Fn() -> ScannerCollectionResult + Send + Sync + 'static, - init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static, - cleanup_fn: impl FnMut() -> Result<()> + Send + 'static, - ) -> Self { - Self { - name, - interval, - collect_fn: Box::new(collect_fn), - init_fn: Mutex::new(Box::new(init_fn)), - cleanup_fn: Mutex::new(Box::new(cleanup_fn)), - } + pub fn new( + name: &'static str, + interval: Duration, + collect_fn: impl Fn() -> ScannerCollectionResult + Send + Sync + 'static, + init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static, + cleanup_fn: impl FnMut() -> Result<()> + Send + 'static, + ) -> Self { + Self { + name, + interval, + collect_fn: Box::new(collect_fn), + init_fn: Mutex::new(Box::new(init_fn)), + cleanup_fn: Mutex::new(Box::new(cleanup_fn)), } + } } impl Scanner for DynamicScanner { - fn name(&self) -> &'static str { - self.name - } + fn name(&self) -> &'static str { + self.name + } - fn interval(&self) -> Duration { - self.interval - } + fn interval(&self) -> Duration { + self.interval + } - fn init(&mut self, config: &toml::Value) -> Result<()> { - let mut init_fn = self.init_fn.lock().unwrap(); - (init_fn)(config) - } + fn init(&mut self, config: &toml::Value) -> Result<()> { + let mut init_fn = self.init_fn.lock().unwrap(); + (init_fn)(config) + } - fn collect(&self) -> ScannerCollectionResult { - (self.collect_fn)() - } + fn collect(&self) -> ScannerCollectionResult { + (self.collect_fn)() + } - fn cleanup(&mut self) -> Result<()> { - let mut cleanup_fn = self.cleanup_fn.lock().unwrap(); - (cleanup_fn)() - } + fn cleanup(&mut self) -> Result<()> { + let mut cleanup_fn = self.cleanup_fn.lock().unwrap(); + (cleanup_fn)() + } } diff --git a/crates/pscand-macros/Cargo.toml b/crates/pscand-macros/Cargo.toml index cfcb2a5..eb2d5ea 100644 --- a/crates/pscand-macros/Cargo.toml +++ b/crates/pscand-macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pscand-macros" +name = "pscand-macros" version.workspace = true edition.workspace = true license.workspace = true @@ -10,5 +10,5 @@ proc-macro = true [dependencies] proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full"] } +quote = "1" +syn = { features = [ "full" ], version = "2" } diff --git a/crates/pscand-macros/src/lib.rs b/crates/pscand-macros/src/lib.rs index 781c792..cb60626 100644 --- a/crates/pscand-macros/src/lib.rs +++ b/crates/pscand-macros/src/lib.rs @@ -1,64 +1,67 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use syn::{ + parse_macro_input, + ItemFn, +}; #[proc_macro_attribute] pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream { - let name_str = parse_macro_input!(name as syn::LitStr).value(); - let input = parse_macro_input!(input as ItemFn); + let name_str = parse_macro_input!(name as syn::LitStr).value(); + let input = parse_macro_input!(input as ItemFn); - let _fn_name = input.sig.ident.clone(); - let body = &input.block; + let _fn_name = input.sig.ident.clone(); + let body = &input.block; - let result = quote! { - #[no_mangle] - pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - struct ScannerImpl; + let result = quote! { + #[no_mangle] + pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { + struct ScannerImpl; - impl pscand_core::Scanner for ScannerImpl { - fn name(&self) -> &'static str { - #name_str - } + impl pscand_core::Scanner for ScannerImpl { + fn name(&self) -> &'static str { + #name_str + } - fn interval(&self) -> std::time::Duration { - std::time::Duration::from_secs(1) - } + fn interval(&self) -> std::time::Duration { + std::time::Duration::from_secs(1) + } - fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { - Ok(()) - } + fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { + Ok(()) + } - fn collect(&self) -> pscand_core::Result> { - #body - } + fn collect(&self) -> pscand_core::Result> { + #body + } - fn cleanup(&mut self) -> pscand_core::Result<()> { - Ok(()) - } - } + fn cleanup(&mut self) -> pscand_core::Result<()> { + Ok(()) + } + } - let handle = unsafe { pscand_core::register_scanner(Box::new(ScannerImpl)) }; - handle as *mut std::os::raw::c_void - } - }; + let handle = unsafe { pscand_core::register_scanner(Box::new(ScannerImpl)) }; + handle as *mut std::os::raw::c_void + } + }; - result.into() + result.into() } #[proc_macro] pub fn register_scanner(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemFn); - let fn_name = input.sig.ident.clone(); + let input = parse_macro_input!(input as ItemFn); + let fn_name = input.sig.ident.clone(); - let result = quote! { - #input + let result = quote! { + #input - #[no_mangle] - pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - let handle = unsafe { pscand_core::register_scanner(Box::new(#fn_name())) }; - handle as *mut std::os::raw::c_void - } - }; + #[no_mangle] + pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { + let handle = unsafe { pscand_core::register_scanner(Box::new(#fn_name())) }; + handle as *mut std::os::raw::c_void + } + }; - result.into() + result.into() } diff --git a/scanners/scanner-power/Cargo.toml b/scanners/scanner-power/Cargo.toml index 111a6d7..1ffeff7 100644 --- a/scanners/scanner-power/Cargo.toml +++ b/scanners/scanner-power/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "scanner-power" +name = "scanner-power" version.workspace = true edition.workspace = true license.workspace = true @@ -7,9 +7,9 @@ authors.workspace = true [dependencies] pscand-core.workspace = true -toml.workspace = true +toml.workspace = true [lib] -name = "scanner_power" -path = "src/lib.rs" -crate-type = ["cdylib"] +crate-type = [ "cdylib" ] +name = "scanner_power" +path = "src/lib.rs" diff --git a/scanners/scanner-power/src/lib.rs b/scanners/scanner-power/src/lib.rs index e45ac1d..ceb729c 100644 --- a/scanners/scanner-power/src/lib.rs +++ b/scanners/scanner-power/src/lib.rs @@ -1,96 +1,104 @@ -use pscand_core::helpers::PowerHelper; -use pscand_core::scanner::{MetricValue, Scanner}; -use std::collections::HashMap; -use std::time::Duration; +use std::{ + collections::HashMap, + time::Duration, +}; + +use pscand_core::{ + helpers::PowerHelper, + scanner::{ + MetricValue, + Scanner, + }, +}; struct PowerScanner; #[no_mangle] -pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - Box::into_raw(Box::new(PowerScanner)) as *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 } impl Default for PowerScanner { - fn default() -> Self { - Self - } + fn default() -> Self { + Self + } } impl Scanner for PowerScanner { - fn name(&self) -> &'static str { - "power" + fn name(&self) -> &'static str { + "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> { + 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 { - Duration::from_secs(2) - } - - fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { - Ok(()) - } - - fn collect(&self) -> pscand_core::Result> { - 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 Ok(supplies) = PowerHelper::power_supplies() { + for (name, info) in supplies { + if let Some(status) = info.get("status") { + metrics.insert( + format!("supply_{}_status", name), + MetricValue::from_string(status), + ); } - - if let Ok(supplies) = PowerHelper::power_supplies() { - for (name, info) in supplies { - if let Some(status) = info.get("status") { - 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::() { - metrics.insert( - format!("supply_{}_capacity", name), - MetricValue::Integer(cap as i64), - ); - } - } - } + if let Some(online) = info.get("online") { + metrics.insert( + format!("supply_{}_online", name), + MetricValue::from_bool(online == "1"), + ); } - - if let Ok(state) = PowerHelper::suspend_state() { + if let Some(capacity) = info.get("capacity") { + if let Ok(cap) = capacity.parse::() { metrics.insert( - "suspend_state".to_string(), - MetricValue::from_string(&state), + format!("supply_{}_capacity", name), + MetricValue::Integer(cap as i64), ); + } } - - Ok(metrics) + } } - fn cleanup(&mut self) -> pscand_core::Result<()> { - Ok(()) + if let Ok(state) = PowerHelper::suspend_state() { + metrics.insert( + "suspend_state".to_string(), + MetricValue::from_string(&state), + ); } + + Ok(metrics) + } + + fn cleanup(&mut self) -> pscand_core::Result<()> { + Ok(()) + } } diff --git a/scanners/scanner-proc/Cargo.toml b/scanners/scanner-proc/Cargo.toml index 37f6f71..a16a2c0 100644 --- a/scanners/scanner-proc/Cargo.toml +++ b/scanners/scanner-proc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "scanner-proc" +name = "scanner-proc" version.workspace = true edition.workspace = true license.workspace = true @@ -7,9 +7,9 @@ authors.workspace = true [dependencies] pscand-core.workspace = true -toml.workspace = true +toml.workspace = true [lib] -name = "scanner_proc" -path = "src/lib.rs" -crate-type = ["cdylib"] +crate-type = [ "cdylib" ] +name = "scanner_proc" +path = "src/lib.rs" diff --git a/scanners/scanner-proc/src/lib.rs b/scanners/scanner-proc/src/lib.rs index 74036c8..0d8f172 100644 --- a/scanners/scanner-proc/src/lib.rs +++ b/scanners/scanner-proc/src/lib.rs @@ -1,99 +1,107 @@ -use pscand_core::helpers::ProcessHelper; -use pscand_core::scanner::{MetricValue, Scanner}; -use std::collections::HashMap; -use std::time::Duration; +use std::{ + collections::HashMap, + time::Duration, +}; + +use pscand_core::{ + helpers::ProcessHelper, + scanner::{ + MetricValue, + Scanner, + }, +}; struct ProcScanner; #[no_mangle] -pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - Box::into_raw(Box::new(ProcScanner)) as *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 } impl Default for ProcScanner { - fn default() -> Self { - Self - } + fn default() -> Self { + Self + } } impl Scanner for ProcScanner { - fn name(&self) -> &'static str { - "proc" + fn name(&self) -> &'static str { + "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> { + 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 { - Duration::from_secs(5) - } + if let Ok(zombies) = ProcessHelper::zombie_processes() { + metrics.insert( + "zombie_count".to_string(), + MetricValue::Integer(zombies.len() as i64), + ); - fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { - Ok(()) - } - - fn collect(&self) -> pscand_core::Result> { - 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), - ); - } + 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)); } - - if let Ok(zombies) = ProcessHelper::zombie_processes() { - metrics.insert( - "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) + metrics.insert( + "zombie_processes".to_string(), + MetricValue::from_string(zombie_info.join(",")), + ); + } } - fn cleanup(&mut self) -> pscand_core::Result<()> { - Ok(()) + 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<()> { + Ok(()) + } } diff --git a/scanners/scanner-sensor/Cargo.toml b/scanners/scanner-sensor/Cargo.toml index 419a293..617ba84 100644 --- a/scanners/scanner-sensor/Cargo.toml +++ b/scanners/scanner-sensor/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "scanner-sensor" +name = "scanner-sensor" version.workspace = true edition.workspace = true license.workspace = true @@ -7,9 +7,9 @@ authors.workspace = true [dependencies] pscand-core.workspace = true -toml.workspace = true +toml.workspace = true [lib] -name = "scanner_sensor" -path = "src/lib.rs" -crate-type = ["cdylib"] +crate-type = [ "cdylib" ] +name = "scanner_sensor" +path = "src/lib.rs" diff --git a/scanners/scanner-sensor/src/lib.rs b/scanners/scanner-sensor/src/lib.rs index a2ea662..2ca786e 100644 --- a/scanners/scanner-sensor/src/lib.rs +++ b/scanners/scanner-sensor/src/lib.rs @@ -1,79 +1,92 @@ -use pscand_core::helpers::SensorHelper; -use pscand_core::scanner::{MetricValue, Scanner}; -use pscand_core::Result; -use std::collections::HashMap; -use std::time::Duration; +use std::{ + collections::HashMap, + time::Duration, +}; + +use pscand_core::{ + helpers::SensorHelper, + scanner::{ + MetricValue, + Scanner, + }, + Result, +}; struct SensorScanner; #[no_mangle] -pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - Box::into_raw(Box::new(SensorScanner)) as *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 } impl Default for SensorScanner { - fn default() -> Self { - Self - } + fn default() -> Self { + Self + } } impl Scanner for SensorScanner { - fn name(&self) -> &'static str { - "sensor" - } + fn name(&self) -> &'static str { + "sensor" + } - fn interval(&self) -> Duration { - Duration::from_secs(2) - } + fn interval(&self) -> Duration { + Duration::from_secs(2) + } - fn init(&mut self, _config: &toml::Value) -> Result<()> { - Ok(()) - } + fn init(&mut self, _config: &toml::Value) -> Result<()> { + Ok(()) + } - fn collect(&self) -> Result> { - let mut metrics = HashMap::new(); + fn collect(&self) -> Result> { + let mut metrics = HashMap::new(); - if let Ok(sensors) = SensorHelper::all_sensors() { - let mut temp_count = 0; - let mut fan_count = 0; + if let Ok(sensors) = SensorHelper::all_sensors() { + let mut temp_count = 0; + let mut fan_count = 0; - for (hwmon, values) in sensors { - for (key, value) in values { - if key.starts_with("temp_") && key.ends_with("_celsius") { - if let Ok(v) = value.parse::() { - metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); - temp_count += 1; - if temp_count <= 3 { - metrics.insert( - format!("temp_{}", temp_count), - MetricValue::from_f64(v), - ); - } - } - } - if key.starts_with("fan_") && key.ends_with("_rpm") { - if let Ok(v) = value.parse::() { - 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::() { - metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); - } - } - } + for (hwmon, values) in sensors { + for (key, value) in values { + if key.starts_with("temp_") && key.ends_with("_celsius") { + if let Ok(v) = value.parse::() { + metrics + .insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); + temp_count += 1; + if temp_count <= 3 { + metrics.insert( + format!("temp_{}", temp_count), + MetricValue::from_f64(v), + ); + } } + } + if key.starts_with("fan_") && key.ends_with("_rpm") { + if let Ok(v) = value.parse::() { + 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::() { + metrics + .insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); + } + } } - - Ok(metrics) + } } - fn cleanup(&mut self) -> Result<()> { - Ok(()) - } + Ok(metrics) + } + + fn cleanup(&mut self) -> Result<()> { + Ok(()) + } } diff --git a/scanners/scanner-system/Cargo.toml b/scanners/scanner-system/Cargo.toml index 548aa5a..fa9162e 100644 --- a/scanners/scanner-system/Cargo.toml +++ b/scanners/scanner-system/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "scanner-system" +name = "scanner-system" version.workspace = true edition.workspace = true license.workspace = true @@ -7,9 +7,9 @@ authors.workspace = true [dependencies] pscand-core.workspace = true -toml.workspace = true +toml.workspace = true [lib] -name = "scanner_system" -path = "src/lib.rs" -crate-type = ["cdylib"] +crate-type = [ "cdylib" ] +name = "scanner_system" +path = "src/lib.rs" diff --git a/scanners/scanner-system/src/lib.rs b/scanners/scanner-system/src/lib.rs index 2ff476c..055d05c 100644 --- a/scanners/scanner-system/src/lib.rs +++ b/scanners/scanner-system/src/lib.rs @@ -1,80 +1,94 @@ -use pscand_core::helpers::{ResourceHelper, SystemHelper}; -use pscand_core::scanner::{MetricValue, Scanner}; -use pscand_core::Result; -use std::collections::HashMap; -use std::time::Duration; +use std::{ + collections::HashMap, + time::Duration, +}; + +use pscand_core::{ + helpers::{ + ResourceHelper, + SystemHelper, + }, + scanner::{ + MetricValue, + Scanner, + }, + Result, +}; #[derive(Default)] struct SystemScanner { - _prev_cpu: Option>, + _prev_cpu: Option>, } #[no_mangle] -pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void { - Box::into_raw(Box::new(SystemScanner::default())) as *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 } impl Scanner for SystemScanner { - fn name(&self) -> &'static str { - "system" + fn name(&self) -> &'static str { + "system" + } + + fn interval(&self) -> Duration { + Duration::from_secs(1) + } + + fn init(&mut self, _config: &toml::Value) -> Result<()> { + Ok(()) + } + + fn collect(&self) -> Result> { + 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 { - Duration::from_secs(1) + if let Ok((load1, load5, load15)) = SystemHelper::load_average() { + 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<()> { - Ok(()) + if let Ok(cpu) = ResourceHelper::cpu_usage() { + if let Some(total) = cpu.get("total_usage_percent") { + metrics + .insert("cpu_percent".to_string(), MetricValue::from_f64(*total)); + } } - fn collect(&self) -> Result> { - let mut metrics = HashMap::new(); - - if let Ok(uptime) = SystemHelper::uptime() { - metrics.insert( - "uptime_secs".to_string(), - MetricValue::from_f64(uptime.as_secs_f64()), - ); - } - - if let Ok((load1, load5, load15)) = SystemHelper::load_average() { - 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 Ok(cpu) = ResourceHelper::cpu_usage() { - if let Some(total) = cpu.get("total_usage_percent") { - metrics.insert("cpu_percent".to_string(), MetricValue::from_f64(*total)); - } - } - - 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) + 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), + ); + } } - fn cleanup(&mut self) -> Result<()> { - Ok(()) - } + Ok(metrics) + } + + fn cleanup(&mut self) -> Result<()> { + Ok(()) + } }