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,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Envelope {
|
pub struct Envelope {
|
||||||
@@ -40,17 +48,29 @@ pub enum Destination {
|
|||||||
pub enum Message {
|
pub enum Message {
|
||||||
PlayerCommand(serde_json::Value),
|
PlayerCommand(serde_json::Value),
|
||||||
PlayerStatus(serde_json::Value),
|
PlayerStatus(serde_json::Value),
|
||||||
Trigger { name: String, value: String },
|
Trigger {
|
||||||
StateChanged { old_state: String, new_state: String },
|
name: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
StateChanged {
|
||||||
|
old_state: String,
|
||||||
|
new_state: String,
|
||||||
|
},
|
||||||
ScreenLockRequest(bool),
|
ScreenLockRequest(bool),
|
||||||
CursorVisibility(bool),
|
CursorVisibility(bool),
|
||||||
WifiCommand(serde_json::Value),
|
WifiCommand(serde_json::Value),
|
||||||
WifiResult(String),
|
WifiResult(String),
|
||||||
WifiProvisioned { ssid: String, ip: String },
|
WifiProvisioned {
|
||||||
|
ssid: String,
|
||||||
|
ip: String,
|
||||||
|
},
|
||||||
ConfigReloadRequest,
|
ConfigReloadRequest,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
PluginReady(String),
|
PluginReady(String),
|
||||||
Custom { kind: String, payload: String },
|
Custom {
|
||||||
|
kind: String,
|
||||||
|
payload: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
|
// ── 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)]
|
#[repr(C)]
|
||||||
pub struct PluginVTable {
|
pub struct PluginVTable {
|
||||||
pub create: unsafe extern "C" fn() -> PluginHandle,
|
pub create: unsafe extern "C" fn() -> PluginHandle,
|
||||||
pub get_info: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
|
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 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 stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
|
||||||
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
|
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 ──
|
// ── 高级接口:插件作者实现此 trait ──
|
||||||
|
|
||||||
/// 消息发送器 — 封装 SendCallback,提供安全的 Rust API
|
/// 消息发送器 — 封装 SendCallback,提供安全的 Rust API
|
||||||
pub struct MessageSender {
|
pub struct MessageSender {
|
||||||
|
ctx: *mut c_void,
|
||||||
cb: SendCallback,
|
cb: SendCallback,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageSender {
|
impl MessageSender {
|
||||||
pub fn new(cb: SendCallback) -> Self {
|
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
|
||||||
Self { cb }
|
Self { ctx, cb }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送消息信封到主程序
|
/// 发送消息信封到主程序
|
||||||
pub fn send(&self, envelope: &Envelope) {
|
pub fn send(&self, envelope: &Envelope) {
|
||||||
if let Ok(json) = serde_json::to_string(envelope) {
|
if let Ok(json) = serde_json::to_string(envelope) {
|
||||||
if let Ok(cstr) = CString::new(json) {
|
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 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 和消息发送器
|
/// 初始化,收到配置 JSON 和消息发送器
|
||||||
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
|
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
|
||||||
|
|
||||||
@@ -206,16 +253,37 @@ pub trait ShowenPlugin: Send {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! export_plugin {
|
macro_rules! export_plugin {
|
||||||
($plugin_type:ty, $constructor:expr) => {
|
($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 {
|
unsafe extern "C" fn __showen_create() -> $crate::PluginHandle {
|
||||||
let plugin: Box<$plugin_type> = Box::new($constructor);
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
Box::into_raw(plugin) as $crate::PluginHandle
|
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 {
|
unsafe extern "C" fn __showen_get_info(handle: $crate::PluginHandle) -> $crate::FfiString {
|
||||||
let plugin = unsafe { &*(handle as *const $plugin_type) };
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
let info = <$plugin_type as $crate::ShowenPlugin>::info(plugin);
|
let plugin = unsafe { &*(handle as *const $plugin_type) };
|
||||||
match serde_json::to_string(&info) {
|
let info = <$plugin_type as $crate::ShowenPlugin>::info(plugin);
|
||||||
Ok(json) => $crate::FfiString::from_string(json),
|
match serde_json::to_string(&info) {
|
||||||
|
Ok(json) => $crate::FfiString::from_string(json),
|
||||||
|
Err(_) => $crate::FfiString::null(),
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
Ok(info) => info,
|
||||||
Err(_) => $crate::FfiString::null(),
|
Err(_) => $crate::FfiString::null(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,25 +291,36 @@ macro_rules! export_plugin {
|
|||||||
unsafe extern "C" fn __showen_init(
|
unsafe extern "C" fn __showen_init(
|
||||||
handle: $crate::PluginHandle,
|
handle: $crate::PluginHandle,
|
||||||
config_json: $crate::FfiStr,
|
config_json: $crate::FfiStr,
|
||||||
|
send_ctx: *mut std::ffi::c_void,
|
||||||
send_cb: $crate::SendCallback,
|
send_cb: $crate::SendCallback,
|
||||||
) -> $crate::FfiResult {
|
) -> $crate::FfiResult {
|
||||||
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
let config = match unsafe { std::ffi::CStr::from_ptr(config_json) }.to_str() {
|
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
||||||
Ok(s) => s,
|
let config = match unsafe { std::ffi::CStr::from_ptr(config_json) }.to_str() {
|
||||||
Err(e) => return $crate::FfiResult::err(format!("invalid config UTF-8: {e}")),
|
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) {
|
let sender = $crate::MessageSender::new(send_ctx, send_cb);
|
||||||
Ok(()) => $crate::FfiResult::ok(),
|
match <$plugin_type as $crate::ShowenPlugin>::init(plugin, config, sender) {
|
||||||
Err(e) => $crate::FfiResult::err(e),
|
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 {
|
unsafe extern "C" fn __showen_start(handle: $crate::PluginHandle) -> $crate::FfiResult {
|
||||||
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
match <$plugin_type as $crate::ShowenPlugin>::start(plugin) {
|
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
||||||
Ok(()) => $crate::FfiResult::ok(),
|
match <$plugin_type as $crate::ShowenPlugin>::start(plugin) {
|
||||||
Err(e) => $crate::FfiResult::err(e),
|
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,
|
handle: $crate::PluginHandle,
|
||||||
message_json: $crate::FfiStr,
|
message_json: $crate::FfiStr,
|
||||||
) -> $crate::FfiResult {
|
) -> $crate::FfiResult {
|
||||||
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
let json_str = match unsafe { std::ffi::CStr::from_ptr(message_json) }.to_str() {
|
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
||||||
Ok(s) => s,
|
let json_str = match unsafe { std::ffi::CStr::from_ptr(message_json) }.to_str() {
|
||||||
Err(e) => return $crate::FfiResult::err(format!("invalid message UTF-8: {e}")),
|
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,
|
let message: $crate::Message = match serde_json::from_str(json_str) {
|
||||||
Err(e) => return $crate::FfiResult::err(format!("invalid message JSON: {e}")),
|
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(),
|
match <$plugin_type as $crate::ShowenPlugin>::handle_message(plugin, message) {
|
||||||
Err(e) => $crate::FfiResult::err(e),
|
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 {
|
unsafe extern "C" fn __showen_stop(handle: $crate::PluginHandle) -> $crate::FfiResult {
|
||||||
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
match <$plugin_type as $crate::ShowenPlugin>::stop(plugin) {
|
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
|
||||||
Ok(()) => $crate::FfiResult::ok(),
|
match <$plugin_type as $crate::ShowenPlugin>::stop(plugin) {
|
||||||
Err(e) => $crate::FfiResult::err(e),
|
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) {
|
unsafe extern "C" fn __showen_destroy(handle: $crate::PluginHandle) {
|
||||||
if !handle.is_null() {
|
if let Ok(()) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
drop(unsafe { Box::from_raw(handle as *mut $plugin_type) });
|
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,
|
handle_message: __showen_handle_message,
|
||||||
stop: __showen_stop,
|
stop: __showen_stop,
|
||||||
destroy: __showen_destroy,
|
destroy: __showen_destroy,
|
||||||
|
get_capabilities: __showen_get_capabilities,
|
||||||
|
self_test: __showen_self_test,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
//! 示例动态插件 — 展示如何使用 showen-plugin-sdk 编写插件
|
//! 示例动态插件 — 展示如何使用 showen-plugin-sdk 编写插件
|
||||||
//!
|
//!
|
||||||
//! 此插件仅打印日志,用于验证动态加载流程。
|
//! 此插件演示动态加载流程及自测机制。
|
||||||
|
|
||||||
use showen_plugin_sdk::{
|
use showen_plugin_sdk::{
|
||||||
export_plugin, Message, MessageSender, PluginInfo, ShowenPlugin,
|
export_plugin, CapabilityTestResult, Message, MessageSender, PluginInfo, ShowenPlugin,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ExamplePlugin {
|
pub struct ExamplePlugin {
|
||||||
sender: Option<MessageSender>,
|
sender: Option<MessageSender>,
|
||||||
|
/// 用于演示可配置的自测失败
|
||||||
|
fail_optional_test: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExamplePlugin {
|
impl ExamplePlugin {
|
||||||
pub fn new() -> Self {
|
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> {
|
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> {
|
||||||
eprintln!("[ExamplePlugin] init called, config length: {}", config_json.len());
|
eprintln!("[ExamplePlugin] init called, config length: {}", config_json.len());
|
||||||
self.sender = Some(sender);
|
self.sender = Some(sender);
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
//! 对 ServiceManager 而言,DynamicPlugin 与静态插件无区别。
|
//! 对 ServiceManager 而言,DynamicPlugin 与静态插件无区别。
|
||||||
|
|
||||||
use crate::core::message::{Envelope, Message};
|
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::{
|
use crate::core::plugin_abi::{
|
||||||
ffi_str_to_str, FfiResult, FfiString, PluginHandle, PluginVTable,
|
ffi_str_to_str, FfiResult, FfiString, PluginHandle, PluginVTable, PLUGIN_VTABLE_SYMBOL,
|
||||||
PLUGIN_VTABLE_SYMBOL,
|
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use libloading::Library;
|
use libloading::Library;
|
||||||
@@ -30,6 +29,8 @@ pub struct DynamicPlugin {
|
|||||||
dependencies: Vec<String>,
|
dependencies: Vec<String>,
|
||||||
/// .so 文件路径(用于调试/日志)
|
/// .so 文件路径(用于调试/日志)
|
||||||
so_path: String,
|
so_path: String,
|
||||||
|
/// Sender 上下文指针(堆分配的 mpsc::Sender)
|
||||||
|
sender_ctx: *mut std::ffi::c_void,
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginHandle 是 *mut c_void,需要手动声明 Send
|
// PluginHandle 是 *mut c_void,需要手动声明 Send
|
||||||
@@ -41,10 +42,7 @@ impl DynamicPlugin {
|
|||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// .so 文件必须是由 showen-plugin-sdk 编译的合法插件
|
/// .so 文件必须是由 showen-plugin-sdk 编译的合法插件
|
||||||
pub unsafe fn load(
|
pub unsafe fn load(so_path: &str, dependencies: Vec<String>) -> Result<Self> {
|
||||||
so_path: &str,
|
|
||||||
dependencies: Vec<String>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let library = unsafe {
|
let library = unsafe {
|
||||||
Library::new(so_path)
|
Library::new(so_path)
|
||||||
.with_context(|| format!("failed to load plugin .so: {so_path}"))?
|
.with_context(|| format!("failed to load plugin .so: {so_path}"))?
|
||||||
@@ -54,9 +52,7 @@ impl DynamicPlugin {
|
|||||||
let vtable: &'static PluginVTable = unsafe {
|
let vtable: &'static PluginVTable = unsafe {
|
||||||
let symbol = library
|
let symbol = library
|
||||||
.get::<*const PluginVTable>(PLUGIN_VTABLE_SYMBOL)
|
.get::<*const PluginVTable>(PLUGIN_VTABLE_SYMBOL)
|
||||||
.with_context(|| {
|
.with_context(|| format!("symbol 'showen_plugin_vtable' not found in {so_path}"))?;
|
||||||
format!("symbol 'showen_plugin_vtable' not found in {so_path}")
|
|
||||||
})?;
|
|
||||||
&**symbol
|
&**symbol
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +79,7 @@ impl DynamicPlugin {
|
|||||||
id,
|
id,
|
||||||
dependencies,
|
dependencies,
|
||||||
so_path: so_path.to_string(),
|
so_path: so_path.to_string(),
|
||||||
|
sender_ctx: std::ptr::null_mut(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,14 +95,8 @@ impl DynamicPlugin {
|
|||||||
|
|
||||||
/// 将 FfiResult 转为 anyhow::Result
|
/// 将 FfiResult 转为 anyhow::Result
|
||||||
unsafe fn check_result(&self, result: FfiResult, operation: &str) -> Result<()> {
|
unsafe fn check_result(&self, result: FfiResult, operation: &str) -> Result<()> {
|
||||||
unsafe { result.into_result() }.map_err(|e| {
|
unsafe { result.into_result() }
|
||||||
anyhow!(
|
.map_err(|e| anyhow!("plugin '{}' {} failed: {}", self.id, operation, e))
|
||||||
"plugin '{}' {} failed: {}",
|
|
||||||
self.id,
|
|
||||||
operation,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,21 +113,40 @@ impl Plugin for DynamicPlugin {
|
|||||||
self.dependencies.clone()
|
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<()> {
|
fn init(&mut self, ctx: PluginContext) -> Result<()> {
|
||||||
// 序列化配置为 JSON
|
|
||||||
let config_json = serde_json::to_string(ctx.config.as_ref())
|
let config_json = serde_json::to_string(ctx.config.as_ref())
|
||||||
.context("failed to serialize config for dynamic plugin")?;
|
.context("failed to serialize config for dynamic plugin")?;
|
||||||
let config_cstr = CString::new(config_json)
|
let config_cstr = CString::new(config_json).context("config JSON contains null byte")?;
|
||||||
.context("config JSON contains null byte")?;
|
|
||||||
|
|
||||||
// 创建 SendCallback — 将 mpsc::Sender 转为 C 函数指针
|
// 将 Sender 分配到堆上,生命周期由 DynamicPlugin 管理
|
||||||
// 使用 thread_local 存储 sender(每次 init 更新)
|
let sender_box = Box::new(ctx.tx);
|
||||||
PLUGIN_SENDER.with(|cell| {
|
self.sender_ctx = Box::into_raw(sender_box) as *mut std::ffi::c_void;
|
||||||
*cell.borrow_mut() = Some(ctx.tx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = unsafe {
|
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") }
|
unsafe { self.check_result(result, "init") }
|
||||||
}
|
}
|
||||||
@@ -149,12 +159,9 @@ impl Plugin for DynamicPlugin {
|
|||||||
fn handle_message(&mut self, msg: Message) -> Result<()> {
|
fn handle_message(&mut self, msg: Message) -> Result<()> {
|
||||||
let msg_json = serde_json::to_string(&msg)
|
let msg_json = serde_json::to_string(&msg)
|
||||||
.context("failed to serialize message for dynamic plugin")?;
|
.context("failed to serialize message for dynamic plugin")?;
|
||||||
let msg_cstr = CString::new(msg_json)
|
let msg_cstr = CString::new(msg_json).context("message JSON contains null byte")?;
|
||||||
.context("message JSON contains null byte")?;
|
|
||||||
|
|
||||||
let result = unsafe {
|
let result = unsafe { (self.vtable.handle_message)(self.handle, msg_cstr.as_ptr()) };
|
||||||
(self.vtable.handle_message)(self.handle, msg_cstr.as_ptr())
|
|
||||||
};
|
|
||||||
unsafe { self.check_result(result, "handle_message") }
|
unsafe { self.check_result(result, "handle_message") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +176,29 @@ impl Drop for DynamicPlugin {
|
|||||||
if !self.handle.is_null() {
|
if !self.handle.is_null() {
|
||||||
unsafe { (self.vtable.destroy)(self.handle) };
|
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 实现 ──
|
// ── SendCallback 实现 ──
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static PLUGIN_SENDER: std::cell::RefCell<Option<mpsc::Sender<Envelope>>> =
|
|
||||||
std::cell::RefCell::new(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// C FFI 回调:插件调用此函数向主程序发消息
|
/// 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) } {
|
let json_str = match unsafe { ffi_str_to_str(envelope_json) } {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
@@ -197,11 +215,8 @@ unsafe extern "C" fn ffi_send_callback(envelope_json: crate::core::plugin_abi::F
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PLUGIN_SENDER.with(|cell| {
|
let tx = unsafe { &*(ctx as *const mpsc::Sender<Envelope>) };
|
||||||
if let Some(tx) = cell.borrow().as_ref() {
|
if let Err(e) = tx.send(envelope) {
|
||||||
if let Err(e) = tx.send(envelope) {
|
eprintln!("[DynamicPlugin] failed to send envelope: {e}");
|
||||||
eprintln!("[DynamicPlugin] failed to send envelope: {e}");
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ use anyhow::Result;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{mpsc, Arc};
|
use std::sync::{mpsc, Arc};
|
||||||
|
|
||||||
|
/// 单项能力测试结果
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CapabilityTestResult {
|
||||||
|
pub capability: String,
|
||||||
|
pub passed: bool,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// 所有功能都通过实现此 trait 接入系统
|
/// 所有功能都通过实现此 trait 接入系统
|
||||||
pub trait Plugin: Send {
|
pub trait Plugin: Send {
|
||||||
/// 唯一标识 (如 "video", "http", "ble")
|
/// 唯一标识 (如 "video", "http", "ble")
|
||||||
@@ -17,6 +25,24 @@ pub trait Plugin: Send {
|
|||||||
vec![]
|
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<()>;
|
fn init(&mut self, ctx: PluginContext) -> Result<()>;
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ impl FfiResult {
|
|||||||
|
|
||||||
/// 插件向主程序发消息的回调函数类型
|
/// 插件向主程序发消息的回调函数类型
|
||||||
/// envelope_json: JSON 序列化的 Envelope
|
/// 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)]
|
#[repr(C)]
|
||||||
@@ -111,20 +111,32 @@ pub struct PluginVTable {
|
|||||||
/// 初始化插件
|
/// 初始化插件
|
||||||
/// config_json: 完整的 AppConfig JSON
|
/// config_json: 完整的 AppConfig JSON
|
||||||
/// send_cb: 发送消息的回调
|
/// 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,
|
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
|
||||||
|
|
||||||
/// 处理消息
|
/// 处理消息
|
||||||
/// message_json: JSON 序列化的 Message
|
/// 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 stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
|
||||||
|
|
||||||
/// 销毁插件实例,释放资源
|
/// 销毁插件实例,释放资源
|
||||||
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
|
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 中导出的符号名称
|
/// 动态插件 .so 中导出的符号名称
|
||||||
|
|||||||
@@ -29,12 +29,28 @@ pub struct PluginManifest {
|
|||||||
#[serde(default = "default_error_policy")]
|
#[serde(default = "default_error_policy")]
|
||||||
pub error_policy: ErrorPolicy,
|
pub error_policy: ErrorPolicy,
|
||||||
pub so_filename: String,
|
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 {
|
fn default_error_policy() -> ErrorPolicy {
|
||||||
ErrorPolicy::AutoRollback
|
ErrorPolicy::AutoRollback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_test_timeout() -> u64 {
|
||||||
|
5000
|
||||||
|
}
|
||||||
|
|
||||||
/// 插件错误处理策略
|
/// 插件错误处理策略
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -250,6 +266,10 @@ mod tests {
|
|||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
error_policy: ErrorPolicy::AutoRollback,
|
error_policy: ErrorPolicy::AutoRollback,
|
||||||
so_filename: "libtest_plugin.so".to_string(),
|
so_filename: "libtest_plugin.so".to_string(),
|
||||||
|
capabilities: vec![],
|
||||||
|
required_capabilities: vec![],
|
||||||
|
test_timeout_ms: 5000,
|
||||||
|
auto_test: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
@@ -333,4 +353,38 @@ mod tests {
|
|||||||
|
|
||||||
let _ = fs::remove_dir_all(&tmp);
|
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::config::AppConfig;
|
||||||
use crate::core::message::{Destination, Envelope, Message};
|
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 crate::core::plugin_loader::ErrorPolicy;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -19,6 +19,14 @@ struct PluginState {
|
|||||||
max_errors: u32,
|
max_errors: u32,
|
||||||
/// 是否启用
|
/// 是否启用
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
/// 挂载时的自测结果
|
||||||
|
test_results: Vec<CapabilityTestResult>,
|
||||||
|
/// 声明的功能列表
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
/// manifest 中声明的必须通过的功能
|
||||||
|
required_capabilities: Vec<String>,
|
||||||
|
/// 是否自动测试
|
||||||
|
auto_test: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PluginState {
|
impl PluginState {
|
||||||
@@ -30,14 +38,14 @@ impl PluginState {
|
|||||||
error_count: 0,
|
error_count: 0,
|
||||||
max_errors: u32::MAX, // 静态插件不自动禁用
|
max_errors: u32::MAX, // 静态插件不自动禁用
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
test_results: vec![],
|
||||||
|
capabilities: vec![],
|
||||||
|
required_capabilities: vec![],
|
||||||
|
auto_test: false, // 静态插件默认不自测
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_dynamic(
|
fn new_dynamic(plugin: Box<dyn Plugin>, error_policy: ErrorPolicy, max_errors: u32) -> Self {
|
||||||
plugin: Box<dyn Plugin>,
|
|
||||||
error_policy: ErrorPolicy,
|
|
||||||
max_errors: u32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
plugin,
|
plugin,
|
||||||
is_dynamic: true,
|
is_dynamic: true,
|
||||||
@@ -45,6 +53,10 @@ impl PluginState {
|
|||||||
error_count: 0,
|
error_count: 0,
|
||||||
max_errors,
|
max_errors,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
test_results: vec![],
|
||||||
|
capabilities: vec![],
|
||||||
|
required_capabilities: vec![],
|
||||||
|
auto_test: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +109,19 @@ impl ServiceManager {
|
|||||||
plugin: Box<dyn Plugin>,
|
plugin: Box<dyn Plugin>,
|
||||||
error_policy: ErrorPolicy,
|
error_policy: ErrorPolicy,
|
||||||
max_errors: u32,
|
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!(
|
println!(
|
||||||
"[ServiceManager] 注册动态插件: {} (策略: {:?}, 最大错误: {})",
|
"[ServiceManager] 注册动态插件: {} (策略: {:?}, 最大错误: {})",
|
||||||
@@ -104,16 +129,19 @@ impl ServiceManager {
|
|||||||
error_policy,
|
error_policy,
|
||||||
max_errors
|
max_errors
|
||||||
);
|
);
|
||||||
self.plugins
|
let mut state = PluginState::new_dynamic(plugin, error_policy, max_errors);
|
||||||
.push(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() → self_test() → start() 所有插件
|
||||||
/// 动态插件 init/start 失败时按策略处理,不中断其他插件
|
/// 动态插件 init/start/test 失败时按策略处理,不中断其他插件
|
||||||
pub fn start_all(&mut self) -> Result<()> {
|
pub fn start_all(&mut self) -> Result<()> {
|
||||||
self.validate_and_sort_plugins()?;
|
self.validate_and_sort_plugins()?;
|
||||||
|
|
||||||
// init
|
// Phase 1: init
|
||||||
for state in &mut self.plugins {
|
for state in &mut self.plugins {
|
||||||
let ctx = PluginContext {
|
let ctx = PluginContext {
|
||||||
tx: self.tx.clone(),
|
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 {
|
for state in &mut self.plugins {
|
||||||
if !state.enabled {
|
if !state.enabled {
|
||||||
continue;
|
continue;
|
||||||
@@ -198,11 +310,7 @@ impl ServiceManager {
|
|||||||
}
|
}
|
||||||
println!("[ServiceManager] 停止插件: {}", state.id());
|
println!("[ServiceManager] 停止插件: {}", state.id());
|
||||||
if let Err(e) = state.plugin.stop() {
|
if let Err(e) = state.plugin.stop() {
|
||||||
eprintln!(
|
eprintln!("[ServiceManager] 停止插件 '{}' 失败: {}", state.id(), e);
|
||||||
"[ServiceManager] 停止插件 '{}' 失败: {}",
|
|
||||||
state.id(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -241,6 +349,8 @@ impl ServiceManager {
|
|||||||
error_count: s.error_count,
|
error_count: s.error_count,
|
||||||
max_errors: s.max_errors,
|
max_errors: s.max_errors,
|
||||||
enabled: s.enabled,
|
enabled: s.enabled,
|
||||||
|
test_results: s.test_results.clone(),
|
||||||
|
capabilities: s.capabilities.clone(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -259,15 +369,13 @@ impl ServiceManager {
|
|||||||
.position(|s| s.id() == plugin_id)
|
.position(|s| s.id() == plugin_id)
|
||||||
.ok_or_else(|| anyhow!("plugin '{plugin_id}' not found for replacement"))?;
|
.ok_or_else(|| anyhow!("plugin '{plugin_id}' not found for replacement"))?;
|
||||||
|
|
||||||
// Stop old plugin
|
if !self.plugins[idx].is_dynamic {
|
||||||
if self.plugins[idx].enabled {
|
return Err(anyhow!(
|
||||||
let _ = self.plugins[idx].plugin.stop();
|
"plugin '{plugin_id}' is not dynamic and cannot be replaced"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace
|
|
||||||
let mut new_state = PluginState::new_dynamic(new_plugin, error_policy, max_errors);
|
let mut new_state = PluginState::new_dynamic(new_plugin, error_policy, max_errors);
|
||||||
|
|
||||||
// Init new plugin
|
|
||||||
let ctx = PluginContext {
|
let ctx = PluginContext {
|
||||||
tx: self.tx.clone(),
|
tx: self.tx.clone(),
|
||||||
config: Arc::clone(&self.config),
|
config: Arc::clone(&self.config),
|
||||||
@@ -275,6 +383,10 @@ impl ServiceManager {
|
|||||||
new_state.plugin.init(ctx)?;
|
new_state.plugin.init(ctx)?;
|
||||||
new_state.plugin.start()?;
|
new_state.plugin.start()?;
|
||||||
|
|
||||||
|
if self.plugins[idx].enabled {
|
||||||
|
let _ = self.plugins[idx].plugin.stop();
|
||||||
|
}
|
||||||
|
|
||||||
self.plugins[idx] = new_state;
|
self.plugins[idx] = new_state;
|
||||||
println!("[ServiceManager] 插件 '{plugin_id}' 热替换成功");
|
println!("[ServiceManager] 插件 '{plugin_id}' 热替换成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -487,6 +599,7 @@ impl ServiceManager {
|
|||||||
"[ServiceManager] 插件 '{}' 错误次数达到阈值,已禁用",
|
"[ServiceManager] 插件 '{}' 错误次数达到阈值,已禁用",
|
||||||
plugin_id
|
plugin_id
|
||||||
);
|
);
|
||||||
|
let _ = state.plugin.stop();
|
||||||
state.enabled = false;
|
state.enabled = false;
|
||||||
}
|
}
|
||||||
ErrorPolicy::AutoRollback => {
|
ErrorPolicy::AutoRollback => {
|
||||||
@@ -495,6 +608,7 @@ impl ServiceManager {
|
|||||||
plugin_id
|
plugin_id
|
||||||
);
|
);
|
||||||
// 先禁用,等待外部 (main.rs / HTTP API) 调用 VersionManager 执行回退
|
// 先禁用,等待外部 (main.rs / HTTP API) 调用 VersionManager 执行回退
|
||||||
|
let _ = state.plugin.stop();
|
||||||
state.enabled = false;
|
state.enabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,4 +630,6 @@ pub struct PluginStateInfo {
|
|||||||
pub error_count: u32,
|
pub error_count: u32,
|
||||||
pub max_errors: u32,
|
pub max_errors: u32,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub test_results: Vec<CapabilityTestResult>,
|
||||||
|
pub capabilities: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use super::config::{parse_str, AppConfig};
|
use super::config::{parse_str, AppConfig};
|
||||||
use super::message::{Destination, Envelope, Message};
|
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::service_manager::ServiceManager;
|
||||||
|
use super::plugin_loader::ErrorPolicy;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -421,3 +422,194 @@ fn topological_sort_places_http_after_video() {
|
|||||||
http_init_pos
|
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)) {
|
match loader.load_plugin(plugin_id, Some(&entry.active_version)) {
|
||||||
Ok((plugin, manifest)) => {
|
Ok((plugin, manifest)) => {
|
||||||
manager.register_dynamic(
|
manager.register_dynamic_with_manifest(
|
||||||
Box::new(plugin),
|
Box::new(plugin),
|
||||||
manifest.error_policy,
|
manifest.error_policy,
|
||||||
entry.max_errors,
|
entry.max_errors,
|
||||||
|
manifest.required_capabilities,
|
||||||
|
manifest.capabilities,
|
||||||
|
manifest.auto_test,
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
" ✓ {} v{} (动态)",
|
" ✓ {} v{} (动态)",
|
||||||
|
|||||||
Reference in New Issue
Block a user