1use 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
64pub type PluginResult<T> = Result<T, PluginError>;
66
67#[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#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
117pub struct PluginMetadata {
118 pub name: String,
120 pub version: String,
122 pub author: String,
124 pub description: String,
126 pub supported_categories: Vec<HardwareCategory>,
128 pub min_platform_version: Option<String>,
130 pub config_schema: Option<String>,
132 pub dependencies: Vec<String>,
134 pub enabled_by_default: bool,
136}
137
138impl PluginMetadata {
139 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 pub fn with_category(mut self, category: HardwareCategory) -> Self {
161 self.supported_categories.push(category);
162 self
163 }
164
165 pub fn with_min_platform_version(mut self, version: String) -> Self {
167 self.min_platform_version = Some(version);
168 self
169 }
170
171 pub fn with_config_schema(mut self, schema: &str) -> Self {
173 self.config_schema = Some(schema.to_string());
174 self
175 }
176
177 pub fn with_dependency(mut self, dep: String) -> Self {
179 self.dependencies.push(dep);
180 self
181 }
182
183 pub fn with_enabled_by_default(mut self, enabled: bool) -> Self {
185 self.enabled_by_default = enabled;
186 self
187 }
188}
189
190#[async_trait]
192pub trait RiHardwareDiscoveryPlugin: Send + Sync {
193 fn metadata(&self) -> PluginMetadata;
195
196 async fn initialize(&mut self, config: &str) -> PluginResult<()> {
198 let _ = config;
199 Ok(())
200 }
201
202 async fn discover(&self, platform: &PlatformInfo) -> PluginResult<Vec<RiDevice>>;
204
205 async fn shutdown(&mut self) -> PluginResult<()> {
207 Ok(())
208 }
209
210 fn status(&self) -> PluginStatus;
212}
213
214#[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#[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#[derive(Default)]
320pub struct PluginRegistry {
321 plugins: Arc<RwLock<HashMap<String, PluginWrapper>>>,
322 enabled: Arc<RwLock<HashSet<String>>>,
323}
324
325impl PluginRegistry {
326 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 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 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 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 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 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 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 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 pub async fn is_enabled(&self, name: &str) -> bool {
440 self.enabled.read().await.contains(name)
441 }
442
443 pub async fn registered_plugins(&self) -> Vec<String> {
445 self.plugins.read().await.keys().cloned().collect()
446 }
447
448 pub async fn enabled_plugins(&self) -> Vec<String> {
450 self.enabled.read().await.iter().cloned().collect()
451 }
452
453 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 pub async fn count(&self) -> usize {
460 self.plugins.read().await.len()
461 }
462
463 pub async fn enabled_count(&self) -> usize {
465 self.enabled.read().await.len()
466 }
467}
468
469#[derive(Default)]
471pub struct PluginLoader {
472 search_paths: Arc<RwLock<Vec<PathBuf>>>,
473}
474
475impl PluginLoader {
476 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 pub fn with_paths(paths: Vec<PathBuf>) -> Self {
491 Self {
492 search_paths: Arc::new(RwLock::new(paths)),
493 }
494 }
495
496 pub async fn add_search_path(&self, path: PathBuf) {
498 self.search_paths.write().await.push(path);
499 }
500
501 pub async fn search_paths(&self) -> Vec<PathBuf> {
503 self.search_paths.read().await.clone()
504 }
505
506 pub async fn clear_search_paths(&self) {
508 self.search_paths.write().await.clear();
509 }
510
511 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 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 pub async fn load(&self, path: &PathBuf) -> PluginResult<Box<dyn RiHardwareDiscoveryPlugin>> {
588 tracing::info!("Loading plugin from: {}", path.display());
589
590 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 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; 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 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 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 fn validate_plugin_path(&self, path: &PathBuf) -> PluginResult<()> {
655 let path_str = path.to_string_lossy();
656
657 if path_str.contains("..") {
659 return Err(PluginError::PermissionDenied(
660 "Directory traversal not allowed in plugin path".to_string()
661 ));
662 }
663
664 if path_str.contains('\0') {
666 return Err(PluginError::PermissionDenied(
667 "Null bytes not allowed in plugin path".to_string()
668 ));
669 }
670
671 #[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 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 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 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 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
813pub 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 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
859pub 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};