feat: 插件自动挂载测试机制 — capabilities + self_test + 3阶段启动

- Plugin trait 增加 capabilities() 和 self_test() 方法
- PluginVTable 增加 get_capabilities 和 self_test FFI
- ServiceManager 三阶段启动: init → self_test → start
- SendCallback 改为 ctx 参数传递,消除 thread_local
- export_plugin! 宏所有 FFI 函数包裹 catch_unwind
- PluginManifest 增加 capabilities/required_capabilities/auto_test
- 新增 3 个自测相关测试用例 (共 59 测试)
This commit is contained in:
showen
2026-03-13 04:31:39 +08:00
parent 1863efb0f5
commit 99ee78984c
9 changed files with 694 additions and 123 deletions

View File

@@ -18,6 +18,14 @@ pub struct PluginInfo {
pub platform: String,
}
/// 单项能力测试结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityTestResult {
pub capability: String,
pub passed: bool,
pub message: String,
}
/// 消息信封
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
@@ -40,17 +48,29 @@ pub enum Destination {
pub enum Message {
PlayerCommand(serde_json::Value),
PlayerStatus(serde_json::Value),
Trigger { name: String, value: String },
StateChanged { old_state: String, new_state: String },
Trigger {
name: String,
value: String,
},
StateChanged {
old_state: String,
new_state: String,
},
ScreenLockRequest(bool),
CursorVisibility(bool),
WifiCommand(serde_json::Value),
WifiResult(String),
WifiProvisioned { ssid: String, ip: String },
WifiProvisioned {
ssid: String,
ip: String,
},
ConfigReloadRequest,
Shutdown,
PluginReady(String),
Custom { kind: String, payload: String },
Custom {
kind: String,
payload: String,
},
}
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
@@ -108,36 +128,45 @@ impl FfiResult {
}
}
pub type SendCallback = unsafe extern "C" fn(envelope_json: FfiStr);
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
#[repr(C)]
pub struct PluginVTable {
pub create: unsafe extern "C" fn() -> PluginHandle,
pub get_info: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
pub init: unsafe extern "C" fn(handle: PluginHandle, config_json: FfiStr, send_cb: SendCallback) -> FfiResult,
pub init: unsafe extern "C" fn(
handle: PluginHandle,
config_json: FfiStr,
send_ctx: *mut c_void,
send_cb: SendCallback,
) -> FfiResult,
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub handle_message: unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub handle_message:
unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
}
// ── 高级接口:插件作者实现此 trait ──
/// 消息发送器 — 封装 SendCallback提供安全的 Rust API
pub struct MessageSender {
ctx: *mut c_void,
cb: SendCallback,
}
impl MessageSender {
pub fn new(cb: SendCallback) -> Self {
Self { cb }
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
Self { ctx, cb }
}
/// 发送消息信封到主程序
pub fn send(&self, envelope: &Envelope) {
if let Ok(json) = serde_json::to_string(envelope) {
if let Ok(cstr) = CString::new(json) {
unsafe { (self.cb)(cstr.as_ptr()) };
unsafe { (self.cb)(self.ctx, cstr.as_ptr()) };
}
}
}
@@ -179,6 +208,24 @@ pub trait ShowenPlugin: Send {
/// 插件信息
fn info(&self) -> PluginInfo;
/// 声明插件支持的功能列表(默认空)
fn capabilities(&self) -> Vec<String> {
vec![]
}
/// 运行自测,返回每项功能的测试结果
/// 默认实现:所有声明的 capability 均标记为通过
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
self.capabilities()
.into_iter()
.map(|c| CapabilityTestResult {
capability: c,
passed: true,
message: "no test defined".into(),
})
.collect()
}
/// 初始化,收到配置 JSON 和消息发送器
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
@@ -206,49 +253,82 @@ pub trait ShowenPlugin: Send {
#[macro_export]
macro_rules! export_plugin {
($plugin_type:ty, $constructor:expr) => {
fn __showen_panic_error(payload: Box<dyn std::any::Any + Send>) -> String {
let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
format!("plugin panicked: {}", msg)
}
unsafe extern "C" fn __showen_create() -> $crate::PluginHandle {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin: Box<$plugin_type> = Box::new($constructor);
Box::into_raw(plugin) as $crate::PluginHandle
})) {
Ok(handle) => handle,
Err(_) => std::ptr::null_mut(),
}
}
unsafe extern "C" fn __showen_get_info(handle: $crate::PluginHandle) -> $crate::FfiString {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &*(handle as *const $plugin_type) };
let info = <$plugin_type as $crate::ShowenPlugin>::info(plugin);
match serde_json::to_string(&info) {
Ok(json) => $crate::FfiString::from_string(json),
Err(_) => $crate::FfiString::null(),
}
})) {
Ok(info) => info,
Err(_) => $crate::FfiString::null(),
}
}
unsafe extern "C" fn __showen_init(
handle: $crate::PluginHandle,
config_json: $crate::FfiStr,
send_ctx: *mut std::ffi::c_void,
send_cb: $crate::SendCallback,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
let config = match unsafe { std::ffi::CStr::from_ptr(config_json) }.to_str() {
Ok(s) => s,
Err(e) => return $crate::FfiResult::err(format!("invalid config UTF-8: {e}")),
};
let sender = $crate::MessageSender::new(send_cb);
let sender = $crate::MessageSender::new(send_ctx, send_cb);
match <$plugin_type as $crate::ShowenPlugin>::init(plugin, config, sender) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
})) {
Ok(result) => result,
Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)),
}
}
unsafe extern "C" fn __showen_start(handle: $crate::PluginHandle) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
match <$plugin_type as $crate::ShowenPlugin>::start(plugin) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
})) {
Ok(result) => result,
Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)),
}
}
unsafe extern "C" fn __showen_handle_message(
handle: $crate::PluginHandle,
message_json: $crate::FfiStr,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
let json_str = match unsafe { std::ffi::CStr::from_ptr(message_json) }.to_str() {
Ok(s) => s,
@@ -262,20 +342,63 @@ macro_rules! export_plugin {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
})) {
Ok(result) => result,
Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)),
}
}
unsafe extern "C" fn __showen_stop(handle: $crate::PluginHandle) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
match <$plugin_type as $crate::ShowenPlugin>::stop(plugin) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
})) {
Ok(result) => result,
Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)),
}
}
unsafe extern "C" fn __showen_destroy(handle: $crate::PluginHandle) {
if let Ok(()) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if !handle.is_null() {
drop(unsafe { Box::from_raw(handle as *mut $plugin_type) });
}
})) {
let _ = ();
}
}
unsafe extern "C" fn __showen_get_capabilities(
handle: $crate::PluginHandle,
) -> $crate::FfiString {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &*(handle as *const $plugin_type) };
let caps = <$plugin_type as $crate::ShowenPlugin>::capabilities(plugin);
match serde_json::to_string(&caps) {
Ok(json) => $crate::FfiString::from_string(json),
Err(_) => $crate::FfiString::from_string("[]".to_string()),
}
})) {
Ok(caps) => caps,
Err(_) => $crate::FfiString::null(),
}
}
unsafe extern "C" fn __showen_self_test(handle: $crate::PluginHandle) -> $crate::FfiString {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
let results = <$plugin_type as $crate::ShowenPlugin>::self_test(plugin);
match serde_json::to_string(&results) {
Ok(json) => $crate::FfiString::from_string(json),
Err(_) => $crate::FfiString::from_string("[]".to_string()),
}
})) {
Ok(results) => results,
Err(_) => $crate::FfiString::null(),
}
}
#[no_mangle]
@@ -287,6 +410,8 @@ macro_rules! export_plugin {
handle_message: __showen_handle_message,
stop: __showen_stop,
destroy: __showen_destroy,
get_capabilities: __showen_get_capabilities,
self_test: __showen_self_test,
};
};
}

