initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ib131388c1056b6708b730a35011811026a6a6964
This commit is contained in:
commit
033e253259
33 changed files with 3126 additions and 0 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
use flake
|
||||
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
**/target
|
||||
1171
Cargo.lock
generated
Normal file
1171
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
41
Cargo.toml
Normal file
41
Cargo.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"pscand-cli",
|
||||
"pscand-core",
|
||||
"pscand-macros",
|
||||
"scanners/scanner-system",
|
||||
"scanners/scanner-sensor",
|
||||
"scanners/scanner-power",
|
||||
"scanners/scanner-proc",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
authors = ["NotAShelf <raf@notashelf.dev>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
pscand-core = { path = "pscand-core" }
|
||||
pscand-macros = { path = "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"
|
||||
parking_lot = "0.12.5"
|
||||
ringbuf = "0.4.8"
|
||||
dirs = "6.0.0"
|
||||
clap = { version = "4.5.59", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
27
contrib/pscand.example.conf
Normal file
27
contrib/pscand.example.conf
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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",
|
||||
]
|
||||
|
||||
[scanners.system]
|
||||
enabled = true
|
||||
interval_secs = 1
|
||||
|
||||
[scanners.sensor]
|
||||
enabled = true
|
||||
interval_secs = 2
|
||||
|
||||
[scanners.power]
|
||||
enabled = true
|
||||
interval_secs = 2
|
||||
|
||||
[scanners.proc]
|
||||
enabled = true
|
||||
interval_secs = 5
|
||||
34
contrib/systemd/pscand.service
Normal file
34
contrib/systemd/pscand.service
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[Unit]
|
||||
Description=Pluggable System Condition Monitoring Daemon
|
||||
Documentation=https://github.com/pscand/pscand
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/pscand run --config /etc/pscand/pscand.conf
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
# Log to journal
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Capabilities for sensor access
|
||||
AmbientCapabilities=CAP_SYS_ADMIN CAP_DAC_OVERRIDE
|
||||
|
||||
# Security hardening
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=false
|
||||
|
||||
# Runtime directory
|
||||
RuntimeDirectory=pscand
|
||||
RuntimeDirectoryMode=0755
|
||||
|
||||
# Tempfs for sensitive /proc data
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
23
flake.nix
Normal file
23
flake.nix
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
description = "Rust Project Template";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
}: let
|
||||
systems = ["x86_64-linux" "aarch64-linux"];
|
||||
forEachSystem = nixpkgs.lib.genAttrs systems;
|
||||
pkgsForEach = nixpkgs.legacyPackages;
|
||||
in {
|
||||
packages = forEachSystem (system: {
|
||||
default = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
||||
});
|
||||
|
||||
devShells = forEachSystem (system: {
|
||||
default = pkgsForEach.${system}.callPackage ./nix/shell.nix {};
|
||||
});
|
||||
|
||||
hydraJobs = self.packages;
|
||||
};
|
||||
}
|
||||
29
nix/package.nix
Normal file
29
nix/package.nix
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
}:
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "sample-rust";
|
||||
version = "0.1.0";
|
||||
|
||||
src = let
|
||||
fs = lib.fileset;
|
||||
s = ../.;
|
||||
in
|
||||
fs.toSource {
|
||||
root = s;
|
||||
fileset = fs.unions [
|
||||
(fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src))
|
||||
(s + /Cargo.lock)
|
||||
(s + /Cargo.toml)
|
||||
];
|
||||
};
|
||||
|
||||
cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock";
|
||||
enableParallelBuilding = true;
|
||||
|
||||
meta = {
|
||||
description = "Sample Rust project";
|
||||
maintainers = with lib.maintainers; [NotAShelf];
|
||||
};
|
||||
})
|
||||
30
nix/shell.nix
Normal file
30
nix/shell.nix
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
mkShell,
|
||||
rustc,
|
||||
cargo,
|
||||
rust-analyzer-unwrapped,
|
||||
rustfmt,
|
||||
clippy,
|
||||
taplo,
|
||||
rustPlatform,
|
||||
}:
|
||||
mkShell {
|
||||
name = "rust";
|
||||
|
||||
strictDeps = true;
|
||||
packages = [
|
||||
rustc
|
||||
cargo
|
||||
|
||||
# Tools
|
||||
rustfmt
|
||||
clippy
|
||||
cargo
|
||||
taplo
|
||||
|
||||
# LSP
|
||||
rust-analyzer-unwrapped
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";
|
||||
}
|
||||
31
pscand-cli/Cargo.toml
Normal file
31
pscand-cli/Cargo.toml
Normal 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
228
pscand-cli/src/main.rs
Normal 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(())
|
||||
}
|
||||
23
pscand-core/Cargo.toml
Normal file
23
pscand-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
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
|
||||
parking_lot.workspace = true
|
||||
dirs.workspace = true
|
||||
sysinfo.workspace = true
|
||||
ringbuf.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "pscand_core"
|
||||
path = "src/lib.rs"
|
||||
126
pscand-core/src/config.rs
Normal file
126
pscand-core/src/config.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
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),
|
||||
}
|
||||
|
||||
pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScannerConfig {
|
||||
pub enabled: bool,
|
||||
pub interval_secs: Option<u64>,
|
||||
pub extra: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_scanner_dirs")]
|
||||
pub scanner_dirs: Vec<PathBuf>,
|
||||
#[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<String, ScannerConfig>,
|
||||
#[serde(default)]
|
||||
pub journal_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub file_enabled: bool,
|
||||
}
|
||||
|
||||
fn default_scanner_dirs() -> Vec<PathBuf> {
|
||||
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 Some(lib) = std::env::var_os("LIB_PSCAND") {
|
||||
dirs.push(PathBuf::from(lib));
|
||||
}
|
||||
|
||||
if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") {
|
||||
dirs.push(PathBuf::from(lib_dir));
|
||||
}
|
||||
|
||||
if let Some(local) = dirs::data_local_dir() {
|
||||
dirs.push(local.join("pscand/scanners"));
|
||||
}
|
||||
|
||||
if let Some(config) = dirs::config_dir() {
|
||||
dirs.push(config.join("pscand/scanners"));
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
fn default_retention_days() -> u32 {
|
||||
7
|
||||
}
|
||||
|
||||
fn default_ring_buffer_size() -> usize {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: &PathBuf) -> ConfigResult<Self> {
|
||||
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 is_scanner_enabled(&self, name: &str) -> bool {
|
||||
self.scanners.get(name).map(|c| c.enabled).unwrap_or(true)
|
||||
}
|
||||
}
|
||||
11
pscand-core/src/helpers/mod.rs
Normal file
11
pscand-core/src/helpers/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
pub mod power;
|
||||
pub mod process;
|
||||
pub mod resource;
|
||||
pub mod sensor;
|
||||
pub mod system;
|
||||
|
||||
pub use power::PowerHelper;
|
||||
pub use process::ProcessHelper;
|
||||
pub use resource::ResourceHelper;
|
||||
pub use sensor::SensorHelper;
|
||||
pub use system::SystemHelper;
|
||||
157
pscand-core/src/helpers/power.rs
Normal file
157
pscand-core/src/helpers/power.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::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,
|
||||
}
|
||||
|
||||
impl PowerHelper {
|
||||
pub fn battery_info() -> std::io::Result<Option<BatteryInfo>> {
|
||||
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");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 capacity = fs::read_to_string(path.join("capacity"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().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::<i64>().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::<i64>().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 voltage = fs::read_to_string(path.join("voltage_now"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f64>().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::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let power_now = fs::read_to_string(path.join("power_now"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
return Ok(Some(BatteryInfo {
|
||||
name,
|
||||
status,
|
||||
capacity,
|
||||
charge_percent,
|
||||
voltage,
|
||||
current_now,
|
||||
power_now,
|
||||
present,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn power_supplies() -> std::io::Result<HashMap<String, HashMap<String, String>>> {
|
||||
let mut supplies = HashMap::new();
|
||||
let power_supply_path = PathBuf::from("/sys/class/power_supply");
|
||||
|
||||
if !power_supply_path.exists() {
|
||||
return Ok(supplies);
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(&power_supply_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut info = HashMap::new();
|
||||
|
||||
for attr in [
|
||||
"type",
|
||||
"status",
|
||||
"capacity",
|
||||
"voltage_now",
|
||||
"power_now",
|
||||
"online",
|
||||
] {
|
||||
let attr_path = path.join(attr);
|
||||
if attr_path.exists() {
|
||||
if let Ok(content) = fs::read_to_string(&attr_path) {
|
||||
info.insert(attr.to_string(), content.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !info.is_empty() {
|
||||
supplies.insert(name, info);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(supplies)
|
||||
}
|
||||
|
||||
pub fn suspend_state() -> std::io::Result<String> {
|
||||
let state_path = PathBuf::from("/sys/power/state");
|
||||
fs::read_to_string(state_path).map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn mem_sleep_state() -> std::io::Result<String> {
|
||||
let state_path = PathBuf::from("/sys/power/mem_sleep");
|
||||
fs::read_to_string(state_path).map(|s| s.trim().to_string())
|
||||
}
|
||||
}
|
||||
143
pscand-core/src/helpers/process.rs
Normal file
143
pscand-core/src/helpers/process.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use std::collections::HashMap;
|
||||
use std::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,
|
||||
}
|
||||
|
||||
impl ProcessHelper {
|
||||
pub fn list_processes() -> std::io::Result<Vec<ProcessInfo>> {
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn process_info(pid: u32) -> std::io::Result<ProcessInfo> {
|
||||
let status_path = format!("/proc/{}/status", pid);
|
||||
let content = fs::read_to_string(status_path)?;
|
||||
|
||||
let mut name = String::new();
|
||||
let mut state = String::new();
|
||||
let mut ppid: u32 = 0;
|
||||
let mut memory_kb: u64 = 0;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("Name:") {
|
||||
name = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
} else if line.starts_with("State:") {
|
||||
state = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
} else if line.starts_with("PPid:") {
|
||||
ppid = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
} else if line.starts_with("VmRSS:") {
|
||||
memory_kb = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ProcessInfo {
|
||||
pid,
|
||||
name,
|
||||
state,
|
||||
ppid,
|
||||
memory_kb,
|
||||
cpu_percent: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn zombie_processes() -> std::io::Result<Vec<ProcessInfo>> {
|
||||
Ok(Self::list_processes()?
|
||||
.into_iter()
|
||||
.filter(|p| p.state.starts_with('Z'))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn process_count() -> std::io::Result<HashMap<String, usize>> {
|
||||
let mut counts = HashMap::new();
|
||||
counts.insert("total".to_string(), 0);
|
||||
counts.insert("running".to_string(), 0);
|
||||
counts.insert("sleeping".to_string(), 0);
|
||||
counts.insert("zombie".to_string(), 0);
|
||||
|
||||
for proc in Self::list_processes()? {
|
||||
*counts.get_mut("total").unwrap() += 1;
|
||||
|
||||
let first_char = proc.state.chars().next().unwrap_or(' ');
|
||||
match first_char {
|
||||
'R' => *counts.get_mut("running").unwrap() += 1,
|
||||
'S' | 'D' => *counts.get_mut("sleeping").unwrap() += 1,
|
||||
'Z' => *counts.get_mut("zombie").unwrap() += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
pub fn top_memory_processes(count: usize) -> std::io::Result<Vec<ProcessInfo>> {
|
||||
let mut processes = Self::list_processes()?;
|
||||
processes.sort_by(|a, b| b.memory_kb.cmp(&a.memory_kb));
|
||||
processes.truncate(count);
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
pub fn top_cpu_processes(count: usize) -> std::io::Result<Vec<ProcessInfo>> {
|
||||
let mut processes = Self::list_processes()?;
|
||||
processes.sort_by(|a, b| {
|
||||
b.cpu_percent
|
||||
.partial_cmp(&a.cpu_percent)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
processes.truncate(count);
|
||||
Ok(processes)
|
||||
}
|
||||
}
|
||||
123
pscand-core/src/helpers/resource.rs
Normal file
123
pscand-core/src/helpers/resource.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
|
||||
pub struct ResourceHelper;
|
||||
|
||||
impl ResourceHelper {
|
||||
pub fn cpu_usage() -> std::io::Result<HashMap<String, f64>> {
|
||||
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<u64> = 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;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn memory_info() -> std::io::Result<HashMap<String, u64>> {
|
||||
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::<u64>() {
|
||||
result.insert(key.to_string(), value * 1024);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn disk_stats() -> std::io::Result<HashMap<String, HashMap<String, u64>>> {
|
||||
let content = fs::read_to_string("/proc/diskstats")?;
|
||||
let mut result = HashMap::new();
|
||||
|
||||
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<HashMap<String, HashMap<String, u64>>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
99
pscand-core/src/helpers/sensor.rs
Normal file
99
pscand-core/src/helpers/sensor.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct SensorHelper;
|
||||
|
||||
impl SensorHelper {
|
||||
pub fn discover_hwmon() -> std::io::Result<Vec<PathBuf>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(hwmons)
|
||||
}
|
||||
|
||||
pub fn read_hwmon_sensor(hwmon_path: &PathBuf, sensor: &str) -> std::io::Result<Option<f64>> {
|
||||
let sensor_path = hwmon_path.join(sensor);
|
||||
if sensor_path.exists() {
|
||||
let content = fs::read_to_string(sensor_path)?;
|
||||
Ok(content.trim().parse::<f64>().ok())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hwmon_info(hwmon_path: &PathBuf) -> std::io::Result<HashMap<String, String>> {
|
||||
let mut info = HashMap::new();
|
||||
|
||||
let name_path = hwmon_path.join("name");
|
||||
if name_path.exists() {
|
||||
info.insert(
|
||||
"name".to_string(),
|
||||
fs::read_to_string(name_path)?.trim().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
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 Ok(temp) = Self::read_hwmon_sensor(hwmon_path, &filename) {
|
||||
if let Some(t) = temp {
|
||||
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 Ok(fan) = Self::read_hwmon_sensor(hwmon_path, &filename) {
|
||||
if let Some(f) = fan {
|
||||
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 Ok(voltage) = Self::read_hwmon_sensor(hwmon_path, &filename) {
|
||||
if let Some(v) = voltage {
|
||||
info.insert(format!("voltage_{}_mv", id), format!("{}", v / 1000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub fn all_sensors() -> std::io::Result<HashMap<String, HashMap<String, String>>> {
|
||||
let mut all = HashMap::new();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
45
pscand-core/src/helpers/system.rs
Normal file
45
pscand-core/src/helpers/system.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct SystemHelper;
|
||||
|
||||
impl SystemHelper {
|
||||
pub fn uptime() -> std::io::Result<Duration> {
|
||||
let uptime_secs = fs::read_to_string("/proc/uptime")?
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
Ok(Duration::from_secs_f64(uptime_secs))
|
||||
}
|
||||
|
||||
pub fn boot_id() -> std::io::Result<String> {
|
||||
fs::read_to_string("/proc/sys/kernel/random/boot_id").map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn load_average() -> std::io::Result<(f64, f64, f64)> {
|
||||
let content = fs::read_to_string("/proc/loadavg")?;
|
||||
let mut parts = content.split_whitespace();
|
||||
let load1 = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
let load5 = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
let load15 = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
Ok((load1, load5, load15))
|
||||
}
|
||||
|
||||
pub fn hostname() -> std::io::Result<String> {
|
||||
fs::read_to_string("/proc/sys/kernel/hostname").map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn kernel_version() -> std::io::Result<String> {
|
||||
fs::read_to_string("/proc/sys/kernel/osrelease").map(|s| s.trim().to_string())
|
||||
}
|
||||
}
|
||||
9
pscand-core/src/lib.rs
Normal file
9
pscand-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod config;
|
||||
pub mod helpers;
|
||||
pub mod logging;
|
||||
pub mod scanner;
|
||||
|
||||
pub use config::Config;
|
||||
pub use logging::{LogEntry, RingBufferLogger};
|
||||
pub use scanner::{MetricValue, Scanner, ScannerError};
|
||||
pub type Result<T> = std::result::Result<T, ScannerError>;
|
||||
137
pscand-core/src/logging.rs
Normal file
137
pscand-core/src/logging.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use parking_lot::Mutex;
|
||||
use ringbuf::{
|
||||
storage::Heap,
|
||||
traits::*,
|
||||
wrap::caching::{CachingCons, CachingProd},
|
||||
SharedRb,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::scanner::MetricValue;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub scanner: String,
|
||||
pub metrics: HashMap<String, MetricValue>,
|
||||
}
|
||||
|
||||
impl LogEntry {
|
||||
pub fn new(scanner: impl Into<String>, metrics: HashMap<String, MetricValue>) -> Self {
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
scanner: scanner.into(),
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
format!(
|
||||
"PSCAND_SCANNER={} PSCAND_METRICS={}",
|
||||
self.scanner, metrics_json
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type RbStorage = Heap<LogEntry>;
|
||||
type SharedRbLog = SharedRb<RbStorage>;
|
||||
|
||||
struct RingBufferHandles {
|
||||
prod: CachingProd<Arc<SharedRbLog>>,
|
||||
cons: CachingCons<Arc<SharedRbLog>>,
|
||||
}
|
||||
|
||||
pub struct RingBufferLogger {
|
||||
buffer: Arc<Mutex<RingBufferHandles>>,
|
||||
file_path: Option<PathBuf>,
|
||||
journal_enabled: bool,
|
||||
file_enabled: bool,
|
||||
}
|
||||
|
||||
impl RingBufferLogger {
|
||||
pub fn new(
|
||||
capacity: usize,
|
||||
file_path: Option<PathBuf>,
|
||||
journal_enabled: bool,
|
||||
file_enabled: bool,
|
||||
) -> Self {
|
||||
let rb = SharedRb::<RbStorage>::new(capacity);
|
||||
let (prod, cons) = rb.split();
|
||||
|
||||
let handles = RingBufferHandles { prod, cons };
|
||||
|
||||
Self {
|
||||
buffer: Arc::new(Mutex::new(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());
|
||||
}
|
||||
|
||||
if self.journal_enabled {
|
||||
self.write_to_journal(&entry);
|
||||
}
|
||||
if self.file_enabled {
|
||||
self.write_to_file(&entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_journal(&self, entry: &LogEntry) {
|
||||
let msg = entry.to_journal();
|
||||
let _ = std::process::Command::new("logger")
|
||||
.arg("-t")
|
||||
.arg("pscand")
|
||||
.arg("-p")
|
||||
.arg("info")
|
||||
.arg(msg)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_recent(&self, count: usize) -> Vec<LogEntry> {
|
||||
let handles = self.buffer.lock();
|
||||
handles.cons.iter().rev().take(count).cloned().collect()
|
||||
}
|
||||
|
||||
pub fn flush_to_file(&self, path: &PathBuf) -> 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)
|
||||
}
|
||||
}
|
||||
112
pscand-core/src/scanner.rs
Normal file
112
pscand-core/src/scanner.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ScannerError>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum MetricValue {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
Boolean(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl MetricValue {
|
||||
pub fn from_i64(v: i64) -> Self {
|
||||
MetricValue::Integer(v)
|
||||
}
|
||||
|
||||
pub fn from_f64(v: f64) -> Self {
|
||||
MetricValue::Float(v)
|
||||
}
|
||||
|
||||
pub fn from_bool(v: bool) -> Self {
|
||||
MetricValue::Boolean(v)
|
||||
}
|
||||
|
||||
pub fn from_str(v: impl Into<String>) -> 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) -> Result<HashMap<String, MetricValue>>;
|
||||
fn cleanup(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait ScannerRegistry: Send + Sync {
|
||||
fn list_scanners(&self) -> Vec<&'static str>;
|
||||
fn get_scanner(&self, name: &str) -> Option<Box<dyn Scanner>>;
|
||||
}
|
||||
|
||||
pub struct DynamicScanner {
|
||||
name: &'static str,
|
||||
interval: Duration,
|
||||
collect_fn: Box<dyn Fn() -> Result<HashMap<String, MetricValue>> + Send + Sync>,
|
||||
init_fn: Mutex<Box<dyn FnMut(&toml::Value) -> Result<()> + Send>>,
|
||||
cleanup_fn: Mutex<Box<dyn FnMut() -> Result<()> + Send>>,
|
||||
}
|
||||
|
||||
impl DynamicScanner {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
interval: Duration,
|
||||
collect_fn: impl Fn() -> Result<HashMap<String, MetricValue>> + 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 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 collect(&self) -> Result<HashMap<String, MetricValue>> {
|
||||
(self.collect_fn)()
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> Result<()> {
|
||||
let mut cleanup_fn = self.cleanup_fn.lock().unwrap();
|
||||
(cleanup_fn)()
|
||||
}
|
||||
}
|
||||
14
pscand-macros/Cargo.toml
Normal file
14
pscand-macros/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "pscand-macros"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
62
pscand-macros/src/lib.rs
Normal file
62
pscand-macros/src/lib.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
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 _fn_name = input.sig.ident.clone();
|
||||
let body = &input.block;
|
||||
|
||||
let result = quote! {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn pscand_core::Scanner> {
|
||||
struct ScannerImpl;
|
||||
|
||||
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 init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect(&self) -> pscand_core::Result<std::collections::HashMap<String, pscand_core::MetricValue>> {
|
||||
#body
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> pscand_core::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Box::new(ScannerImpl)
|
||||
}
|
||||
};
|
||||
|
||||
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 result = quote! {
|
||||
#input
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn pscand_core::Scanner> {
|
||||
Box::new(#fn_name())
|
||||
}
|
||||
};
|
||||
|
||||
result.into()
|
||||
}
|
||||
15
scanners/scanner-power/Cargo.toml
Normal file
15
scanners/scanner-power/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "scanner-power"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pscand-core.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "scanner_power"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
94
scanners/scanner-power/src/lib.rs
Normal file
94
scanners/scanner-power/src/lib.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#![allow(improper_ctypes)]
|
||||
use pscand_core::helpers::PowerHelper;
|
||||
use pscand_core::scanner::{MetricValue, Scanner};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
struct PowerScanner;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> {
|
||||
Box::new(PowerScanner)
|
||||
}
|
||||
|
||||
impl Default for PowerScanner {
|
||||
fn default() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Scanner for PowerScanner {
|
||||
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<HashMap<String, MetricValue>> {
|
||||
let mut metrics = HashMap::new();
|
||||
|
||||
if let Ok(Some(battery)) = PowerHelper::battery_info() {
|
||||
metrics.insert(
|
||||
"battery_present".to_string(),
|
||||
MetricValue::from_bool(battery.present),
|
||||
);
|
||||
metrics.insert(
|
||||
"battery_charge_percent".to_string(),
|
||||
MetricValue::Integer(battery.charge_percent as i64),
|
||||
);
|
||||
metrics.insert(
|
||||
"battery_voltage_v".to_string(),
|
||||
MetricValue::from_f64(battery.voltage),
|
||||
);
|
||||
metrics.insert(
|
||||
"battery_power_now_mw".to_string(),
|
||||
MetricValue::Integer(battery.power_now),
|
||||
);
|
||||
metrics.insert(
|
||||
"battery_status".to_string(),
|
||||
MetricValue::from_str(&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_str(status),
|
||||
);
|
||||
}
|
||||
if let Some(online) = info.get("online") {
|
||||
metrics.insert(
|
||||
format!("supply_{}_online", name),
|
||||
MetricValue::from_bool(online == "1"),
|
||||
);
|
||||
}
|
||||
if let Some(capacity) = info.get("capacity") {
|
||||
if let Ok(cap) = capacity.parse::<u32>() {
|
||||
metrics.insert(
|
||||
format!("supply_{}_capacity", name),
|
||||
MetricValue::Integer(cap as i64),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(state) = PowerHelper::suspend_state() {
|
||||
metrics.insert("suspend_state".to_string(), MetricValue::from_str(&state));
|
||||
}
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> pscand_core::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
15
scanners/scanner-proc/Cargo.toml
Normal file
15
scanners/scanner-proc/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "scanner-proc"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pscand-core.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "scanner_proc"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
99
scanners/scanner-proc/src/lib.rs
Normal file
99
scanners/scanner-proc/src/lib.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use pscand_core::helpers::ProcessHelper;
|
||||
use pscand_core::scanner::{MetricValue, Scanner};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
struct ProcScanner;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> {
|
||||
Box::new(ProcScanner)
|
||||
}
|
||||
|
||||
impl Default for ProcScanner {
|
||||
fn default() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Scanner for ProcScanner {
|
||||
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<HashMap<String, MetricValue>> {
|
||||
let mut metrics = HashMap::new();
|
||||
|
||||
if let Ok(counts) = ProcessHelper::process_count() {
|
||||
if let Some(total) = counts.get("total") {
|
||||
metrics.insert(
|
||||
"process_total".to_string(),
|
||||
MetricValue::Integer(*total as i64),
|
||||
);
|
||||
}
|
||||
if let Some(running) = counts.get("running") {
|
||||
metrics.insert(
|
||||
"process_running".to_string(),
|
||||
MetricValue::Integer(*running as i64),
|
||||
);
|
||||
}
|
||||
if let Some(sleeping) = counts.get("sleeping") {
|
||||
metrics.insert(
|
||||
"process_sleeping".to_string(),
|
||||
MetricValue::Integer(*sleeping as i64),
|
||||
);
|
||||
}
|
||||
if let Some(zombie) = counts.get("zombie") {
|
||||
metrics.insert(
|
||||
"process_zombie".to_string(),
|
||||
MetricValue::Integer(*zombie as i64),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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_str(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_str(&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(())
|
||||
}
|
||||
}
|
||||
15
scanners/scanner-sensor/Cargo.toml
Normal file
15
scanners/scanner-sensor/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "scanner-sensor"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pscand-core.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "scanner_sensor"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
80
scanners/scanner-sensor/src/lib.rs
Normal file
80
scanners/scanner-sensor/src/lib.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#![allow(improper_ctypes)]
|
||||
use pscand_core::helpers::SensorHelper;
|
||||
use pscand_core::scanner::{MetricValue, Scanner};
|
||||
use pscand_core::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
struct SensorScanner;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> {
|
||||
Box::new(SensorScanner)
|
||||
}
|
||||
|
||||
impl Default for SensorScanner {
|
||||
fn default() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Scanner for SensorScanner {
|
||||
fn name(&self) -> &'static str {
|
||||
"sensor"
|
||||
}
|
||||
|
||||
fn interval(&self) -> Duration {
|
||||
Duration::from_secs(2)
|
||||
}
|
||||
|
||||
fn init(&mut self, _config: &toml::Value) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect(&self) -> Result<HashMap<String, MetricValue>> {
|
||||
let mut metrics = HashMap::new();
|
||||
|
||||
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::<f64>() {
|
||||
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::<f64>() {
|
||||
metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
|
||||
fan_count += 1;
|
||||
if fan_count <= 2 {
|
||||
metrics
|
||||
.insert(format!("fan_{}", fan_count), MetricValue::from_f64(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
if key.starts_with("voltage_") {
|
||||
if let Ok(v) = value.parse::<f64>() {
|
||||
metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
15
scanners/scanner-system/Cargo.toml
Normal file
15
scanners/scanner-system/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "scanner-system"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pscand-core.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "scanner_system"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
88
scanners/scanner-system/src/lib.rs
Normal file
88
scanners/scanner-system/src/lib.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#![allow(improper_ctypes)]
|
||||
use pscand_core::helpers::{ResourceHelper, SystemHelper};
|
||||
use pscand_core::scanner::{MetricValue, Scanner};
|
||||
use pscand_core::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
struct SystemScanner {
|
||||
_prev_cpu: Option<HashMap<String, f64>>,
|
||||
}
|
||||
|
||||
impl Default for SystemScanner {
|
||||
fn default() -> Self {
|
||||
Self { _prev_cpu: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> {
|
||||
Box::new(SystemScanner::default())
|
||||
}
|
||||
|
||||
impl Scanner for SystemScanner {
|
||||
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<HashMap<String, MetricValue>> {
|
||||
let mut metrics = HashMap::new();
|
||||
|
||||
if let Ok(uptime) = SystemHelper::uptime() {
|
||||
metrics.insert(
|
||||
"uptime_secs".to_string(),
|
||||
MetricValue::from_f64(uptime.as_secs_f64()),
|
||||
);
|
||||
}
|
||||
|
||||
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) = mem.get("MemAvailable") {
|
||||
if let Some(total) = mem.get("MemTotal") {
|
||||
let used_mem = total.saturating_sub(*used);
|
||||
metrics.insert(
|
||||
"mem_used_bytes".to_string(),
|
||||
MetricValue::Integer(used_mem as i64),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue