feat: 实现动态插件系统 (6阶段完成)
- 阶段1: 消息类型序列化 (Serialize/Deserialize, &'static str → String) - 阶段2: FFI 边界类型 + Plugin SDK (plugin_abi, showen-plugin-sdk crate) - 阶段3: PluginLoader + DynamicPlugin (libloading 动态加载 .so) - 阶段4: 版本管理 + 错误策略 (VersionManager, PluginState, 自动回退) - 阶段5: 远程仓库客户端 (HTTP 下载 + tar.gz 安装) - 阶段6: 示例插件 + HTTP 管理 API + 全目录 README 文档 54/54 测试通过,0 warnings。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
336
src/core/plugin_loader.rs
Normal file
336
src/core/plugin_loader.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
//! 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<String>,
|
||||
#[serde(default = "default_error_policy")]
|
||||
pub error_policy: ErrorPolicy,
|
||||
pub so_filename: String,
|
||||
}
|
||||
|
||||
fn default_error_policy() -> ErrorPolicy {
|
||||
ErrorPolicy::AutoRollback
|
||||
}
|
||||
|
||||
/// 插件错误处理策略
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[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<String, PluginRegistryEntry>,
|
||||
}
|
||||
|
||||
/// 注册表中每个插件的条目
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginRegistryEntry {
|
||||
pub active_version: String,
|
||||
#[serde(default)]
|
||||
pub last_stable_version: Option<String>,
|
||||
#[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<PathBuf>) -> Self {
|
||||
Self {
|
||||
store_path: store_path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取存储路径
|
||||
pub fn store_path(&self) -> &Path {
|
||||
&self.store_path
|
||||
}
|
||||
|
||||
/// 读取全局注册表
|
||||
pub fn load_registry(&self) -> Result<PluginRegistry> {
|
||||
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<Vec<PluginManifest>> {
|
||||
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;
|
||||
}
|
||||
|
||||
// 跳过非插件文件(如 registry.json)
|
||||
let _plugin_id = match path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(name) if name != "registry.json" => name.to_string(),
|
||||
_ => 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<PluginManifest> {
|
||||
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)?;
|
||||
|
||||
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<Vec<String>> {
|
||||
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 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(),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user