initial commit

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib131388c1056b6708b730a35011811026a6a6964
This commit is contained in:
raf 2026-02-18 20:13:00 +03:00
commit 033e253259
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
33 changed files with 3126 additions and 0 deletions

31
pscand-cli/Cargo.toml Normal file
View file

@ -0,0 +1,31 @@
[package]
name = "pscand-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
[[bin]]
name = "pscand"
path = "src/main.rs"
[dependencies]
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
[profile.release]
lto = true

228
pscand-cli/src/main.rs Normal file
View file

@ -0,0 +1,228 @@
use clap::Parser;
use libloading::Library;
use pscand_core::logging::{LogEntry, RingBufferLogger};
use pscand_core::scanner::Scanner;
use pscand_core::Config as CoreConfig;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::time::interval;
type ScannerCreator = unsafe extern "C" fn() -> Box<dyn Scanner>;
#[derive(Parser, Debug)]
#[command(
name = "pscand",
version = "0.1.0",
about = "Pluggable System Condition Monitoring Daemon"
)]
enum Args {
Run(RunArgs),
List,
}
#[derive(Parser, Debug)]
struct RunArgs {
#[arg(short, long, default_value = "/etc/pscand/pscand.conf")]
config: PathBuf,
#[arg(short, long)]
debug: bool,
}
struct LoadedScanner {
name: String,
scanner: Arc<RwLock<Box<dyn Scanner>>>,
#[allow(dead_code)]
library: Library,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
match args {
Args::Run(run_args) => {
run_daemon(run_args).await?;
}
Args::List => {
list_scanners().await?;
}
}
Ok(())
}
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();
} else {
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)?
} else {
log::warn!("Config file not found, using defaults");
CoreConfig::default()
};
std::fs::create_dir_all(&config.log_dir)?;
let log_file = config.log_dir.join("pscand.log");
let logger = Arc::new(RingBufferLogger::new(
config.ring_buffer_size,
Some(log_file),
config.journal_enabled,
config.file_enabled,
));
std::panic::set_hook(Box::new({
let logger = Arc::clone(&logger);
let log_dir = config.log_dir.clone();
move |panic_info| {
let entries = logger.get_recent(60);
let crash_log = log_dir.join("crash.log");
if let Ok(mut file) = std::fs::File::create(&crash_log) {
use std::io::Write;
let _ = writeln!(file, "=== Crash at {} ===", chrono::Utc::now());
let _ = writeln!(file, "Panic: {}", panic_info);
let _ = writeln!(file, "\n=== Last {} log entries ===", entries.len());
for entry in entries {
let _ = writeln!(file, "{}", entry.to_json());
}
}
}
}));
let scanners = load_scanners(&config).await?;
if scanners.is_empty() {
log::warn!("No scanners loaded!");
} else {
log::info!("Loaded {} scanners", scanners.len());
}
let mut handles = Vec::new();
for loaded in scanners {
let logger = Arc::clone(&logger);
let name = loaded.name.clone();
let scanner = loaded.scanner.clone();
let handle = tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(1));
loop {
ticker.tick().await;
let scanner_guard = scanner.read().await;
match scanner_guard.collect() {
Ok(metrics) => {
let entry = LogEntry::new(name.as_str(), metrics);
logger.push(entry);
}
Err(e) => {
log::error!("Scanner {} error: {}", name, e);
}
}
}
});
handles.push(handle);
}
tokio::signal::ctrl_c().await?;
log::info!("Shutting down pscand");
for handle in handles {
handle.abort();
}
Ok(())
}
async fn load_scanners(
config: &CoreConfig,
) -> Result<Vec<LoadedScanner>, Box<dyn std::error::Error>> {
let mut loaded = Vec::new();
for dir in &config.scanner_dirs {
if !dir.exists() {
continue;
}
log::info!("Loading scanners from {:?}", dir);
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("so") {
continue;
}
unsafe {
match Library::new(&path) {
Ok(lib) => {
let creator: libloading::Symbol<ScannerCreator> =
match lib.get(b"pscand_scanner") {
Ok(s) => s,
Err(e) => {
log::warn!("Scanner {:?} missing symbol: {}", path, e);
continue;
}
};
let scanner = creator();
let name = scanner.name().to_string();
let scanner_enabled = config.is_scanner_enabled(&name);
if !scanner_enabled {
log::info!("Scanner {} disabled in config", name);
continue;
}
let mut scanner = scanner;
if let Some(scanner_config) = config.scanner_config(&name) {
let toml_map: toml::map::Map<String, toml::Value> =
scanner_config.extra.clone().into_iter().collect();
let toml_val = toml::Value::Table(toml_map);
if let Err(e) = scanner.init(&toml_val) {
log::error!("Failed to init scanner {}: {}", name, e);
continue;
}
}
loaded.push(LoadedScanner {
name,
scanner: Arc::new(RwLock::new(scanner)),
library: lib,
});
}
Err(e) => {
log::warn!("Failed to load scanner {:?}: {}", path, e);
}
}
}
}
}
Ok(loaded)
}
async fn list_scanners() -> Result<(), Box<dyn std::error::Error>> {
println!("Available built-in scanners:");
println!(" - system: CPU, memory, disk, network, load average");
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/");
Ok(())
}