//! PluginLoader — 扫描 plugin_store/ 目录,发现并加载动态插件 //! //! 目录结构: //! ```text //! plugin_store/ //! ├── registry.json //! └── custom-sensor/ //! ├── 1.0.0/ //! │ ├── manifest.json //! │ └── libcustom_sensor.so //! └── 1.1.0/ //! ├── manifest.json //! └── libcustom_sensor.so //! ``` use crate::core::dynamic_plugin::DynamicPlugin; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; /// 插件清单 (manifest.json) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginManifest { pub id: String, pub version: String, pub sdk_version: String, #[serde(default)] pub dependencies: Vec, #[serde(default = "default_error_policy")] pub error_policy: ErrorPolicy, pub so_filename: String, /// 插件声明支持的功能列表 #[serde(default)] pub capabilities: Vec, /// 挂载时必须通过测试的功能(capabilities 的子集) #[serde(default)] pub required_capabilities: Vec, /// 自测超时(毫秒),默认 5000 #[serde(default = "default_test_timeout")] pub test_timeout_ms: u64, /// 是否在挂载时自动运行自测,默认 true #[serde(default = "default_true")] pub auto_test: bool, } fn default_error_policy() -> ErrorPolicy { ErrorPolicy::AutoRollback } fn default_test_timeout() -> u64 { 5000 } /// 插件错误处理策略 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ErrorPolicy { /// 自动回退到上一个稳定版本 AutoRollback, /// 禁用插件并记录日志 DisableAndLog, } /// 全局注册表 (registry.json) #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct PluginRegistry { #[serde(default)] pub plugins: std::collections::HashMap, } /// 注册表中每个插件的条目 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginRegistryEntry { pub active_version: String, #[serde(default)] pub last_stable_version: Option, #[serde(default = "default_true")] pub enabled: bool, #[serde(default = "default_error_policy")] pub error_policy: ErrorPolicy, #[serde(default = "default_max_errors")] pub max_errors: u32, } fn default_true() -> bool { true } fn default_max_errors() -> u32 { 5 } /// 插件加载器 pub struct PluginLoader { store_path: PathBuf, } impl PluginLoader { pub fn new(store_path: impl Into) -> Self { Self { store_path: store_path.into(), } } /// 获取存储路径 pub fn store_path(&self) -> &Path { &self.store_path } /// 读取全局注册表 pub fn load_registry(&self) -> Result { let registry_path = self.store_path.join("registry.json"); if !registry_path.exists() { return Ok(PluginRegistry::default()); } let content = std::fs::read_to_string(®istry_path) .with_context(|| format!("failed to read {}", registry_path.display()))?; serde_json::from_str(&content) .with_context(|| format!("failed to parse {}", registry_path.display())) } /// 保存全局注册表 pub fn save_registry(&self, registry: &PluginRegistry) -> Result<()> { let registry_path = self.store_path.join("registry.json"); let content = serde_json::to_string_pretty(registry).context("failed to serialize registry")?; std::fs::write(®istry_path, content) .with_context(|| format!("failed to write {}", registry_path.display())) } /// 发现所有可用插件(扫描目录) pub fn discover_plugins(&self) -> Result> { let mut manifests = Vec::new(); if !self.store_path.exists() { return Ok(manifests); } for entry in std::fs::read_dir(&self.store_path) .with_context(|| format!("failed to read {}", self.store_path.display()))? { let entry = entry?; let path = entry.path(); if !path.is_dir() { continue; } // Skip non-plugin entries (e.g. registry.json) match path.file_name().and_then(|n| n.to_str()) { Some(name) if name != "registry.json" => {} _ => continue, } // 扫描版本子目录 for version_entry in std::fs::read_dir(&path)? { let version_entry = version_entry?; let version_path = version_entry.path(); if !version_path.is_dir() { continue; } let manifest_path = version_path.join("manifest.json"); if manifest_path.exists() { match self.read_manifest(&manifest_path) { Ok(manifest) => manifests.push(manifest), Err(e) => { eprintln!( "[PluginLoader] 跳过无效清单 {}: {e}", manifest_path.display() ); } } } } } Ok(manifests) } /// 读取插件清单 fn read_manifest(&self, path: &Path) -> Result { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read {}", path.display()))?; serde_json::from_str(&content) .with_context(|| format!("failed to parse {}", path.display())) } /// 加载指定插件 /// version: None 表示使用注册表中的 active_version pub fn load_plugin( &self, plugin_id: &str, version: Option<&str>, ) -> Result<(DynamicPlugin, PluginManifest)> { let version = match version { Some(v) => v.to_string(), None => { let registry = self.load_registry()?; registry .plugins .get(plugin_id) .map(|e| e.active_version.clone()) .ok_or_else(|| anyhow!("plugin '{plugin_id}' not found in registry"))? } }; let version_dir = self.store_path.join(plugin_id).join(&version); let manifest_path = version_dir.join("manifest.json"); let manifest = self.read_manifest(&manifest_path)?; if manifest.id != plugin_id { return Err(anyhow!( "plugin manifest id mismatch: requested '{plugin_id}', found '{}'", manifest.id )); } if manifest.version != version { return Err(anyhow!( "plugin manifest version mismatch: requested '{version}', found '{}'", manifest.version )); } let so_path = version_dir.join(&manifest.so_filename); if !so_path.exists() { return Err(anyhow!("plugin .so not found: {}", so_path.display())); } let so_path_str = so_path.to_string_lossy().to_string(); let mut plugin = unsafe { DynamicPlugin::load(&so_path_str, manifest.dependencies.clone())? }; plugin.set_id(manifest.id.clone()); Ok((plugin, manifest)) } /// 列出插件的所有已安装版本 pub fn list_versions(&self, plugin_id: &str) -> Result> { let plugin_dir = self.store_path.join(plugin_id); if !plugin_dir.exists() { return Ok(vec![]); } let mut versions = Vec::new(); for entry in std::fs::read_dir(&plugin_dir)? { let entry = entry?; if entry.path().is_dir() { if let Some(name) = entry.file_name().to_str() { versions.push(name.to_string()); } } } versions.sort(); Ok(versions) } } #[cfg(test)] mod tests { use super::*; use std::fs; fn unique_test_dir(name: &str) -> std::path::PathBuf { let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system time should be after unix epoch") .as_nanos(); std::env::temp_dir().join(format!("showen_plugin_loader_{name}_{nanos}")) } fn setup_test_store(base: &Path) { let plugin_dir = base.join("test-plugin").join("1.0.0"); fs::create_dir_all(&plugin_dir).unwrap(); let manifest = PluginManifest { id: "test-plugin".to_string(), version: "1.0.0".to_string(), sdk_version: "0.2.0".to_string(), dependencies: vec![], error_policy: ErrorPolicy::AutoRollback, so_filename: "libtest_plugin.so".to_string(), capabilities: vec![], required_capabilities: vec![], test_timeout_ms: 5000, auto_test: true, }; fs::write( plugin_dir.join("manifest.json"), serde_json::to_string_pretty(&manifest).unwrap(), ) .unwrap(); } #[test] fn discover_plugins_finds_manifests() { let tmp = std::env::temp_dir().join("showen_test_discover"); let _ = fs::remove_dir_all(&tmp); fs::create_dir_all(&tmp).unwrap(); setup_test_store(&tmp); let loader = PluginLoader::new(&tmp); let manifests = loader.discover_plugins().unwrap(); assert_eq!(manifests.len(), 1); assert_eq!(manifests[0].id, "test-plugin"); assert_eq!(manifests[0].version, "1.0.0"); let _ = fs::remove_dir_all(&tmp); } #[test] fn empty_store_returns_no_plugins() { let tmp = std::env::temp_dir().join("showen_test_empty"); let _ = fs::remove_dir_all(&tmp); let loader = PluginLoader::new(&tmp); let manifests = loader.discover_plugins().unwrap(); assert!(manifests.is_empty()); } #[test] fn registry_round_trip() { let tmp = std::env::temp_dir().join("showen_test_registry"); let _ = fs::remove_dir_all(&tmp); fs::create_dir_all(&tmp).unwrap(); let loader = PluginLoader::new(&tmp); let mut registry = PluginRegistry::default(); registry.plugins.insert( "test-plugin".to_string(), PluginRegistryEntry { active_version: "1.0.0".to_string(), last_stable_version: None, enabled: true, error_policy: ErrorPolicy::AutoRollback, max_errors: 5, }, ); loader.save_registry(®istry).unwrap(); let loaded = loader.load_registry().unwrap(); assert_eq!(loaded.plugins.len(), 1); let entry = &loaded.plugins["test-plugin"]; assert_eq!(entry.active_version, "1.0.0"); assert!(entry.enabled); let _ = fs::remove_dir_all(&tmp); } #[test] fn list_versions_returns_sorted() { let tmp = std::env::temp_dir().join("showen_test_versions"); let _ = fs::remove_dir_all(&tmp); let plugin_dir = tmp.join("my-plugin"); fs::create_dir_all(plugin_dir.join("1.1.0")).unwrap(); fs::create_dir_all(plugin_dir.join("1.0.0")).unwrap(); fs::create_dir_all(plugin_dir.join("2.0.0")).unwrap(); let loader = PluginLoader::new(&tmp); let versions = loader.list_versions("my-plugin").unwrap(); assert_eq!(versions, vec!["1.0.0", "1.1.0", "2.0.0"]); let _ = fs::remove_dir_all(&tmp); } #[test] fn manifest_parses_capabilities() { let json = r#"{ "id": "sensor", "version": "1.0.0", "sdk_version": "0.2.0", "so_filename": "libsensor.so", "capabilities": ["temperature", "humidity"], "required_capabilities": ["temperature"], "test_timeout_ms": 3000, "auto_test": true }"#; let manifest: PluginManifest = serde_json::from_str(json).unwrap(); assert_eq!(manifest.capabilities, vec!["temperature", "humidity"]); assert_eq!(manifest.required_capabilities, vec!["temperature"]); assert_eq!(manifest.test_timeout_ms, 3000); assert!(manifest.auto_test); } #[test] fn manifest_capabilities_default_empty() { let json = r#"{ "id": "basic", "version": "1.0.0", "sdk_version": "0.2.0", "so_filename": "libbasic.so" }"#; let manifest: PluginManifest = serde_json::from_str(json).unwrap(); assert!(manifest.capabilities.is_empty()); assert!(manifest.required_capabilities.is_empty()); assert_eq!(manifest.test_timeout_ms, 5000); assert!(manifest.auto_test); } #[test] fn manifest_test_timeout_ms_is_configurable_from_manifest() { let json = r#"{ "id": "timed-plugin", "version": "1.0.0", "sdk_version": "0.2.0", "so_filename": "libtimed_plugin.so", "test_timeout_ms": 12000 }"#; let manifest: PluginManifest = serde_json::from_str(json).unwrap(); assert_eq!(manifest.test_timeout_ms, 12000); } #[test] fn load_plugin_rejects_manifest_id_mismatch() { let tmp = unique_test_dir("id_mismatch"); let plugin_dir = tmp.join("expected-plugin").join("1.0.0"); fs::create_dir_all(&plugin_dir).unwrap(); fs::write( plugin_dir.join("manifest.json"), r#"{ "id": "other-plugin", "version": "1.0.0", "sdk_version": "0.2.0", "so_filename": "libexpected_plugin.so" }"#, ) .unwrap(); let loader = PluginLoader::new(&tmp); let error = match loader.load_plugin("expected-plugin", Some("1.0.0")) { Ok(_) => panic!("id mismatch should be rejected"), Err(error) => error, }; assert!(error.to_string().contains( "plugin manifest id mismatch: requested 'expected-plugin', found 'other-plugin'" )); let _ = fs::remove_dir_all(&tmp); } #[test] fn load_plugin_rejects_manifest_version_mismatch() { let tmp = unique_test_dir("version_mismatch"); let plugin_dir = tmp.join("expected-plugin").join("1.0.0"); fs::create_dir_all(&plugin_dir).unwrap(); fs::write( plugin_dir.join("manifest.json"), r#"{ "id": "expected-plugin", "version": "2.0.0", "sdk_version": "0.2.0", "so_filename": "libexpected_plugin.so" }"#, ) .unwrap(); let loader = PluginLoader::new(&tmp); let error = match loader.load_plugin("expected-plugin", Some("1.0.0")) { Ok(_) => panic!("version mismatch should be rejected"), Err(error) => error, }; assert!(error .to_string() .contains("plugin manifest version mismatch: requested '1.0.0', found '2.0.0'")); let _ = fs::remove_dir_all(&tmp); } }