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::DMSCDevice;
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 DMSCHardwareDiscoveryPlugin: 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<DMSCDevice>>;
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 DMSCHardwareDiscoveryPlugin>>>,
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 DMSCHardwareDiscoveryPlugin>) -> 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 DMSCHardwareDiscoveryPlugin>, 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<DMSCDevice>> {
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 DMSCHardwareDiscoveryPlugin>) -> 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<DMSCDevice>> {
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<DMSCDevice>> {
400 let enabled = self.enabled.read().await;
401 let plugins = self.plugins.read().await;
402
403 let mut all_devices = Vec::new();
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::new();
479 search_paths.push(PathBuf::from("./plugins"));
480 search_paths.push(PathBuf::from("/usr/local/lib/dmsc/plugins"));
481 #[cfg(target_os = "macos")]
482 search_paths.push(PathBuf::from("/opt/homebrew/lib/dmsc/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::new();
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::new();
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 DMSCHardwareDiscoveryPlugin>> {
580 tracing::info!("Loading plugin from: {}", path.display());
581
582 if !path.exists() {
583 return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
584 }
585
586 let library = Arc::new(
587 unsafe {
588 Library::new(path)
589 .map_err(|e| PluginError::LibraryLoadFailed(format!(
590 "Failed to load library {}: {}", path.display(), e
591 )))?
592 }
593 );
594
595 let plugin = self.load_plugin_from_library(&library, path)?;
596
597 Ok(plugin)
598 }
599
600 fn load_plugin_from_library(&self, library: &Arc<Library>, path: &PathBuf) -> PluginResult<Box<dyn DMSCHardwareDiscoveryPlugin>> {
602 #[allow(improper_ctypes_definitions)]
603 type CreatePluginFn = unsafe extern "C" fn() -> *mut dyn DMSCHardwareDiscoveryPlugin;
604
605 unsafe {
606 let create_symbol: Symbol<CreatePluginFn> = library
607 .get(b"create_dmsc_plugin")
608 .map_err(|e| PluginError::SymbolResolutionFailed(format!(
609 "Failed to resolve create_dmsc_plugin symbol in {}: {}", path.display(), e
610 )))?;
611
612 let plugin_ptr = create_symbol();
613
614 if plugin_ptr.is_null() {
615 return Err(PluginError::LoadFailed(format!(
616 "create_dmsc_plugin returned null pointer for {}", path.display()
617 )));
618 }
619
620 let plugin = Box::from_raw(plugin_ptr);
621
622 tracing::info!(
623 "Successfully loaded plugin: {} v{}",
624 plugin.metadata().name,
625 plugin.metadata().version
626 );
627
628 Ok(plugin)
629 }
630 }
631
632 pub async fn validate(&self, path: &PathBuf) -> PluginResult<PluginMetadata> {
634 if !path.exists() {
635 return Err(PluginError::LoadFailed(format!("Plugin file not found: {}", path.display())));
636 }
637
638 let library = Arc::new(
639 unsafe {
640 Library::new(path)
641 .map_err(|e| PluginError::LibraryLoadFailed(format!(
642 "Failed to load library {}: {}", path.display(), e
643 )))?
644 }
645 );
646
647 #[allow(improper_ctypes_definitions)]
648 type GetMetadataFn = unsafe extern "C" fn() -> PluginMetadata;
649
650 unsafe {
651 let metadata_symbol: Symbol<GetMetadataFn> = library
652 .get(b"get_dmsc_plugin_metadata")
653 .map_err(|e| PluginError::SymbolResolutionFailed(format!(
654 "Failed to resolve get_dmsc_plugin_metadata symbol in {}: {}", path.display(), e
655 )))?;
656
657 let metadata = metadata_symbol();
658
659 tracing::info!(
660 "Validated plugin: {} v{} (author: {})",
661 metadata.name,
662 metadata.version,
663 metadata.author
664 );
665
666 Ok(metadata)
667 }
668 }
669
670 pub async fn get_api_version(&self, path: &PathBuf) -> PluginResult<u32> {
672 type GetVersionFn = unsafe extern "C" fn() -> u32;
673
674 let library = Arc::new(
675 unsafe {
676 Library::new(path)
677 .map_err(|e| PluginError::LibraryLoadFailed(format!(
678 "Failed to load library {}: {}", path.display(), e
679 )))?
680 }
681 );
682
683 unsafe {
684 let version_symbol: Symbol<GetVersionFn> = library
685 .get(b"get_dmsc_plugin_api_version")
686 .map_err(|e| PluginError::SymbolResolutionFailed(format!(
687 "Failed to resolve get_dmsc_plugin_api_version symbol in {}: {}", path.display(), e
688 )))?;
689
690 let version = version_symbol();
691
692 tracing::debug!("Plugin API version for {}: {}", path.display(), version);
693
694 Ok(version)
695 }
696 }
697
698 pub async fn is_compatible(&self, path: &PathBuf) -> bool {
700 const CURRENT_API_VERSION: u32 = 1;
701
702 match self.get_api_version(path).await {
703 Ok(version) => version == CURRENT_API_VERSION,
704 Err(e) => {
705 tracing::warn!("Failed to check API version for {}: {}", path.display(), e);
706 false
707 }
708 }
709 }
710}
711
712impl Drop for PluginLoader {
713 fn drop(&mut self) {
714 tracing::debug!("PluginLoader dropped");
715 }
716}
717
718pub struct CustomProviderPlugin {
720 name: String,
721 version: String,
722 description: String,
723 discover_func: Arc<dyn Fn() -> Vec<DMSCDevice> + Send + Sync>,
724 status: PluginStatus,
725}
726
727impl CustomProviderPlugin {
728 pub fn new<F>(name: &str, version: &str, description: &str, discover_func: F) -> Self
730 where
731 F: Fn() -> Vec<DMSCDevice> + Send + Sync + 'static,
732 {
733 Self {
734 name: name.to_string(),
735 version: version.to_string(),
736 description: description.to_string(),
737 discover_func: Arc::new(discover_func),
738 status: PluginStatus::Loaded(),
739 }
740 }
741}
742
743#[async_trait]
744impl DMSCHardwareDiscoveryPlugin for CustomProviderPlugin {
745 fn metadata(&self) -> PluginMetadata {
746 PluginMetadata::new(
747 self.name.clone(),
748 self.version.clone(),
749 "User".to_string(),
750 self.description.clone(),
751 )
752 }
753
754 async fn discover(&self, _platform: &PlatformInfo) -> PluginResult<Vec<DMSCDevice>> {
755 let devices = (self.discover_func)();
756 Ok(devices)
757 }
758
759 fn status(&self) -> PluginStatus {
760 self.status.clone()
761 }
762}
763
764pub fn create_custom_plugin<F>(name: &str, version: &str, description: &str, discover_fn: F) -> Box<dyn DMSCHardwareDiscoveryPlugin>
766where
767 F: Fn() -> Vec<DMSCDevice> + Send + Sync + 'static,
768{
769 Box::new(CustomProviderPlugin::new(name, version, description, discover_fn))
770}
771
772use std::collections::{HashMap, HashSet};