pscand-cli: cleanup

Fixes path expansion and scanner lock release. Among other things.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia8695314852aaa4914f59da57351d1086a6a6964
This commit is contained in:
raf 2026-02-19 01:11:39 +03:00
commit 9bec96db1b
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 660 additions and 483 deletions

72
Cargo.lock generated
View file

@ -82,6 +82,15 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.0"
@ -176,12 +185,41 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dirs"
version = "6.0.0"
@ -248,6 +286,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@ -533,6 +581,7 @@ dependencies = [
"ringbuf",
"serde",
"serde_json",
"sha2",
"sysinfo",
"thiserror",
"tokio",
@ -730,6 +779,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -880,6 +940,12 @@ version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@ -892,6 +958,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"

View file

@ -32,6 +32,7 @@ parking_lot = "0.12.5"
ringbuf = "0.4.8"
dirs = "6.0.0"
clap = { version = "4.5.59", features = ["derive"] }
sha2 = "0.10.9"
[profile.release]
lto = true

View file

@ -26,3 +26,4 @@ ringbuf.workspace = true
dirs.workspace = true
sysinfo.workspace = true
clap.workspace = true
sha2.workspace = true

View file

@ -2,18 +2,31 @@
use clap::Parser;
use libloading::Library;
use pscand_core::Config as CoreConfig;
use pscand_core::logging::{LogLevel, RingBufferLogger};
use pscand_core::scanner::Scanner;
use pscand_core::Config as CoreConfig;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
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;
type ScannerCreator = pscand_core::ScannerCreatorFfi;
fn expand_path(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
let path_str = path.to_str().ok_or("Invalid path encoding")?;
if path_str.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
return Ok(home.join(&path_str[2..]));
}
}
Ok(path.to_path_buf())
}
#[derive(Parser, Debug)]
#[command(
name = "pscand",
@ -34,6 +47,37 @@ struct RunArgs {
debug: bool,
}
fn verify_library(path: &Path) -> Result<(), String> {
let mut file = fs::File::open(path).map_err(|e| {
format!(
"failed to open library {} for verification: {}",
path.display(),
e
)
})?;
let mut buffer = [0u8; 4096];
let bytes_read = file.read(&mut buffer).map_err(|e| {
format!(
"failed to read library {} for hash calculation: {}",
path.display(),
e
)
})?;
if bytes_read < 4 {
return Err(format!(
"library {} is too small to be valid",
path.display()
));
}
let mut hasher = Sha256::new();
hasher.update(&buffer[..bytes_read]);
let _hash = hasher.finalize();
Ok(())
}
struct LoadedScanner {
name: String,
scanner: Arc<RwLock<Box<dyn Scanner>>>,
@ -115,9 +159,14 @@ impl DaemonState {
Ok(())
}
async fn write_status(&self, logger: &RingBufferLogger) -> std::io::Result<()> {
async fn write_status(
&self,
logger: &RingBufferLogger,
) -> std::io::Result<()> {
let uptime = SystemTime::now()
.duration_since(self.start_time.try_read().map(|t| *t).unwrap_or(UNIX_EPOCH))
.duration_since(
self.start_time.try_read().map(|t| *t).unwrap_or(UNIX_EPOCH),
)
.map(|d| d.as_secs())
.unwrap_or(0);
let collections = self.collection_count.try_read().map(|c| *c).unwrap_or(0);
@ -158,10 +207,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match args {
Args::Run(run_args) => {
run_daemon(run_args).await?;
}
},
Args::List => {
list_scanners().await?;
}
},
}
Ok(())
@ -169,30 +218,44 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
if args.debug {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("debug"),
)
.init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info"),
)
.init();
}
log::info!("Starting pscand daemon");
let config = if args.config.exists() {
CoreConfig::load(&args.config)?
// Expand ~ in config path
let config_path = expand_path(&args.config)
.map_err(|e| format!("Failed to expand config path: {}", e))?;
let config = if config_path.exists() {
CoreConfig::load(&config_path)?
} else {
log::warn!("Config file not found at {:?}", args.config);
log::info!("Creating default config. Run with --config to specify a different path.");
log::warn!("Config file not found at {:?}", config_path);
log::info!(
"Creating default config. Run with --config to specify a different path."
);
// Create default config directory if it doesn't exist
if let Some(parent) = args.config.parent() {
if let Some(parent) = config_path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
log::warn!("Failed to create config directory: {}", e);
log::error!("Failed to create config directory {:?}: {}", parent, e);
}
}
CoreConfig::default()
};
std::fs::create_dir_all(&config.log_dir)?;
std::fs::create_dir_all(&config.log_dir).map_err(|e| {
format!("Failed to create log directory {:?}: {}", config.log_dir, e)
})?;
let log_file = config.log_dir.join("pscand.log");
let logger = Arc::new(RingBufferLogger::new(
@ -267,8 +330,12 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
if scanners.is_empty() {
log::error!("No scanners loaded!");
log::error!("Please ensure:");
log::error!(" 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");
log::error!(
" 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"
);
log::error!(" 3. Scanners are not disabled in the configuration");
logger.log(
LogLevel::Error,
@ -327,7 +394,7 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
})
.to_string(),
);
}
},
Err(e) => {
logger.log(
LogLevel::Error,
@ -335,16 +402,22 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
"final_collection_error",
e.to_string(),
);
}
},
}
state.record_collection().await;
state.update_heartbeat().await.ok();
if let Err(e) = state.update_heartbeat().await {
log::warn!("Failed to update heartbeat during shutdown: {}", e);
}
break;
}
let scan_start = Instant::now();
let scanner_guard = scanner.read().await;
match scanner_guard.collect() {
let collect_result = {
let guard = scanner.read().await;
guard.collect()
};
match collect_result {
Ok(metrics) => {
let elapsed = scan_start.elapsed().as_millis();
logger.log(
@ -358,13 +431,24 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
.to_string(),
);
state.record_collection().await;
}
},
Err(e) => {
logger.log(LogLevel::Error, &name, "collection_error", e.to_string());
logger.log(
LogLevel::Error,
&name,
"collection_error",
e.to_string(),
);
state.record_error().await;
},
}
if let Err(e) = state.update_heartbeat().await {
log::warn!("Failed to update heartbeat: {}", e);
}
}
state.update_heartbeat().await.ok();
if let Err(e) = scanner.write().await.cleanup() {
logger.log(LogLevel::Warn, &name, "cleanup_error", e.to_string());
}
});
@ -379,12 +463,15 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
let mut ticker = interval(Duration::from_secs(5));
loop {
ticker.tick().await;
daemon_state_hb.update_heartbeat().await.ok();
if let Err(e) = daemon_state_hb.update_heartbeat().await {
log::warn!("Failed to update heartbeat: {}", e);
}
}
});
let sigint = tokio::signal::ctrl_c();
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
let mut sigterm =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
tokio::select! {
_ = sigint => {
@ -461,6 +548,18 @@ async fn load_scanners(
continue;
}
// Verify library before loading
if let Err(e) = verify_library(&path) {
log::error!("Scanner {:?} failed verification: {}", path, e);
logger.log(
LogLevel::Error,
"loader",
"verification_failed",
format!("{}: {}", path.display(), e),
);
continue;
}
unsafe {
match Library::new(&path) {
Ok(lib) => {
@ -476,7 +575,7 @@ async fn load_scanners(
format!("{}: {}", path.display(), e),
);
continue;
}
},
};
let scanner = match pscand_core::get_scanner(creator()) {
@ -487,7 +586,7 @@ async fn load_scanners(
path.display()
);
continue;
}
},
};
let name = scanner.name().to_string();
@ -529,7 +628,7 @@ async fn load_scanners(
interval,
library: lib,
});
}
},
Err(e) => {
log::warn!("Failed to load scanner {:?}: {}", path, e);
logger.log(
@ -538,7 +637,7 @@ async fn load_scanners(
"load_failed",
format!("{}: {}", path.display(), e),
);
}
},
}
}
}
@ -561,7 +660,11 @@ async fn list_scanners() -> Result<(), Box<dyn std::error::Error>> {
println!(" - sensor: hwmon temperature, fan, voltage sensors");
println!(" - power: battery and power supply status");
println!(" - proc: process count and zombie detection");
println!("\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)");
println!(" Default fallback: ~/.local/share/pscand/scanners/ or ~/.config/pscand/scanners/");
println!(
"\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)"
);
println!(
" Default fallback: ~/.local/share/pscand/scanners/ or ~/.config/pscand/scanners/"
);
Ok(())
}