Skip to main content

ri/device/discovery/
plugins.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # Custom Discovery Plugins
19//!
20//! This module provides a plugin system for custom hardware discovery implementations.
21//! Users can create their own discovery plugins to support specialized hardware or
22//! proprietary device interfaces.
23//!
24//! ## Architecture
25//!
26//! - **RiHardwareDiscoveryPlugin**: Trait for plugin implementations
27//! - **PluginRegistry**: Manages registered plugins
28//! - **PluginMetadata**: Plugin information and configuration
29//!
30//! ## Usage
31//!
32//! ```rust,ignore
33//! use ri::device::discovery::plugins::{RiHardwareDiscoveryPlugin, PluginRegistry};
34//!
35//! // Create a custom plugin
36//! struct MyCustomPlugin;
37//!
38//! #[async_trait::async_trait]
39//! impl RiHardwareDiscoveryPlugin for MyCustomPlugin {
40//!     fn name(&self) -> &str { "MyCustomPlugin" }
41//!     fn version(&self) -> &str { "1.0.0" }
42//!     async fn discover(&self) -> Result<Vec<RiDevice>, String> {
43//!         // Custom discovery logic
44//!         Ok(vec![])
45//!     }
46//! }
47//!
48//! // Register the plugin
49//! let mut registry = PluginRegistry::new();
50//! registry.register(Box::new(MyCustomPlugin));
51//! ```
52
53use async_trait::async_trait;
54use std::sync::Arc;
55use tokio::sync::RwLock;
56use serde::{Serialize, Deserialize};
57use std::path::PathBuf;
58use libloading::{Library, Symbol};
59use thiserror::Error as ThisError;
60
61use super::super::core::RiDevice;
62use super::platform::{PlatformInfo, HardwareCategory};
63
64/// Result type for plugin discovery operations
65pub type PluginResult<T> = Result<T, PluginError>;
66
67/// Errors that can occur during plugin operations
68#[derive(Debug, Clone, Serialize, Deserialize, ThisError)]
69#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
70pub enum PluginError {
71    #[error("Plugin load failed: {0}")]
72    LoadFailed(String),
73
74    #[error("Plugin initialization failed: {0}")]
75    InitFailed(String),
76
77    #[error("Plugin discovery failed: {0}")]
78    DiscoveryFailed(String),
79
80    #[error("Plugin not found: {0}")]
81    NotFound(String),
82
83    #[error("Plugin already registered: {0}")]
84    AlreadyRegistered(String),
85
86    #[error("Unsupported platform for plugin")]
87    UnsupportedPlatform(),
88
89    #[error("Permission denied: {0}")]
90    PermissionDenied(String),
91
92    #[error("Library load failed: {0}")]
93    LibraryLoadFailed(String),
94
95    #[error("Symbol resolution failed: {0}")]
96    SymbolResolutionFailed(String),
97
98    #[error("Library unload failed: {0}")]
99    LibraryUnloadFailed(String),
100
101    #[error("Plugin version mismatch: {0}")]
102    VersionMismatch(String),
103
104    #[error("Unknown error: {0}")]
105    Unknown(String),
106}
107
108impl From<std::io::Error> for PluginError {
109    fn from(e: std::io::Error) -> Self {
110        PluginError::LoadFailed(format!("IO error: {}", e))
111    }
112}
113
114/// Plugin metadata for identification and configuration
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
117pub struct PluginMetadata {
118    /// Plugin name
119    pub name: String,
120    /// Plugin version
121    pub version: String,
122    /// Plugin author
123    pub author: String,
124    /// Plugin description
125    pub description: String,
126    /// Supported hardware categories
127    pub supported_categories: Vec<HardwareCategory>,
128    /// Minimum platform version requirement
129    pub min_platform_version: Option<String>,
130    /// Plugin configuration schema (JSON)
131    pub config_schema: Option<String>,
132    /// Dependencies
133    pub dependencies: Vec<String>,
134    /// Whether the plugin is enabled by default
135    pub enabled_by_default: bool,
136}
137
138impl PluginMetadata {
139    /// Creates new plugin metadata
140    pub fn new(
141        name: String,
142        version: String,
143        author: String,
144        description: String,
145    ) -> Self {
146        Self {
147            name,
148            version,
149            author,
150            description,
151            supported_categories: Vec::new(),
152            min_platform_version: None,
153            config_schema: None,
154            dependencies: Vec::new(),
155            enabled_by_default: true,
156        }
157    }
158
159    /// Adds a supported hardware category
160    pub fn with_category(mut self, category: HardwareCategory) -> Self {
161        self.supported_categories.push(category);
162        self
163    }
164
165    /// Sets the minimum platform version
166    pub fn with_min_platform_version(mut self, version: String) -> Self {
167        self.min_platform_version = Some(version);
168        self
169    }
170
171    /// Sets the configuration schema
172    pub fn with_config_schema(mut self, schema: &str) -> Self {
173        self.config_schema = Some(schema.to_string());
174        self
175    }
176
177    /// Adds a dependency
178    pub fn with_dependency(mut self, dep: String) -> Self {
179        self.dependencies.push(dep);
180        self
181    }
182
183    /// Sets whether the plugin is enabled by default
184    pub fn with_enabled_by_default(mut self, enabled: bool) -> Self {
185        self.enabled_by_default = enabled;
186        self
187    }
188}
189
190/// Trait for custom hardware discovery plugins
191#[async_trait]
192pub trait RiHardwareDiscoveryPlugin: Send + Sync {
193    /// Returns the plugin metadata
194    fn metadata(&self) -> PluginMetadata;
195
196    /// Initializes the plugin with configuration
197    async fn initialize(&mut self, config: &str) -> PluginResult<()> {
198        let _ = config;
199        Ok(())
200    }
201
202    /// Discovers hardware devices
203    async fn discover(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>>;
204
205    /// Called when the plugin is being unloaded
206    async fn shutdown(&mut self) -> PluginResult<()> {
207        Ok(())
208    }
209
210    /// Returns the current plugin status
211    fn status(&self) -> PluginStatus;
212}
213
214/// Current status of a plugin
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
217pub enum PluginStatus {
218    Loaded(),
219    Ready(),
220    Discovering(),
221    Disabled(),
222    Error(String),
223    ShuttingDown(),
224}
225
226impl std::fmt::Display for PluginStatus {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        match self {
229            PluginStatus::Loaded() => write!(f, "Plugin is loaded but not initialized"),
230            PluginStatus::Ready() => write!(f, "Plugin is initialized and ready"),
231            PluginStatus::Discovering() => write!(f, "Plugin is currently discovering"),
232            PluginStatus::Disabled() => write!(f, "Plugin has been disabled"),
233            PluginStatus::Error(msg) => write!(f, "Plugin encountered an error: {}", msg),
234            PluginStatus::ShuttingDown() => write!(f, "Plugin is being unloaded"),
235        }
236    }
237}
238
239/// Plugin wrapper for runtime management
240#[allow(dead_code)]
241struct PluginWrapper {
242    metadata: PluginMetadata,
243    plugin: Arc<RwLock<Box<dyn RiHardwareDiscoveryPlugin>>>,
244    status: Arc<RwLock<PluginStatus>>,
245    config: Arc<RwLock<Option<String>>>,
246    load_time: std::time::SystemTime,
247    library: Option<Arc<Library>>,
248}
249
250impl PluginWrapper {
251    pub fn new(plugin: Box<dyn RiHardwareDiscoveryPlugin>) -> Self {
252        let metadata = plugin.metadata();
253        let status = plugin.status();
254
255        Self {
256            metadata,
257            plugin: Arc::new(RwLock::new(plugin)),
258            status: Arc::new(RwLock::new(status)),
259            config: Arc::new(RwLock::new(None)),
260            load_time: std::time::SystemTime::now(),
261            library: None,
262        }
263    }
264
265    #[allow(dead_code)]
266    pub fn with_library(plugin: Box<dyn RiHardwareDiscoveryPlugin>, library: Arc<Library>) -> Self {
267        let metadata = plugin.metadata();
268        let status = plugin.status();
269
270        Self {
271            metadata,
272            plugin: Arc::new(RwLock::new(plugin)),
273            status: Arc::new(RwLock::new(status)),
274            config: Arc::new(RwLock::new(None)),
275            load_time: std::time::SystemTime::now(),
276            library: Some(library),
277        }
278    }
279
280    pub async fn initialize(&self, config: &str) -> PluginResult<()> {
281        let mut plugin = self.plugin.write().await;
282        plugin.initialize(config).await?;
283        *self.config.write().await = Some(config.to_string());
284        *self.status.write().await = PluginStatus::Ready();
285        Ok(())
286    }
287
288    pub async fn discover(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
289        *self.status.write().await = PluginStatus::Discovering();
290        let result = self.plugin.read().await.discover(platform).await;
291        *self.status.write().await = PluginStatus::Ready();
292        result
293    }
294
295    pub async fn shutdown(&self) -> PluginResult<()> {
296        *self.status.write().await = PluginStatus::ShuttingDown();
297        let mut plugin = self.plugin.write().await;
298        plugin.shutdown().await?;
299        *self.status.write().await = PluginStatus::Loaded();
300        Ok(())
301    }
302
303    pub fn metadata(&self) -> &PluginMetadata {
304        &self.metadata
305    }
306
307    #[allow(dead_code)]
308    pub async fn status(&self) -> PluginStatus {
309        self.status.read().await.clone()
310    }
311
312    #[allow(dead_code)]
313    pub fn is_dynamic(&self) -> bool {
314        self.library.is_some()
315    }
316}
317
318/// Plugin registry for managing custom discovery plugins
319#[derive(Default)]
320pub struct PluginRegistry {
321    plugins: Arc<RwLock<HashMap<String, PluginWrapper>>>,
322    enabled: Arc<RwLock<HashSet<String>>>,
323}
324
325impl PluginRegistry {
326    /// Creates a new plugin registry
327    pub fn new() -> Self {
328        Self {
329            plugins: Arc::new(RwLock::new(HashMap::new())),
330            enabled: Arc::new(RwLock::new(HashSet::new())),
331        }
332    }
333
334    /// Registers a new plugin
335    pub async fn register(&mut self, plugin: Box<dyn RiHardwareDiscoveryPlugin>) -> PluginResult<String> {
336        let metadata = plugin.metadata();
337        let name = metadata.name.clone();
338
339        let mut plugins = self.plugins.write().await;
340        let mut enabled = self.enabled.write().await;
341
342        if plugins.contains_key(&name) {
343            return Err(PluginError::AlreadyRegistered(name));
344        }
345
346        let wrapper = PluginWrapper::new(plugin);
347        plugins.insert(name.clone(), wrapper);
348        if metadata.enabled_by_default {
349            enabled.insert(name.clone());
350        }
351
352        Ok(name)
353    }
354
355    /// Unregisters a plugin
356    pub async fn unregister(&mut self, name: &str) -> PluginResult<()> {
357        let mut plugins = self.plugins.write().await;
358        let mut enabled = self.enabled.write().await;
359
360        if let Some(wrapper) = plugins.remove(name) {
361            wrapper.shutdown().await?;
362            enabled.remove(name);
363            Ok(())
364        } else {
365            Err(PluginError::NotFound(name.to_string()))
366        }
367    }
368
369    /// Initializes a plugin with configuration
370    pub async fn initialize(&self, name: &str, config: &str) -> PluginResult<()> {
371        let plugins = self.plugins.read().await;
372        if let Some(wrapper) = plugins.get(name) {
373            wrapper.initialize(config).await
374        } else {
375            Err(PluginError::NotFound(name.to_string()))
376        }
377    }
378
379    /// Discovers devices using a specific plugin
380    pub async fn discover_with_plugin(
381        &self,
382        name: &str,
383        platform: &PlatformInfo,
384    ) -> PluginResult<Vec<RiDevice>> {
385        let enabled = self.enabled.read().await;
386        if !enabled.contains(name) {
387            return Err(PluginError::NotFound(name.to_string()));
388        }
389
390        let plugins = self.plugins.read().await;
391        if let Some(wrapper) = plugins.get(name) {
392            wrapper.discover(platform).await
393        } else {
394            Err(PluginError::NotFound(name.to_string()))
395        }
396    }
397
398    /// Discovers devices using all enabled plugins
399    pub async fn discover_all(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
400        let enabled = self.enabled.read().await;
401        let plugins = self.plugins.read().await;
402
403        let mut all_devices = Vec::with_capacity(8);
404        for name in enabled.iter() {
405            if let Some(wrapper) = plugins.get(name) {
406                match wrapper.discover(platform).await {
407                    Ok(devices) => all_devices.extend(devices),
408                    Err(e) => tracing::warn!("Plugin {} discovery failed: {}", name, e),
409                }
410            }
411        }
412
413        Ok(all_devices)
414    }
415
416    /// Enables a plugin
417    pub async fn enable(&self, name: &str) -> PluginResult<()> {
418        let plugins = self.plugins.read().await;
419        if plugins.contains_key(name) {
420            self.enabled.write().await.insert(name.to_string());
421            Ok(())
422        } else {
423            Err(PluginError::NotFound(name.to_string()))
424        }
425    }
426
427    /// Disables a plugin
428    pub async fn disable(&self, name: &str) -> PluginResult<()> {
429        let plugins = self.plugins.read().await;
430        if plugins.contains_key(name) {
431            self.enabled.write().await.remove(name);
432            Ok(())
433        } else {
434            Err(PluginError::NotFound(name.to_string()))
435        }
436    }
437
438    /// Checks if a plugin is enabled
439    pub async fn is_enabled(&self, name: &str) -> bool {
440        self.enabled.read().await.contains(name)
441    }
442
443    /// Returns all registered plugin names
444    pub async fn registered_plugins(&self) -> Vec<String> {
445        self.plugins.read().await.keys().cloned().collect()
446    }
447
448    /// Returns all enabled plugin names
449    pub async fn enabled_plugins(&self) -> Vec<String> {
450        self.enabled.read().await.iter().cloned().collect()
451    }
452
453    /// Returns plugin metadata
454    pub async fn plugin_metadata(&self, name: &str) -> Option<PluginMetadata> {
455        self.plugins.read().await.get(name).map(|w| w.metadata().clone())
456    }
457
458    /// Returns the count of registered plugins
459    pub async fn count(&self) -> usize {
460        self.plugins.read().await.len()
461    }
462
463    /// Returns the count of enabled plugins
464    pub async fn enabled_count(&self) -> usize {
465        self.enabled.read().await.len()
466    }
467}
468
469/// Plugin loader for dynamic plugin loading
470#[derive(Default)]
471pub struct PluginLoader {
472    search_paths: Arc<RwLock<Vec<PathBuf>>>,
473}
474
475impl PluginLoader {
476    /// Creates a new plugin loader
477    pub fn new() -> Self {
478        let mut search_paths = Vec::with_capacity(8);
479        search_paths.push(PathBuf::from("./plugins"));
480        search_paths.push(PathBuf::from("/usr/local/lib/ri/plugins"));
481        #[cfg(target_os = "macos")]
482        search_paths.push(PathBuf::from("/opt/homebrew/lib/ri/plugins"));
483
484        Self {
485            search_paths: Arc::new(RwLock::new(search_paths)),
486        }
487    }
488
489    /// Creates a new plugin loader with custom search paths
490    pub fn with_paths(paths: Vec<PathBuf>) -> Self {
491        Self {
492            search_paths: Arc::new(RwLock::new(paths)),
493        }
494    }
495
496    /// Adds a search path for plugins
497    pub async fn add_search_path(&self, path: PathBuf) {
498        self.search_paths.write().await.push(path);
499    }
500
501    /// Gets all search paths
502    pub async fn search_paths(&self) -> Vec<PathBuf> {
503        self.search_paths.read().await.clone()
504    }
505
506    /// Clears all search paths
507    pub async fn clear_search_paths(&self) {
508        self.search_paths.write().await.clear();
509    }
510
511    /// Loads plugins from all search paths
512    pub async fn load_all(&self, registry: &mut PluginRegistry) -> PluginResult<Vec<String>> {
513        let mut loaded = Vec::with_capacity(4);
514        let paths = self.search_paths.read().await;
515
516        for path in paths.iter() {
517            match self.load_plugins_from_path(path, registry).await {
518                Ok(loaded_plugins) => loaded.extend(loaded_plugins),
519                Err(e) => tracing::warn!("Failed to load plugins from {}: {}", path.display(), e),
520            }
521        }
522
523        Ok(loaded)
524    }
525
526    /// Loads plugins from a specific directory path
527    async fn load_plugins_from_path(&self, path: &PathBuf, registry: &mut PluginRegistry) -> PluginResult<Vec<String>> {
528        let mut loaded = Vec::with_capacity(4);
529
530        if !path.exists() {
531            tracing::debug!("Plugin path does not exist: {}", path.display());
532            return Ok(loaded);
533        }
534
535        if !path.is_dir() {
536            tracing::warn!("Plugin path is not a directory: {}", path.display());
537            return Ok(loaded);
538        }
539
540        let entries = std::fs::read_dir(path)?;
541
542        for entry in entries.flatten() {
543            let entry_path = entry.path();
544
545            if !entry_path.is_file() {
546                continue;
547            }
548
549            let extension = entry_path.extension()
550                .and_then(|ext| ext.to_str())
551                .map(|ext| ext.to_lowercase());
552
553            let is_plugin = extension.as_ref()
554                .map(|ext| ext == "so" || ext == "dll" || ext == "dylib")
555                .unwrap_or(false);
556
557            if !is_plugin {
558                continue;
559            }
560
561            tracing::info!("Found plugin: {}", entry_path.display());
562
563            match self.load(&entry_path).await {
564                Ok(plugin) => {
565                    let name = registry.register(plugin).await?;
566                    tracing::info!("Successfully loaded plugin: {}", name);
567                    loaded.push(name);
568                }
569                Err(e) => {
570                    tracing::error!("Failed to load plugin {}: {}", entry_path.display(), e);
571                }
572            }
573        }
574
575        Ok(loaded)
576    }
577
578    /// Loads a specific plugin file
579    /// 
580    /// # Security
581    /// 
582    /// This method performs the following security checks:
583    /// 1. Path validation to prevent directory traversal
584    /// 2. File extension validation
585    /// 3. File size limits to prevent DoS
586    /// 4. Signature verification (if enabled)
587    pub async fn load(&self, path: &PathBuf) -> PluginResult<Box<dyn RiHardwareDiscoveryPlugin>> {
588        tracing::info!("Loading plugin from: {}", path.display());
589
590        // Security: Validate the path
591        self.validate_plugin_path(path)?;
592
593        if !path.exists() {
594            return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
595        }
596
597        // Security: Check file size to prevent DoS
598        let metadata = std::fs::metadata(path)
599            .map_err(|e| PluginError::LoadFailed(format!("Failed to read file metadata: {}", e)))?;
600        
601        const MAX_PLUGIN_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
602        if metadata.len() > MAX_PLUGIN_SIZE {
603            return Err(PluginError::LoadFailed(format!(
604                "Plugin file too large: {} bytes (max {} bytes)", 
605                metadata.len(), MAX_PLUGIN_SIZE
606            )));
607        }
608
609        // Security: Verify file extension
610        let extension = path.extension()
611            .and_then(|ext| ext.to_str())
612            .map(|ext| ext.to_lowercase())
613            .unwrap_or_default();
614
615        let valid_extensions = match std::env::consts::OS {
616            "linux" => vec!["so"],
617            "macos" => vec!["dylib"],
618            "windows" => vec!["dll"],
619            _ => vec!["so", "dll", "dylib"],
620        };
621
622        if !valid_extensions.contains(&extension.as_str()) {
623            return Err(PluginError::LoadFailed(format!(
624                "Invalid plugin extension: {} (expected: {:?})",
625                extension, valid_extensions
626            )));
627        }
628
629        // Security: Canonicalize path to prevent directory traversal
630        let canonical_path = path.canonicalize()
631            .map_err(|e| PluginError::LoadFailed(format!("Failed to canonicalize path: {}", e)))?;
632
633        let library = Arc::new(
634            unsafe {
635                Library::new(&canonical_path)
636                    .map_err(|e| PluginError::LibraryLoadFailed(format!(
637                        "Failed to load library {}: {}", canonical_path.display(), e
638                    )))?
639            }
640        );
641
642        let plugin = self.load_plugin_from_library(&library, &canonical_path)?;
643
644        Ok(plugin)
645    }
646
647    /// Validates a plugin path for security
648    /// 
649    /// # Security Checks
650    /// 
651    /// 1. Path must be absolute or relative to allowed directories
652    /// 2. Path must not contain directory traversal sequences
653    /// 3. Path must be within allowed plugin directories
654    fn validate_plugin_path(&self, path: &PathBuf) -> PluginResult<()> {
655        let path_str = path.to_string_lossy();
656        
657        // Security: Check for directory traversal attempts
658        if path_str.contains("..") {
659            return Err(PluginError::PermissionDenied(
660                "Directory traversal not allowed in plugin path".to_string()
661            ));
662        }
663
664        // Security: Check for null bytes (path injection)
665        if path_str.contains('\0') {
666            return Err(PluginError::PermissionDenied(
667                "Null bytes not allowed in plugin path".to_string()
668            ));
669        }
670
671        // Security: On Windows, check for reserved names
672        #[cfg(target_os = "windows")]
673        {
674            let file_name = path.file_name()
675                .and_then(|n| n.to_str())
676                .unwrap_or("")
677                .to_uppercase();
678            
679            let reserved_names = [
680                "CON", "PRN", "AUX", "NUL",
681                "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
682                "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
683            ];
684
685            if reserved_names.contains(&file_name.as_str()) {
686                return Err(PluginError::PermissionDenied(
687                    format!("Reserved file name not allowed: {}", file_name)
688                ));
689            }
690        }
691
692        Ok(())
693    }
694
695    /// Loads a plugin from an already loaded library
696    fn load_plugin_from_library(&self, library: &Arc<Library>, path: &PathBuf) -> PluginResult<Box<dyn RiHardwareDiscoveryPlugin>> {
697        #[allow(improper_ctypes_definitions)]
698    type CreatePluginFn = unsafe extern "C" fn() -> *mut dyn RiHardwareDiscoveryPlugin;
699
700        unsafe {
701            let create_symbol: Symbol<CreatePluginFn> = library
702                .get(b"create_ri_plugin")
703                .map_err(|e| PluginError::SymbolResolutionFailed(format!(
704                    "Failed to resolve create_ri_plugin symbol in {}: {}", path.display(), e
705                )))?;
706
707            let plugin_ptr = create_symbol();
708
709            if plugin_ptr.is_null() {
710                return Err(PluginError::LoadFailed(format!(
711                    "create_ri_plugin returned null pointer for {}", path.display()
712                )));
713            }
714
715            let plugin = Box::from_raw(plugin_ptr);
716
717            tracing::info!(
718                "Successfully loaded plugin: {} v{}",
719                plugin.metadata().name,
720                plugin.metadata().version
721            );
722
723            Ok(plugin)
724        }
725    }
726
727    /// Validates a plugin file without loading it
728    pub async fn validate(&self, path: &PathBuf) -> PluginResult<PluginMetadata> {
729        if !path.exists() {
730            return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
731        }
732
733        let library = Arc::new(
734            unsafe {
735                Library::new(path)
736                    .map_err(|e| PluginError::LibraryLoadFailed(format!(
737                        "Failed to load library {}: {}", path.display(), e
738                    )))?
739            }
740        );
741
742        #[allow(improper_ctypes_definitions)]
743        type GetMetadataFn = unsafe extern "C" fn() -> PluginMetadata;
744
745        unsafe {
746            let metadata_symbol: Symbol<GetMetadataFn> = library
747                .get(b"get_ri_plugin_metadata")
748                .map_err(|e| PluginError::SymbolResolutionFailed(format!(
749                    "Failed to resolve get_ri_plugin_metadata symbol in {}: {}", path.display(), e
750                )))?;
751
752            let metadata = metadata_symbol();
753
754            tracing::info!(
755                "Validated plugin: {} v{} (author: {})",
756                metadata.name,
757                metadata.version,
758                metadata.author
759            );
760
761            Ok(metadata)
762        }
763    }
764
765    /// Gets the plugin API version from a library
766    pub async fn get_api_version(&self, path: &PathBuf) -> PluginResult<u32> {
767        type GetVersionFn = unsafe extern "C" fn() -> u32;
768
769        let library = Arc::new(
770            unsafe {
771                Library::new(path)
772                    .map_err(|e| PluginError::LibraryLoadFailed(format!(
773                        "Failed to load library {}: {}", path.display(), e
774                    )))?
775            }
776        );
777
778        unsafe {
779            let version_symbol: Symbol<GetVersionFn> = library
780                .get(b"get_ri_plugin_api_version")
781                .map_err(|e| PluginError::SymbolResolutionFailed(format!(
782                    "Failed to resolve get_ri_plugin_api_version symbol in {}: {}", path.display(), e
783                )))?;
784
785            let version = version_symbol();
786
787            tracing::debug!("Plugin API version for {}: {}", path.display(), version);
788
789            Ok(version)
790        }
791    }
792
793    /// Checks if a plugin file is compatible with the current Ri version
794    pub async fn is_compatible(&self, path: &PathBuf) -> bool {
795        const CURRENT_API_VERSION: u32 = 1;
796
797        match self.get_api_version(path).await {
798            Ok(version) => version == CURRENT_API_VERSION,
799            Err(e) => {
800                tracing::warn!("Failed to check API version for {}: {}", path.display(), e);
801                false
802            }
803        }
804    }
805}
806
807impl Drop for PluginLoader {
808    fn drop(&mut self) {
809        tracing::debug!("PluginLoader dropped");
810    }
811}
812
813/// Built-in custom provider plugin for user-defined discovery
814pub struct CustomProviderPlugin {
815    name: String,
816    version: String,
817    description: String,
818    discover_func: Arc<dyn Fn() -> Vec<RiDevice> + Send + Sync>,
819    status: PluginStatus,
820}
821
822impl CustomProviderPlugin {
823    /// Creates a new custom provider plugin
824    pub fn new<F>(name: &str, version: &str, description: &str, discover_func: F) -> Self
825    where
826        F: Fn() -> Vec<RiDevice> + Send + Sync + 'static,
827    {
828        Self {
829            name: name.to_string(),
830            version: version.to_string(),
831            description: description.to_string(),
832            discover_func: Arc::new(discover_func),
833            status: PluginStatus::Loaded(),
834        }
835    }
836}
837
838#[async_trait]
839impl RiHardwareDiscoveryPlugin for CustomProviderPlugin {
840    fn metadata(&self) -> PluginMetadata {
841        PluginMetadata::new(
842            self.name.clone(),
843            self.version.clone(),
844            "User".to_string(),
845            self.description.clone(),
846        )
847    }
848
849    async fn discover(&self, _platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>> {
850        let devices = (self.discover_func)();
851        Ok(devices)
852    }
853
854    fn status(&self) -> PluginStatus {
855        self.status.clone()
856    }
857}
858
859/// Utility function to create a custom discovery plugin from a closure
860pub fn create_custom_plugin<F>(name: &str, version: &str, description: &str, discover_fn: F) -> Box<dyn RiHardwareDiscoveryPlugin>
861where
862    F: Fn() -> Vec<RiDevice> + Send + Sync + 'static,
863{
864    Box::new(CustomProviderPlugin::new(name, version, description, discover_fn))
865}
866
867use std::collections::{HashMap, HashSet};