use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Serialize, Deserialize};
use std::path::PathBuf;
use libloading::{Library, Symbol};
use thiserror::Error as ThisError;
use super::super::core::RiDevice;
use super::platform::{PlatformInfo, HardwareCategory};
pub type PluginResult<T> = Result<T, PluginError>;
#[derive(Debug, Clone, Serialize, Deserialize, ThisError)]
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub enum PluginError {
#[error("Plugin load failed: {0}")]
LoadFailed(String),
#[error("Plugin initialization failed: {0}")]
InitFailed(String),
#[error("Plugin discovery failed: {0}")]
DiscoveryFailed(String),
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("Plugin already registered: {0}")]
AlreadyRegistered(String),
#[error("Unsupported platform for plugin")]
UnsupportedPlatform(),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Library load failed: {0}")]
LibraryLoadFailed(String),
#[error("Symbol resolution failed: {0}")]
SymbolResolutionFailed(String),
#[error("Library unload failed: {0}")]
LibraryUnloadFailed(String),
#[error("Plugin version mismatch: {0}")]
VersionMismatch(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
impl From<std::io::Error> for PluginError {
fn from(e: std::io::Error) -> Self {
PluginError::LoadFailed(format!("IO error: {}", e))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub supported_categories: Vec<HardwareCategory>,
pub min_platform_version: Option<String>,
pub config_schema: Option<String>,
pub dependencies: Vec<String>,
pub enabled_by_default: bool,
}
impl PluginMetadata {
pub fn new(
name: String,
version: String,
author: String,
description: String,
) -> Self {
Self {
name,
version,
author,
description,
supported_categories: Vec::new(),
min_platform_version: None,
config_schema: None,
dependencies: Vec::new(),
enabled_by_default: true,
}
}
pub fn with_category(mut self, category: HardwareCategory) -> Self {
self.supported_categories.push(category);
self
}
pub fn with_min_platform_version(mut self, version: String) -> Self {
self.min_platform_version = Some(version);
self
}
pub fn with_config_schema(mut self, schema: &str) -> Self {
self.config_schema = Some(schema.to_string());
self
}
pub fn with_dependency(mut self, dep: String) -> Self {
self.dependencies.push(dep);
self
}
pub fn with_enabled_by_default(mut self, enabled: bool) -> Self {
self.enabled_by_default = enabled;
self
}
}
#[async_trait]
pub trait RiHardwareDiscoveryPlugin: Send + Sync {
fn metadata(&self) -> PluginMetadata;
async fn initialize(&mut self, config: &str) -> PluginResult<()> {
let _ = config;
Ok(())
}
async fn discover(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>>;
async fn shutdown(&mut self) -> PluginResult<()> {
Ok(())
}
fn status(&self) -> PluginStatus;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub enum PluginStatus {
Loaded(),
Ready(),
Discovering(),
Disabled(),
Error(String),
ShuttingDown(),
}
impl std::fmt::Display for PluginStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginStatus::Loaded() => write!(f, "Plugin is loaded but not initialized"),
PluginStatus::Ready() => write!(f, "Plugin is initialized and ready"),
PluginStatus::Discovering() => write!(f, "Plugin is currently discovering"),
PluginStatus::Disabled() => write!(f, "Plugin has been disabled"),
PluginStatus::Error(msg) => write!(f, "Plugin encountered an error: {}", msg),
PluginStatus::ShuttingDown() => write!(f, "Plugin is being unloaded"),
}
}
}
#[allow(dead_code)]
struct PluginWrapper {
metadata: PluginMetadata,
plugin: Arc<RwLock<Box<dyn RiHardwareDiscoveryPlugin>>>,
status: Arc<RwLock<PluginStatus>>,
config: Arc<RwLock<Option<String>>>,
load_time: std::time::SystemTime,
library: Option<Arc<Library>>,
}
impl PluginWrapper {
pub fn new(plugin: Box<dyn RiHardwareDiscoveryPlugin>) -> Self {
let metadata = plugin.metadata();
let status = plugin.status();
Self {
metadata,
plugin: Arc::new(RwLock::new(plugin)),
status: Arc::new(RwLock::new(status)),
config: Arc::new(RwLock::new(None)),
load_time: std::time::SystemTime::now(),
library: None,
}
}
#[allow(dead_code)]
pub fn with_library(plugin: Box<dyn RiHardwareDiscoveryPlugin>, library: Arc<Library>) -> Self {
let metadata = plugin.metadata();
let status = plugin.status();
Self {
metadata,
plugin: Arc::new(RwLock::new(plugin)),
status: Arc::new(RwLock::new(status)),
config: Arc::new(RwLock::new(None)),
load_time: std::time::SystemTime::now(),
library: Some(library),
}
}
pub async fn initialize(&self, config: &str) -> PluginResult<()> {
let mut plugin = self.plugin.write().await;
plugin.initialize(config).await?;
*self.config.write().await = Some(config.to_string());
*self.status.write().await = PluginStatus::Ready();
Ok(())
}
pub async fn discover(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
*self.status.write().await = PluginStatus::Discovering();
let result = self.plugin.read().await.discover(platform).await;
*self.status.write().await = PluginStatus::Ready();
result
}
pub async fn shutdown(&self) -> PluginResult<()> {
*self.status.write().await = PluginStatus::ShuttingDown();
let mut plugin = self.plugin.write().await;
plugin.shutdown().await?;
*self.status.write().await = PluginStatus::Loaded();
Ok(())
}
pub fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
#[allow(dead_code)]
pub async fn status(&self) -> PluginStatus {
self.status.read().await.clone()
}
#[allow(dead_code)]
pub fn is_dynamic(&self) -> bool {
self.library.is_some()
}
}
#[derive(Default)]
pub struct PluginRegistry {
plugins: Arc<RwLock<HashMap<String, PluginWrapper>>>,
enabled: Arc<RwLock<HashSet<String>>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: Arc::new(RwLock::new(HashMap::new())),
enabled: Arc::new(RwLock::new(HashSet::new())),
}
}
pub async fn register(&mut self, plugin: Box<dyn RiHardwareDiscoveryPlugin>) -> PluginResult<String> {
let metadata = plugin.metadata();
let name = metadata.name.clone();
let mut plugins = self.plugins.write().await;
let mut enabled = self.enabled.write().await;
if plugins.contains_key(&name) {
return Err(PluginError::AlreadyRegistered(name));
}
let wrapper = PluginWrapper::new(plugin);
plugins.insert(name.clone(), wrapper);
if metadata.enabled_by_default {
enabled.insert(name.clone());
}
Ok(name)
}
pub async fn unregister(&mut self, name: &str) -> PluginResult<()> {
let mut plugins = self.plugins.write().await;
let mut enabled = self.enabled.write().await;
if let Some(wrapper) = plugins.remove(name) {
wrapper.shutdown().await?;
enabled.remove(name);
Ok(())
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn initialize(&self, name: &str, config: &str) -> PluginResult<()> {
let plugins = self.plugins.read().await;
if let Some(wrapper) = plugins.get(name) {
wrapper.initialize(config).await
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn discover_with_plugin(
&self,
name: &str,
platform: &PlatformInfo,
) -> PluginResult<Vec<RiDevice>> {
let enabled = self.enabled.read().await;
if !enabled.contains(name) {
return Err(PluginError::NotFound(name.to_string()));
}
let plugins = self.plugins.read().await;
if let Some(wrapper) = plugins.get(name) {
wrapper.discover(platform).await
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn discover_all(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
let enabled = self.enabled.read().await;
let plugins = self.plugins.read().await;
let mut all_devices = Vec::with_capacity(8);
for name in enabled.iter() {
if let Some(wrapper) = plugins.get(name) {
match wrapper.discover(platform).await {
Ok(devices) => all_devices.extend(devices),
Err(e) => tracing::warn!("Plugin {} discovery failed: {}", name, e),
}
}
}
Ok(all_devices)
}
pub async fn enable(&self, name: &str) -> PluginResult<()> {
let plugins = self.plugins.read().await;
if plugins.contains_key(name) {
self.enabled.write().await.insert(name.to_string());
Ok(())
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn disable(&self, name: &str) -> PluginResult<()> {
let plugins = self.plugins.read().await;
if plugins.contains_key(name) {
self.enabled.write().await.remove(name);
Ok(())
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn is_enabled(&self, name: &str) -> bool {
self.enabled.read().await.contains(name)
}
pub async fn registered_plugins(&self) -> Vec<String> {
self.plugins.read().await.keys().cloned().collect()
}
pub async fn enabled_plugins(&self) -> Vec<String> {
self.enabled.read().await.iter().cloned().collect()
}
pub async fn plugin_metadata(&self, name: &str) -> Option<PluginMetadata> {
self.plugins.read().await.get(name).map(|w| w.metadata().clone())
}
pub async fn count(&self) -> usize {
self.plugins.read().await.len()
}
pub async fn enabled_count(&self) -> usize {
self.enabled.read().await.len()
}
}
#[derive(Default)]
pub struct PluginLoader {
search_paths: Arc<RwLock<Vec<PathBuf>>>,
}
impl PluginLoader {
pub fn new() -> Self {
let mut search_paths = Vec::with_capacity(8);
search_paths.push(PathBuf::from("./plugins"));
search_paths.push(PathBuf::from("/usr/local/lib/ri/plugins"));
#[cfg(target_os = "macos")]
search_paths.push(PathBuf::from("/opt/homebrew/lib/ri/plugins"));
Self {
search_paths: Arc::new(RwLock::new(search_paths)),
}
}
pub fn with_paths(paths: Vec<PathBuf>) -> Self {
Self {
search_paths: Arc::new(RwLock::new(paths)),
}
}
pub async fn add_search_path(&self, path: PathBuf) {
self.search_paths.write().await.push(path);
}
pub async fn search_paths(&self) -> Vec<PathBuf> {
self.search_paths.read().await.clone()
}
pub async fn clear_search_paths(&self) {
self.search_paths.write().await.clear();
}
pub async fn load_all(&self, registry: &mut PluginRegistry) -> PluginResult<Vec<String>> {
let mut loaded = Vec::with_capacity(4);
let paths = self.search_paths.read().await;
for path in paths.iter() {
match self.load_plugins_from_path(path, registry).await {
Ok(loaded_plugins) => loaded.extend(loaded_plugins),
Err(e) => tracing::warn!("Failed to load plugins from {}: {}", path.display(), e),
}
}
Ok(loaded)
}
async fn load_plugins_from_path(&self, path: &PathBuf, registry: &mut PluginRegistry) -> PluginResult<Vec<String>> {
let mut loaded = Vec::with_capacity(4);
if !path.exists() {
tracing::debug!("Plugin path does not exist: {}", path.display());
return Ok(loaded);
}
if !path.is_dir() {
tracing::warn!("Plugin path is not a directory: {}", path.display());
return Ok(loaded);
}
let entries = std::fs::read_dir(path)?;
for entry in entries.flatten() {
let entry_path = entry.path();
if !entry_path.is_file() {
continue;
}
let extension = entry_path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase());
let is_plugin = extension.as_ref()
.map(|ext| ext == "so" || ext == "dll" || ext == "dylib")
.unwrap_or(false);
if !is_plugin {
continue;
}
tracing::info!("Found plugin: {}", entry_path.display());
match self.load(&entry_path).await {
Ok(plugin) => {
let name = registry.register(plugin).await?;
tracing::info!("Successfully loaded plugin: {}", name);
loaded.push(name);
}
Err(e) => {
tracing::error!("Failed to load plugin {}: {}", entry_path.display(), e);
}
}
}
Ok(loaded)
}
pub async fn load(&self, path: &PathBuf) -> PluginResult<Box<dyn RiHardwareDiscoveryPlugin>> {
tracing::info!("Loading plugin from: {}", path.display());
self.validate_plugin_path(path)?;
if !path.exists() {
return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
}
let metadata = std::fs::metadata(path)
.map_err(|e| PluginError::LoadFailed(format!("Failed to read file metadata: {}", e)))?;
const MAX_PLUGIN_SIZE: u64 = 100 * 1024 * 1024; if metadata.len() > MAX_PLUGIN_SIZE {
return Err(PluginError::LoadFailed(format!(
"Plugin file too large: {} bytes (max {} bytes)",
metadata.len(), MAX_PLUGIN_SIZE
)));
}
let extension = path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
.unwrap_or_default();
let valid_extensions = match std::env::consts::OS {
"linux" => vec!["so"],
"macos" => vec!["dylib"],
"windows" => vec!["dll"],
_ => vec!["so", "dll", "dylib"],
};
if !valid_extensions.contains(&extension.as_str()) {
return Err(PluginError::LoadFailed(format!(
"Invalid plugin extension: {} (expected: {:?})",
extension, valid_extensions
)));
}
let canonical_path = path.canonicalize()
.map_err(|e| PluginError::LoadFailed(format!("Failed to canonicalize path: {}", e)))?;
let library = Arc::new(
unsafe {
Library::new(&canonical_path)
.map_err(|e| PluginError::LibraryLoadFailed(format!(
"Failed to load library {}: {}", canonical_path.display(), e
)))?
}
);
let plugin = self.load_plugin_from_library(&library, &canonical_path)?;
Ok(plugin)
}
fn validate_plugin_path(&self, path: &PathBuf) -> PluginResult<()> {
let path_str = path.to_string_lossy();
if path_str.contains("..") {
return Err(PluginError::PermissionDenied(
"Directory traversal not allowed in plugin path".to_string()
));
}
if path_str.contains('\0') {
return Err(PluginError::PermissionDenied(
"Null bytes not allowed in plugin path".to_string()
));
}
#[cfg(target_os = "windows")]
{
let file_name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_uppercase();
let reserved_names = [
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
if reserved_names.contains(&file_name.as_str()) {
return Err(PluginError::PermissionDenied(
format!("Reserved file name not allowed: {}", file_name)
));
}
}
Ok(())
}
fn load_plugin_from_library(&self, library: &Arc<Library>, path: &PathBuf) -> PluginResult<Box<dyn RiHardwareDiscoveryPlugin>> {
#[allow(improper_ctypes_definitions)]
type CreatePluginFn = unsafe extern "C" fn() -> *mut dyn RiHardwareDiscoveryPlugin;
unsafe {
let create_symbol: Symbol<CreatePluginFn> = library
.get(b"create_ri_plugin")
.map_err(|e| PluginError::SymbolResolutionFailed(format!(
"Failed to resolve create_ri_plugin symbol in {}: {}", path.display(), e
)))?;
let plugin_ptr = create_symbol();
if plugin_ptr.is_null() {
return Err(PluginError::LoadFailed(format!(
"create_ri_plugin returned null pointer for {}", path.display()
)));
}
let plugin = Box::from_raw(plugin_ptr);
tracing::info!(
"Successfully loaded plugin: {} v{}",
plugin.metadata().name,
plugin.metadata().version
);
Ok(plugin)
}
}
pub async fn validate(&self, path: &PathBuf) -> PluginResult<PluginMetadata> {
if !path.exists() {
return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
}
let library = Arc::new(
unsafe {
Library::new(path)
.map_err(|e| PluginError::LibraryLoadFailed(format!(
"Failed to load library {}: {}", path.display(), e
)))?
}
);
#[allow(improper_ctypes_definitions)]
type GetMetadataFn = unsafe extern "C" fn() -> PluginMetadata;
unsafe {
let metadata_symbol: Symbol<GetMetadataFn> = library
.get(b"get_ri_plugin_metadata")
.map_err(|e| PluginError::SymbolResolutionFailed(format!(
"Failed to resolve get_ri_plugin_metadata symbol in {}: {}", path.display(), e
)))?;
let metadata = metadata_symbol();
tracing::info!(
"Validated plugin: {} v{} (author: {})",
metadata.name,
metadata.version,
metadata.author
);
Ok(metadata)
}
}
pub async fn get_api_version(&self, path: &PathBuf) -> PluginResult<u32> {
type GetVersionFn = unsafe extern "C" fn() -> u32;
let library = Arc::new(
unsafe {
Library::new(path)
.map_err(|e| PluginError::LibraryLoadFailed(format!(
"Failed to load library {}: {}", path.display(), e
)))?
}
);
unsafe {
let version_symbol: Symbol<GetVersionFn> = library
.get(b"get_ri_plugin_api_version")
.map_err(|e| PluginError::SymbolResolutionFailed(format!(
"Failed to resolve get_ri_plugin_api_version symbol in {}: {}", path.display(), e
)))?;
let version = version_symbol();
tracing::debug!("Plugin API version for {}: {}", path.display(), version);
Ok(version)
}
}
pub async fn is_compatible(&self, path: &PathBuf) -> bool {
const CURRENT_API_VERSION: u32 = 1;
match self.get_api_version(path).await {
Ok(version) => version == CURRENT_API_VERSION,
Err(e) => {
tracing::warn!("Failed to check API version for {}: {}", path.display(), e);
false
}
}
}
}
impl Drop for PluginLoader {
fn drop(&mut self) {
tracing::debug!("PluginLoader dropped");
}
}
pub struct CustomProviderPlugin {
name: String,
version: String,
description: String,
discover_func: Arc<dyn Fn() -> Vec<RiDevice> + Send + Sync>,
status: PluginStatus,
}
impl CustomProviderPlugin {
pub fn new<F>(name: &str, version: &str, description: &str, discover_func: F) -> Self
where
F: Fn() -> Vec<RiDevice> + Send + Sync + 'static,
{
Self {
name: name.to_string(),
version: version.to_string(),
description: description.to_string(),
discover_func: Arc::new(discover_func),
status: PluginStatus::Loaded(),
}
}
}
#[async_trait]
impl RiHardwareDiscoveryPlugin for CustomProviderPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new(
self.name.clone(),
self.version.clone(),
"User".to_string(),
self.description.clone(),
)
}
async fn discover(&self, _platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
let devices = (self.discover_func)();
Ok(devices)
}
fn status(&self) -> PluginStatus {
self.status.clone()
}
}
pub fn create_custom_plugin<F>(name: &str, version: &str, description: &str, discover_fn: F) -> Box<dyn RiHardwareDiscoveryPlugin>
where
F: Fn() -> Vec<RiDevice> + Send + Sync + 'static,
{
Box::new(CustomProviderPlugin::new(name, version, description, discover_fn))
}
use std::collections::{HashMap, HashSet};