scanner: make plugin interface ffi-safe with handle-based registry

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8e4790db3cc29f84f4e0d7d8eff36c2c6a6a6964
This commit is contained in:
raf 2026-02-18 22:49:12 +03:00
commit f4961c7f95
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
9 changed files with 93 additions and 44 deletions

View file

@ -26,6 +26,3 @@ ringbuf.workspace = true
dirs.workspace = true dirs.workspace = true
sysinfo.workspace = true sysinfo.workspace = true
clap.workspace = true clap.workspace = true
[profile.release]
lto = true

View file

@ -1,3 +1,5 @@
#![allow(improper_ctypes_definitions)]
use clap::Parser; use clap::Parser;
use libloading::Library; use libloading::Library;
use pscand_core::logging::{LogLevel, RingBufferLogger}; use pscand_core::logging::{LogLevel, RingBufferLogger};
@ -10,7 +12,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::time::interval; use tokio::time::interval;
type ScannerCreator = unsafe extern "C" fn() -> Box<dyn Scanner>; type ScannerCreator = pscand_core::ScannerCreatorFfi;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command( #[command(
@ -452,7 +454,16 @@ async fn load_scanners(
} }
}; };
let scanner = creator(); let scanner = match pscand_core::get_scanner(creator()) {
Some(s) => s,
None => {
log::error!(
"Failed to get scanner from library {}",
path.display()
);
continue;
}
};
let name = scanner.name().to_string(); let name = scanner.name().to_string();
let scanner_enabled = config.is_scanner_enabled(&name); let scanner_enabled = config.is_scanner_enabled(&name);

View file

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

View file

