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:
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(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() {
|
||||
let tx = unsafe { &*(ctx as *const mpsc::Sender<Envelope>) };
|
||||
if let Err(e) = tx.send(envelope) {
|
||||
eprintln!("[DynamicPlugin] failed to send envelope: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
|
||||
@@ -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 中导出的符号名称
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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{} (动态)",
|
||||
|
||||
Reference in New Issue
Block a user