View File

@@ -1,18 +1,23 @@
//! 示例动态插件 — 展示如何使用 showen-plugin-sdk 编写插件
//!
//! 此插件仅打印日志,用于验证动态加载流程。
//! 此插件演示动态加载流程及自测机制
use showen_plugin_sdk::{
export_plugin, Message, MessageSender, PluginInfo, ShowenPlugin,
export_plugin, CapabilityTestResult, Message, MessageSender, PluginInfo, ShowenPlugin,
};
pub struct ExamplePlugin {
sender: Option<MessageSender>,
/// 用于演示可配置的自测失败
fail_optional_test: bool,
}
impl ExamplePlugin {
pub fn new() -> Self {
Self { sender: None }
Self {
sender: None,
fail_optional_test: false,
}
}
}
@@ -26,6 +31,29 @@ impl ShowenPlugin for ExamplePlugin {
}
}
fn capabilities(&self) -> Vec<String> {
vec!["logging".to_string(), "metrics".to_string()]
}
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
vec![
CapabilityTestResult {
capability: "logging".to_string(),
passed: true,
message: "log output verified".to_string(),
},
CapabilityTestResult {
capability: "metrics".to_string(),
passed: !self.fail_optional_test,
message: if self.fail_optional_test {
"metrics backend unreachable".to_string()
} else {
"metrics endpoint ok".to_string()
},
},
]
}
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> {
eprintln!("[ExamplePlugin] init called, config length: {}", config_json.len());
self.sender = Some(sender);

View File

@@ -4,10 +4,9 @@
//! 对 ServiceManager 而言DynamicPlugin 与静态插件无区别。
use crate::core::message::{Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext, PluginInfo};
use crate::core::plugin::{CapabilityTestResult, Plugin, PluginContext, PluginInfo};
use crate::core::plugin_abi::{
ffi_str_to_str, FfiResult, FfiString, PluginHandle, PluginVTable,
PLUGIN_VTABLE_SYMBOL,
ffi_str_to_str, FfiResult, FfiString, PluginHandle, PluginVTable, PLUGIN_VTABLE_SYMBOL,
};
use anyhow::{anyhow, Context, Result};
use libloading::Library;
@@ -30,6 +29,8 @@ pub struct DynamicPlugin {
dependencies: Vec<String>,
/// .so 文件路径(用于调试/日志)
so_path: String,
/// Sender 上下文指针(堆分配的 mpsc::Sender
sender_ctx: *mut std::ffi::c_void,
}
// PluginHandle 是 *mut c_void需要手动声明 Send
@@ -41,10 +42,7 @@ impl DynamicPlugin {
///
/// # Safety
/// .so 文件必须是由 showen-plugin-sdk 编译的合法插件
pub unsafe fn load(
so_path: &str,
dependencies: Vec<String>,
) -> Result<Self> {
pub unsafe fn load(so_path: &str, dependencies: Vec<String>) -> Result<Self> {
let library = unsafe {
Library::new(so_path)
.with_context(|| format!("failed to load plugin .so: {so_path}"))?
@@ -54,9 +52,7 @@ impl DynamicPlugin {
let vtable: &'static PluginVTable = unsafe {
let symbol = library
.get::<*const PluginVTable>(PLUGIN_VTABLE_SYMBOL)
.with_context(|| {
format!("symbol 'showen_plugin_vtable' not found in {so_path}")
})?;
.with_context(|| format!("symbol 'showen_plugin_vtable' not found in {so_path}"))?;
&**symbol
};
@@ -83,6 +79,7 @@ impl DynamicPlugin {
id,
dependencies,
so_path: so_path.to_string(),
sender_ctx: std::ptr::null_mut(),
})
}
@@ -98,14 +95,8 @@ impl DynamicPlugin {
/// 将 FfiResult 转为 anyhow::Result
unsafe fn check_result(&self, result: FfiResult, operation: &str) -> Result<()> {
unsafe { result.into_result() }.map_err(|e| {
anyhow!(
"plugin '{}' {} failed: {}",
self.id,
operation,
e
)
})
unsafe { result.into_result() }
.map_err(|e| anyhow!("plugin '{}' {} failed: {}", self.id, operation, e))
}
}
@@ -122,21 +113,40 @@ impl Plugin for DynamicPlugin {
self.dependencies.clone()
}
fn capabilities(&self) -> Vec<String> {
let ffi_str: FfiString = unsafe { (self.vtable.get_capabilities)(self.handle) };
let json = match unsafe { ffi_str.into_string() } {
Some(s) => s,
None => return vec![],
};
serde_json::from_str(&json).unwrap_or_default()
}
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
let ffi_str: FfiString = unsafe { (self.vtable.self_test)(self.handle) };
let json = match unsafe { ffi_str.into_string() } {
Some(s) => s,
None => return vec![],
};
serde_json::from_str(&json).unwrap_or_default()
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
// 序列化配置为 JSON
let config_json = serde_json::to_string(ctx.config.as_ref())
.context("failed to serialize config for dynamic plugin")?;
let config_cstr = CString::new(config_json)
.context("config JSON contains null byte")?;
let config_cstr = CString::new(config_json).context("config JSON contains null byte")?;
// 创建 SendCallback — 将 mpsc::Sender 转为 C 函数指针
// 使用 thread_local 存储 sender每次 init 更新)
PLUGIN_SENDER.with(|cell| {
*cell.borrow_mut() = Some(ctx.tx);
});
// Sender 分配到堆上,生命周期由 DynamicPlugin 管理
let sender_box = Box::new(ctx.tx);
self.sender_ctx = Box::into_raw(sender_box) as *mut std::ffi::c_void;
let result = unsafe {
(self.vtable.init)(self.handle, config_cstr.as_ptr(), ffi_send_callback)
(self.vtable.init)(
self.handle,
config_cstr.as_ptr(),
self.sender_ctx,
ffi_send_callback,
)
};
unsafe { self.check_result(result, "init") }
}
@@ -149,12 +159,9 @@ impl Plugin for DynamicPlugin {
fn handle_message(&mut self, msg: Message) -> Result<()> {
let msg_json = serde_json::to_string(&msg)
.context("failed to serialize message for dynamic plugin")?;
let msg_cstr = CString::new(msg_json)
.context("message JSON contains null byte")?;
let msg_cstr = CString::new(msg_json).context("message JSON contains null byte")?;
let result = unsafe {
(self.vtable.handle_message)(self.handle, msg_cstr.as_ptr())
};
let result = unsafe { (self.vtable.handle_message)(self.handle, msg_cstr.as_ptr()) };
unsafe { self.check_result(result, "handle_message") }
}
@@ -169,18 +176,29 @@ impl Drop for DynamicPlugin {
if !self.handle.is_null() {
unsafe { (self.vtable.destroy)(self.handle) };
}
if !self.sender_ctx.is_null() {
unsafe {
drop(Box::from_raw(
self.sender_ctx as *mut mpsc::Sender<Envelope>,
));
}
self.sender_ctx = std::ptr::null_mut();
}
}
}
// ── SendCallback 实现 ──
thread_local! {
static PLUGIN_SENDER: std::cell::RefCell<Option<mpsc::Sender<Envelope>>> =
std::cell::RefCell::new(None);
/// C FFI 回调:插件调用此函数向主程序发消息
unsafe extern "C" fn ffi_send_callback(
ctx: *mut std::ffi::c_void,
envelope_json: crate::core::plugin_abi::FfiStr,
) {
if ctx.is_null() {
eprintln!("[DynamicPlugin] send callback received null sender ctx");
return;
}
/// C FFI 回调:插件调用此函数向主程序发消息
unsafe extern "C" fn ffi_send_callback(envelope_json: crate::core::plugin_abi::FfiStr) {
let json_str = match unsafe { ffi_str_to_str(envelope_json) } {
Some(s) => s,
None => {
@@ -197,11 +215,8 @@ unsafe extern "C" fn ffi_send_callback(envelope_json: crate::core::plugin_abi::F
}
};
PLUGIN_SENDER.with(|cell| {
if let Some(tx) = cell.borrow().as_ref() {
let tx = unsafe { &*(ctx as *const mpsc::Sender<Envelope>) };
if let Err(e) = tx.send(envelope) {
eprintln!("[DynamicPlugin] failed to send envelope: {e}");
}
}
});
}

View File

@@ -4,6 +4,14 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::sync::{mpsc, Arc};
/// 单项能力测试结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityTestResult {
pub capability: String,
pub passed: bool,
pub message: String,
}
/// 所有功能都通过实现此 trait 接入系统
pub trait Plugin: Send {
/// 唯一标识 (如 "video", "http", "ble")
@@ -17,6 +25,24 @@ pub trait Plugin: Send {
vec![]
}
/// 声明插件支持的功能列表(默认空)
fn capabilities(&self) -> Vec<String> {
vec![]
}
/// 运行自测,返回每项功能的测试结果
/// 默认实现:所有声明的 capability 均标记为通过
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
self.capabilities()
.into_iter()
.map(|c| CapabilityTestResult {
capability: c,
passed: true,
message: "no test defined".into(),
})
.collect()
}
/// 初始化:获取发送通道,声明订阅的消息类型
fn init(&mut self, ctx: PluginContext) -> Result<()>;

View File

@@ -97,7 +97,7 @@ impl FfiResult {
/// 插件向主程序发消息的回调函数类型
/// envelope_json: JSON 序列化的 Envelope
pub type SendCallback = unsafe extern "C" fn(envelope_json: FfiStr);
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
/// 插件虚函数表 — 每个动态插件导出一个此结构体
#[repr(C)]
@@ -111,20 +111,32 @@ pub struct PluginVTable {
/// 初始化插件
/// config_json: 完整的 AppConfig JSON
/// send_cb: 发送消息的回调
pub init: unsafe extern "C" fn(handle: PluginHandle, config_json: FfiStr, send_cb: SendCallback) -> FfiResult,
pub init: unsafe extern "C" fn(
handle: PluginHandle,
config_json: FfiStr,
send_ctx: *mut c_void,
send_cb: SendCallback,
) -> FfiResult,
/// 启动插件
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
/// 处理消息
/// message_json: JSON 序列化的 Message
pub handle_message: unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub handle_message:
unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
/// 停止插件
pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
/// 销毁插件实例,释放资源
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
/// 获取功能列表 (返回 JSON: Vec<String>)
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
/// 运行自测 (返回 JSON: Vec<CapabilityTestResult>)
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
}
/// 动态插件 .so 中导出的符号名称

View File

@@ -29,12 +29,28 @@ pub struct PluginManifest {
#[serde(default = "default_error_policy")]
pub error_policy: ErrorPolicy,
pub so_filename: String,
/// 插件声明支持的功能列表
#[serde(default)]
pub capabilities: Vec<String>,
/// 挂载时必须通过测试的功能capabilities 的子集)
#[serde(default)]
pub required_capabilities: Vec<String>,
/// 自测超时(毫秒),默认 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, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
@@ -250,6 +266,10 @@ mod tests {
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(
@@ -333,4 +353,38 @@ mod tests {
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);
}
}

View File

@@ -1,6 +1,6 @@
use crate::core::config::AppConfig;
use crate::core::message::{Destination, Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext};
use crate::core::plugin::{CapabilityTestResult, Plugin, PluginContext};
use crate::core::plugin_loader::ErrorPolicy;
use anyhow::{anyhow, Result};
use std::collections::{HashMap, HashSet};
@@ -19,6 +19,14 @@ struct PluginState {
max_errors: u32,
/// 是否启用
enabled: bool,
/// 挂载时的自测结果
test_results: Vec<CapabilityTestResult>,
/// 声明的功能列表
capabilities: Vec<String>,
/// manifest 中声明的必须通过的功能
required_capabilities: Vec<String>,
/// 是否自动测试
auto_test: bool,
}
impl PluginState {
@@ -30,14 +38,14 @@ impl PluginState {
error_count: 0,
max_errors: u32::MAX, // 静态插件不自动禁用
enabled: true,
test_results: vec![],
capabilities: vec![],
required_capabilities: vec![],
auto_test: false, // 静态插件默认不自测
}
}
fn new_dynamic(
plugin: Box<dyn Plugin>,
error_policy: ErrorPolicy,
max_errors: u32,
) -> Self {
fn new_dynamic(plugin: Box<dyn Plugin>, error_policy: ErrorPolicy, max_errors: u32) -> Self {
Self {
plugin,
is_dynamic: true,
@@ -45,6 +53,10 @@ impl PluginState {
error_count: 0,
max_errors,
enabled: true,
test_results: vec![],
capabilities: vec![],
required_capabilities: vec![],
auto_test: true,
}
}
@@ -97,6 +109,19 @@ impl ServiceManager {
plugin: Box<dyn Plugin>,
error_policy: ErrorPolicy,
max_errors: u32,
) {
self.register_dynamic_with_manifest(plugin, error_policy, max_errors, vec![], vec![], true);
}
/// 注册动态插件(带 manifest 自测信息)
pub fn register_dynamic_with_manifest(
&mut self,
plugin: Box<dyn Plugin>,
error_policy: ErrorPolicy,
max_errors: u32,
required_capabilities: Vec<String>,
capabilities: Vec<String>,
auto_test: bool,
) {
println!(
"[ServiceManager] 注册动态插件: {} (策略: {:?}, 最大错误: {})",
@@ -104,16 +129,19 @@ impl ServiceManager {
error_policy,
max_errors
);
self.plugins
.push(PluginState::new_dynamic(plugin, error_policy, max_errors));
let mut state = PluginState::new_dynamic(plugin, error_policy, max_errors);
state.required_capabilities = required_capabilities;
state.capabilities = capabilities;
state.auto_test = auto_test;
self.plugins.push(state);
}
/// 按注册顺序 init() + start() 所有插件
/// 动态插件 init/start 失败时按策略处理,不中断其他插件
/// 按注册顺序 init() → self_test() → start() 所有插件
/// 动态插件 init/start/test 失败时按策略处理,不中断其他插件
pub fn start_all(&mut self) -> Result<()> {
self.validate_and_sort_plugins()?;
// init
// Phase 1: init
for state in &mut self.plugins {
let ctx = PluginContext {
tx: self.tx.clone(),
@@ -135,7 +163,91 @@ impl ServiceManager {
}
}
// start
// Phase 2: self_test (init 之后, start 之前)
for state in &mut self.plugins {
if !state.enabled || !state.auto_test {
continue;
}
// 获取插件的功能列表并运行自测
let caps = state.plugin.capabilities();
if caps.is_empty() && state.required_capabilities.is_empty() {
// 无功能声明 → 跳过自测
continue;
}
println!(
"[ServiceManager] 自测插件: {} (功能: {:?})",
state.id(),
caps
);
state.capabilities = caps;
let results = state.plugin.self_test();
// 检查 required_capabilities 中的项是否全部通过
let mut has_required_failure = false;
let passed_caps: std::collections::HashSet<&str> = results
.iter()
.filter(|r| r.passed)
.map(|r| r.capability.as_str())
.collect();
// 检查每个 required capability 是否出现在通过列表中
for req in &state.required_capabilities {
if !passed_caps.contains(req.as_str()) {
eprintln!(
"[ServiceManager] ✗ [必须] {}{}",
req,
results
.iter()
.find(|r| r.capability == *req)
.map(|r| r.message.as_str())
.unwrap_or("未在测试结果中出现")
);
has_required_failure = true;
}
}
for result in &results {
if result.passed {
println!(
"[ServiceManager] ✓ {}{}",
result.capability, result.message
);
} else if !state.required_capabilities.contains(&result.capability) {
eprintln!(
"[ServiceManager] ✗ [可选] {}{}",
result.capability, result.message
);
}
}
state.test_results = results;
if has_required_failure {
if state.is_dynamic {
match &state.error_policy {
ErrorPolicy::AutoRollback => {
eprintln!(
"[ServiceManager] 动态插件 '{}' 必须能力自测失败,禁用 (待回退)",
state.id()
);
}
ErrorPolicy::DisableAndLog => {
eprintln!(
"[ServiceManager] 动态插件 '{}' 必须能力自测失败,禁用",
state.id()
);
}
}
state.enabled = false;
} else {
return Err(anyhow!("静态插件 '{}' 必须能力自测失败", state.id()));
}
}
}
// Phase 3: start
for state in &mut self.plugins {
if !state.enabled {
continue;
@@ -198,11 +310,7 @@ impl ServiceManager {
}
println!("[ServiceManager] 停止插件: {}", state.id());
if let Err(e) = state.plugin.stop() {
eprintln!(
"[ServiceManager] 停止插件 '{}' 失败: {}",
state.id(),
e
);
eprintln!("[ServiceManager] 停止插件 '{}' 失败: {}", state.id(), e);
}
}
Ok(())
@@ -241,6 +349,8 @@ impl ServiceManager {
error_count: s.error_count,
max_errors: s.max_errors,
enabled: s.enabled,
test_results: s.test_results.clone(),
capabilities: s.capabilities.clone(),
})
.collect()
}
@@ -259,15 +369,13 @@ impl ServiceManager {
.position(|s| s.id() == plugin_id)
.ok_or_else(|| anyhow!("plugin '{plugin_id}' not found for replacement"))?;
// Stop old plugin
if self.plugins[idx].enabled {
let _ = self.plugins[idx].plugin.stop();
if !self.plugins[idx].is_dynamic {
return Err(anyhow!(
"plugin '{plugin_id}' is not dynamic and cannot be replaced"
));
}
// Replace
let mut new_state = PluginState::new_dynamic(new_plugin, error_policy, max_errors);
// Init new plugin
let ctx = PluginContext {
tx: self.tx.clone(),
config: Arc::clone(&self.config),
@@ -275,6 +383,10 @@ impl ServiceManager {
new_state.plugin.init(ctx)?;
new_state.plugin.start()?;
if self.plugins[idx].enabled {
let _ = self.plugins[idx].plugin.stop();
}
self.plugins[idx] = new_state;
println!("[ServiceManager] 插件 '{plugin_id}' 热替换成功");
Ok(())
@@ -487,6 +599,7 @@ impl ServiceManager {
"[ServiceManager] 插件 '{}' 错误次数达到阈值,已禁用",
plugin_id
);
let _ = state.plugin.stop();
state.enabled = false;
}
ErrorPolicy::AutoRollback => {
@@ -495,6 +608,7 @@ impl ServiceManager {
plugin_id
);
// 先禁用,等待外部 (main.rs / HTTP API) 调用 VersionManager 执行回退
let _ = state.plugin.stop();
state.enabled = false;
}
}
@@ -516,4 +630,6 @@ pub struct PluginStateInfo {
pub error_count: u32,
pub max_errors: u32,
pub enabled: bool,
pub test_results: Vec<CapabilityTestResult>,
pub capabilities: Vec<String>,
}

View File

@@ -1,7 +1,8 @@
use super::config::{parse_str, AppConfig};
use super::message::{Destination, Envelope, Message};
use super::plugin::{Platform, Plugin, PluginContext, PluginInfo};
use super::plugin::{CapabilityTestResult, Platform, Plugin, PluginContext, PluginInfo};
use super::service_manager::ServiceManager;
use super::plugin_loader::ErrorPolicy;
use anyhow::Result;
use std::sync::{Arc, Mutex};
@@ -421,3 +422,194 @@ fn topological_sort_places_http_after_video() {
http_init_pos
);
}
// ── 自测相关测试 ──
/// 支持自测的 TestPlugin 变体
struct TestPluginWithSelfTest {
id: String,
events: Arc<Mutex<Vec<String>>>,
caps: Vec<String>,
test_results: Vec<CapabilityTestResult>,
}
impl TestPluginWithSelfTest {
fn new(
id: &str,
events: Arc<Mutex<Vec<String>>>,
caps: Vec<String>,
test_results: Vec<CapabilityTestResult>,
) -> Self {
Self {
id: id.to_string(),
events,
caps,
test_results,
}
}
fn record(&self, entry: impl Into<String>) {
lock_events(&self.events).push(entry.into());
}
}
impl Plugin for TestPluginWithSelfTest {
fn id(&self) -> &str {
&self.id
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: self.id.clone(),
version: "test".to_string(),
description: "test plugin with self_test".to_string(),
platform: Platform::Any,
}
}
fn capabilities(&self) -> Vec<String> {
self.caps.clone()
}
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
self.record(format!("self_test:{}", self.id));
self.test_results.clone()
}
fn init(&mut self, _ctx: PluginContext) -> Result<()> {
self.record(format!("init:{}", self.id));
Ok(())
}
fn start(&mut self) -> Result<()> {
self.record(format!("start:{}", self.id));
Ok(())
}
fn handle_message(&mut self, _msg: Message) -> Result<()> {
Ok(())
}
fn stop(&mut self) -> Result<()> {
self.record(format!("stop:{}", self.id));
Ok(())
}
}
#[test]
fn self_test_all_pass_allows_normal_start() {
let events = Arc::new(Mutex::new(Vec::new()));
let mut manager = ServiceManager::new(test_config());
let plugin = TestPluginWithSelfTest::new(
"sensor",
events.clone(),
vec!["temperature".into()],
vec![CapabilityTestResult {
capability: "temperature".into(),
passed: true,
message: "ok".into(),
}],
);
manager.register_dynamic_with_manifest(
Box::new(plugin),
ErrorPolicy::DisableAndLog,
5,
vec!["temperature".into()],
vec!["temperature".into()],
true,
);
manager.start_all().expect("start_all should succeed when all tests pass");
let log = lock_events(&events);
assert!(log.contains(&"init:sensor".to_string()));
assert!(log.contains(&"self_test:sensor".to_string()));
assert!(log.contains(&"start:sensor".to_string()));
}
#[test]
fn self_test_required_capability_fails_disables_dynamic_plugin() {
let events = Arc::new(Mutex::new(Vec::new()));
let mut manager = ServiceManager::new(test_config());
let plugin = TestPluginWithSelfTest::new(
"sensor",
events.clone(),
vec!["temperature".into()],
vec![CapabilityTestResult {
capability: "temperature".into(),
passed: false,
message: "sensor not connected".into(),
}],
);
manager.register_dynamic_with_manifest(
Box::new(plugin),
ErrorPolicy::DisableAndLog,
5,
vec!["temperature".into()],
vec!["temperature".into()],
true,
);
// Should succeed (dynamic plugin failure doesn't abort)
manager.start_all().expect("start_all should succeed");
let log = lock_events(&events);
assert!(log.contains(&"init:sensor".to_string()));
assert!(log.contains(&"self_test:sensor".to_string()));
// start should NOT have been called
assert!(!log.contains(&"start:sensor".to_string()));
// Plugin should be disabled
let states = manager.plugin_states();
assert!(!states[0].enabled, "plugin should be disabled after required capability failure");
}
#[test]
fn self_test_optional_capability_fails_still_starts() {
let events = Arc::new(Mutex::new(Vec::new()));
let mut manager = ServiceManager::new(test_config());
let plugin = TestPluginWithSelfTest::new(
"sensor",
events.clone(),
vec!["temperature".into(), "humidity".into()],
vec![
CapabilityTestResult {
capability: "temperature".into(),
passed: true,
message: "ok".into(),
},
CapabilityTestResult {
capability: "humidity".into(),
passed: false,
message: "sensor not calibrated".into(),
},
],
);
// Only temperature is required; humidity is optional
manager.register_dynamic_with_manifest(
Box::new(plugin),
ErrorPolicy::DisableAndLog,
5,
vec!["temperature".into()],
vec!["temperature".into(), "humidity".into()],
true,
);
manager.start_all().expect("start_all should succeed");
let log = lock_events(&events);
assert!(log.contains(&"self_test:sensor".to_string()));
assert!(log.contains(&"start:sensor".to_string()), "plugin should start despite optional failure");
// Test results should be recorded
let states = manager.plugin_states();
assert_eq!(states[0].test_results.len(), 2);
assert!(states[0].test_results[0].passed);
assert!(!states[0].test_results[1].passed);
}

View File

@@ -80,10 +80,13 @@ fn main() -> Result<()> {
match loader.load_plugin(plugin_id, Some(&entry.active_version)) {
Ok((plugin, manifest)) => {
manager.register_dynamic(
manager.register_dynamic_with_manifest(
Box::new(plugin),
manifest.error_policy,
entry.max_errors,
manifest.required_capabilities,
manifest.capabilities,
manifest.auto_test,
);
println!(
"{} v{} (动态)",