@ -1,10 +1,24 @@
use std::collections::HashMap; 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::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
pub type ScannerBox = Box<dyn Scanner>;
pub type ScannerResult = Result<ScannerBox>;
pub type ScannerMetrics = HashMap<String, MetricValue>;
pub type ScannerCollectionResult = Result<ScannerMetrics>;
pub type ScannerCollectFn = Box<dyn Fn() -> ScannerCollectionResult + Send + Sync>;
pub type ScannerInitFnMut = Mutex<Box<dyn FnMut(&toml::Value) -> Result<()> + Send>>;
pub type ScannerCleanupFnMut = Mutex<Box<dyn FnMut() -> Result<()> + Send>>;
pub type 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);
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ScannerError { pub enum ScannerError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
@ -43,7 +57,7 @@ impl MetricValue {
MetricValue::Boolean(v) MetricValue::Boolean(v)
} }
pub fn from_str(v: impl Into<String>) -> Self { pub fn from_string(v: impl Into<String>) -> Self {
MetricValue::String(v.into()) MetricValue::String(v.into())
} }
} }
@ -52,28 +66,56 @@ pub trait Scanner: Send + Sync {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn interval(&self) -> Duration; fn interval(&self) -> Duration;
fn init(&mut self, config: &toml::Value) -> Result<()>; fn init(&mut self, config: &toml::Value) -> Result<()>;
fn collect(&self) -> Result<HashMap<String, MetricValue>>; fn collect(&self) -> ScannerCollectionResult;
fn cleanup(&mut self) -> Result<()>; fn cleanup(&mut self) -> Result<()>;
} }
static SCANNER_HANDLES: LazyLock<Mutex<HashMap<usize, ScannerBox>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1);
#[inline]
/// Register a scanner and return a handle.
/// # 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
}
#[inline]
/// Retrieve a scanner by handle, consuming the registration.
/// # Safety
/// 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<ScannerBox> {
let handle = handle as usize;
if handle == 0 {
None
} else {
SCANNER_HANDLES.lock().unwrap().remove(&handle)
}
}
pub trait ScannerRegistry: Send + Sync { pub trait ScannerRegistry: Send + Sync {
fn list_scanners(&self) -> Vec<&'static str>; fn list_scanners(&self) -> Vec<&'static str>;
fn get_scanner(&self, name: &str) -> Option<Box<dyn Scanner>>; fn get_scanner(&self, name: &str) -> Option<ScannerBox>;
} }
pub struct DynamicScanner { pub struct DynamicScanner {
name: &'static str, name: &'static str,
interval: Duration, interval: Duration,
collect_fn: Box<dyn Fn() -> Result<HashMap<String, MetricValue>> + Send + Sync>, collect_fn: ScannerCollectFn,
init_fn: Mutex<Box<dyn FnMut(&toml::Value) -> Result<()> + Send>>, init_fn: ScannerInitFnMut,
cleanup_fn: Mutex<Box<dyn FnMut() -> Result<()> + Send>>, cleanup_fn: ScannerCleanupFnMut,
} }
impl DynamicScanner { impl DynamicScanner {
pub fn new( pub fn new(
name: &'static str, name: &'static str,
interval: Duration, interval: Duration,
collect_fn: impl Fn() -> Result<HashMap<String, MetricValue>> + Send + Sync + 'static, collect_fn: impl Fn() -> ScannerCollectionResult + Send + Sync + 'static,
init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static, init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static,
cleanup_fn: impl FnMut() -> Result<()> + Send + 'static, cleanup_fn: impl FnMut() -> Result<()> + Send + 'static,
) -> Self { ) -> Self {
@ -101,7 +143,7 @@ impl Scanner for DynamicScanner {
(init_fn)(config) (init_fn)(config)
} }
fn collect(&self) -> Result<HashMap<String, MetricValue>> { fn collect(&self) -> ScannerCollectionResult {
(self.collect_fn)() (self.collect_fn)()
} }

View file

@ -12,7 +12,7 @@ pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream {
let result = quote! { let result = quote! {
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> Box<dyn pscand_core::Scanner> { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
struct ScannerImpl; struct ScannerImpl;
impl pscand_core::Scanner for ScannerImpl { impl pscand_core::Scanner for ScannerImpl {
@ -37,7 +37,8 @@ pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream {
} }
} }
Box::new(ScannerImpl) let handle = unsafe { pscand_core::register_scanner(Box::new(ScannerImpl)) };
handle as *mut std::os::raw::c_void
} }
}; };
@ -53,8 +54,9 @@ pub fn register_scanner(input: TokenStream) -> TokenStream {
#input #input
#[no_mangle] #[no_mangle]
pub extern "C" fn pscand_scanner() -> Box<dyn pscand_core::Scanner> { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::new(#fn_name()) let handle = unsafe { pscand_core::register_scanner(Box::new(#fn_name())) };
handle as *mut std::os::raw::c_void
} }
}; };

View file

@ -1,4 +1,3 @@
#![allow(improper_ctypes)]
use pscand_core::helpers::PowerHelper; use pscand_core::helpers::PowerHelper;
use pscand_core::scanner::{MetricValue, Scanner}; use pscand_core::scanner::{MetricValue, Scanner};
use std::collections::HashMap; use std::collections::HashMap;
@ -6,9 +5,9 @@ use std::time::Duration;
struct PowerScanner; struct PowerScanner;
#[unsafe(no_mangle)] #[no_mangle]
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::new(PowerScanner) Box::into_raw(Box::new(PowerScanner)) as *mut std::os::raw::c_void
} }
impl Default for PowerScanner { impl Default for PowerScanner {
@ -52,7 +51,7 @@ impl Scanner for PowerScanner {
); );
metrics.insert( metrics.insert(
"battery_status".to_string(), "battery_status".to_string(),
MetricValue::from_str(&battery.status), MetricValue::from_string(&battery.status),
); );
} }
@ -61,7 +60,7 @@ impl Scanner for PowerScanner {
if let Some(status) = info.get("status") { if let Some(status) = info.get("status") {
metrics.insert( metrics.insert(
format!("supply_{}_status", name), format!("supply_{}_status", name),
MetricValue::from_str(status), MetricValue::from_string(status),
); );
} }
if let Some(online) = info.get("online") { if let Some(online) = info.get("online") {
@ -82,7 +81,10 @@ impl Scanner for PowerScanner {
} }
if let Ok(state) = PowerHelper::suspend_state() { if let Ok(state) = PowerHelper::suspend_state() {
metrics.insert("suspend_state".to_string(), MetricValue::from_str(&state)); metrics.insert(
"suspend_state".to_string(),
MetricValue::from_string(&state),
);
} }
Ok(metrics) Ok(metrics)

View file

@ -5,9 +5,9 @@ use std::time::Duration;
struct ProcScanner; struct ProcScanner;
#[unsafe(no_mangle)] #[no_mangle]
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::new(ProcScanner) Box::into_raw(Box::new(ProcScanner)) as *mut std::os::raw::c_void
} }
impl Default for ProcScanner { impl Default for ProcScanner {
@ -72,7 +72,7 @@ impl Scanner for ProcScanner {
} }
metrics.insert( metrics.insert(
"zombie_processes".to_string(), "zombie_processes".to_string(),
MetricValue::from_str(zombie_info.join(",")), MetricValue::from_string(zombie_info.join(",")),
); );
} }
} }
@ -81,7 +81,7 @@ impl Scanner for ProcScanner {
for (i, proc) in top_mem.iter().enumerate() { for (i, proc) in top_mem.iter().enumerate() {
metrics.insert( metrics.insert(
format!("top_mem_{}_name", i + 1), format!("top_mem_{}_name", i + 1),
MetricValue::from_str(&proc.name), MetricValue::from_string(&proc.name),
); );
metrics.insert( metrics.insert(
format!("top_mem_{}_mb", i + 1), format!("top_mem_{}_mb", i + 1),

View file

@ -1,4 +1,3 @@
#![allow(improper_ctypes)]
use pscand_core::helpers::SensorHelper; use pscand_core::helpers::SensorHelper;
use pscand_core::scanner::{MetricValue, Scanner}; use pscand_core::scanner::{MetricValue, Scanner};
use pscand_core::Result; use pscand_core::Result;
@ -7,9 +6,9 @@ use std::time::Duration;
struct SensorScanner; struct SensorScanner;
#[unsafe(no_mangle)] #[no_mangle]
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
Box::new(SensorScanner) Box::into_raw(Box::new(SensorScanner)) as *mut std::os::raw::c_void
} }
impl Default for SensorScanner { impl Default for SensorScanner {

View file

@ -1,23 +1,17 @@
#![allow(improper_ctypes)]
use pscand_core::helpers::{ResourceHelper, SystemHelper}; use pscand_core::helpers::{ResourceHelper, SystemHelper};
use pscand_core::scanner::{MetricValue, Scanner}; use pscand_core::scanner::{MetricValue, Scanner};
use pscand_core::Result; use pscand_core::Result;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
#[derive(Default)]
struct SystemScanner { struct SystemScanner {
_prev_cpu: Option<HashMap<String, f64>>, _prev_cpu: Option<HashMap<String, f64>>,
} }
impl Default for SystemScanner { #[no_mangle]
fn default() -> Self { pub extern "C" fn pscand_scanner() -> *mut std::os::raw::c_void {
Self { _prev_cpu: None } Box::into_raw(Box::new(SystemScanner::default())) as *mut std::os::raw::c_void
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pscand_scanner() -> Box<dyn Scanner> {
Box::new(SystemScanner::default())
} }
impl Scanner for SystemScanner { impl Scanner for SystemScanner {