dmsc/device/discovery/
plugins.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC 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//! - **DMSCHardwareDiscoveryPlugin**: Trait for plugin implementations
27//! - **PluginRegistry**: Manages registered plugins
28//! - **PluginMetadata**: Plugin information and configuration
29//!
30//! ## Usage
31//!
32//! ```rust,ignore
33//! use dmsc::device::discovery::plugins::{DMSCHardwareDiscoveryPlugin, PluginRegistry};
34//!
35//! // Create a custom plugin
36//! struct MyCustomPlugin;
37//!
38//! #[async_trait::async_trait]
39//! impl DMSCHardwareDiscoveryPlugin for MyCustomPlugin {
40//!     fn name(&self) -> &str { "MyCustomPlugin" }
41//!     fn version(&self) -> &str { "1.0.0" }
42//!     async fn discover(&self) -> Result<Vec<DMSCDevice>, 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::DMSCDevice;
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 DMSCHardwareDiscoveryPlugin: 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<DMSCDevice>>;
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 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/// 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 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    /// 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<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    /// Discovers devices using all enabled plugins
399    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    /// 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::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    /// 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::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    /// 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::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    /// Loads a specific plugin file
579    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    /// Loads a plugin from an already loaded library
601    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    /// Validates a plugin file without loading it
633    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    /// Gets the plugin API version from a library
671    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    /// Checks if a plugin file is compatible with the current DMSC version
699    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
718/// Built-in custom provider plugin for user-defined discovery
719pub 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    /// Creates a new custom provider plugin
729    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
764/// Utility function to create a custom discovery plugin from a closure
765pub 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};