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:
showen
2026-03-13 03:38:08 +08:00
parent 5dcc1ad98e
commit 7135f28545
62 changed files with 3501 additions and 299 deletions

336
src/core/plugin_loader.rs Normal file
View 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(&registry_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(&registry_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(&registry).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);
}
}