diff --git a/plugin-sdk/src/lib.rs b/plugin-sdk/src/lib.rs index a308a68..1ec37bd 100644 --- a/plugin-sdk/src/lib.rs +++ b/plugin-sdk/src/lib.rs @@ -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 { + vec![] + } + + /// 运行自测,返回每项功能的测试结果 + /// 默认实现:所有声明的 capability 均标记为通过 + fn self_test(&mut self) -> Vec { + 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,16 +253,37 @@ pub trait ShowenPlugin: Send { #[macro_export] macro_rules! export_plugin { ($plugin_type:ty, $constructor:expr) => { + fn __showen_panic_error(payload: Box) -> String { + let msg = if let Some(s) = payload.downcast_ref::<&'static str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "unknown panic".to_string() + }; + format!("plugin panicked: {}", msg) + } + unsafe extern "C" fn __showen_create() -> $crate::PluginHandle { - let plugin: Box<$plugin_type> = Box::new($constructor); - Box::into_raw(plugin) as $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 { - 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), + 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(), } } @@ -223,25 +291,36 @@ macro_rules! export_plugin { 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 { - 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); - match <$plugin_type as $crate::ShowenPlugin>::init(plugin, config, sender) { - Ok(()) => $crate::FfiResult::ok(), - Err(e) => $crate::FfiResult::err(e), + 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_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 { - 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), + 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)), } } @@ -249,32 +328,76 @@ macro_rules! export_plugin { handle: $crate::PluginHandle, message_json: $crate::FfiStr, ) -> $crate::FfiResult { - 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, - Err(e) => return $crate::FfiResult::err(format!("invalid message UTF-8: {e}")), - }; - let message: $crate::Message = match serde_json::from_str(json_str) { - Ok(m) => m, - Err(e) => return $crate::FfiResult::err(format!("invalid message JSON: {e}")), - }; - match <$plugin_type as $crate::ShowenPlugin>::handle_message(plugin, message) { - Ok(()) => $crate::FfiResult::ok(), - Err(e) => $crate::FfiResult::err(e), + 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, + Err(e) => return $crate::FfiResult::err(format!("invalid message UTF-8: {e}")), + }; + let message: $crate::Message = match serde_json::from_str(json_str) { + Ok(m) => m, + Err(e) => return $crate::FfiResult::err(format!("invalid message JSON: {e}")), + }; + match <$plugin_type as $crate::ShowenPlugin>::handle_message(plugin, message) { + 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 { - 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), + 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 !handle.is_null() { - drop(unsafe { Box::from_raw(handle as *mut $plugin_type) }); + 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(), } } @@ -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, }; }; } diff --git a/plugins/example-plugin/src/lib.rs b/plugins/example-plugin/src/lib.rs index b538b3e..b50ed71 100644 --- a/plugins/example-plugin/src/lib.rs +++ b/plugins/example-plugin/src/lib.rs @@ -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, + /// 用于演示可配置的自测失败 + 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 { + vec!["logging".to_string(), "metrics".to_string()] + } + + fn self_test(&mut self) -> Vec { + 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); diff --git a/src/core/dynamic_plugin.rs b/src/core/dynamic_plugin.rs index 8f06506..09ca3f8 100644 --- a/src/core/dynamic_plugin.rs +++ b/src/core/dynamic_plugin.rs @@ -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, /// .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, - ) -> Result { + pub unsafe fn load(so_path: &str, dependencies: Vec) -> Result { 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 { + 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 { + 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, + )); + } + self.sender_ctx = std::ptr::null_mut(); + } } } // ── SendCallback 实现 ── -thread_local! { - static PLUGIN_SENDER: std::cell::RefCell>> = - std::cell::RefCell::new(None); -} - /// C FFI 回调:插件调用此函数向主程序发消息 -unsafe extern "C" fn ffi_send_callback(envelope_json: crate::core::plugin_abi::FfiStr) { +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; + } + 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() { - if let Err(e) = tx.send(envelope) { - eprintln!("[DynamicPlugin] failed to send envelope: {e}"); - } - } - }); + let tx = unsafe { &*(ctx as *const mpsc::Sender) }; + if let Err(e) = tx.send(envelope) { + eprintln!("[DynamicPlugin] failed to send envelope: {e}"); + } } diff --git a/src/core/plugin.rs b/src/core/plugin.rs index 8fcf2d1..f54cc96 100644 --- a/src/core/plugin.rs +++ b/src/core/plugin.rs @@ -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 { + vec![] + } + + /// 运行自测,返回每项功能的测试结果 + /// 默认实现:所有声明的 capability 均标记为通过 + fn self_test(&mut self) -> Vec { + self.capabilities() + .into_iter() + .map(|c| CapabilityTestResult { + capability: c, + passed: true, + message: "no test defined".into(), + }) + .collect() + } + /// 初始化:获取发送通道,声明订阅的消息类型 fn init(&mut self, ctx: PluginContext) -> Result<()>; diff --git a/src/core/plugin_abi.rs b/src/core/plugin_abi.rs index ddf50d6..0faa3c5 100644 --- a/src/core/plugin_abi.rs +++ b/src/core/plugin_abi.rs @@ -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) + pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString, + + /// 运行自测 (返回 JSON: Vec) + pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString, } /// 动态插件 .so 中导出的符号名称 diff --git a/src/core/plugin_loader.rs b/src/core/plugin_loader.rs index 0c4d346..fdab77d 100644 --- a/src/core/plugin_loader.rs +++ b/src/core/plugin_loader.rs @@ -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, + /// 挂载时必须通过测试的功能(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, 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); + } } diff --git a/src/core/service_manager.rs b/src/core/service_manager.rs index 3d3b5a0..356aabc 100644 --- a/src/core/service_manager.rs +++ b/src/core/service_manager.rs @@ -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, + /// 声明的功能列表 + capabilities: Vec, + /// manifest 中声明的必须通过的功能 + required_capabilities: Vec, + /// 是否自动测试 + 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, - error_policy: ErrorPolicy, - max_errors: u32, - ) -> Self { + fn new_dynamic(plugin: Box, 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, 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, + error_policy: ErrorPolicy, + max_errors: u32, + required_capabilities: Vec, + capabilities: Vec, + 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, + pub capabilities: Vec, } diff --git a/src/core/tests.rs b/src/core/tests.rs index 1d2ccc9..cd751e6 100644 --- a/src/core/tests.rs +++ b/src/core/tests.rs @@ -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>>, + caps: Vec, + test_results: Vec, +} + +impl TestPluginWithSelfTest { + fn new( + id: &str, + events: Arc>>, + caps: Vec, + test_results: Vec, + ) -> Self { + Self { + id: id.to_string(), + events, + caps, + test_results, + } + } + + fn record(&self, entry: impl Into) { + 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 { + self.caps.clone() + } + + fn self_test(&mut self) -> Vec { + 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); +} diff --git a/src/main.rs b/src/main.rs index f50b79e..64b1835 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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{} (动态)",