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

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

View File

@@ -18,6 +18,14 @@ pub struct PluginInfo {
pub platform: String,
}
/// 单项能力测试结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityTestResult {
pub capability: String,
pub passed: bool,
pub message: String,
}
/// 消息信封
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
@@ -40,17 +48,29 @@ pub enum Destination {
pub enum Message {
PlayerCommand(serde_json::Value),
PlayerStatus(serde_json::Value),
Trigger { name: String, value: String },
StateChanged { old_state: String, new_state: String },
Trigger {
name: String,
value: String,
},
StateChanged {
old_state: String,
new_state: String,
},
ScreenLockRequest(bool),
CursorVisibility(bool),
WifiCommand(serde_json::Value),
WifiResult(String),
WifiProvisioned { ssid: String, ip: String },
WifiProvisioned {
ssid: String,
ip: String,
},
ConfigReloadRequest,
Shutdown,
PluginReady(String),
Custom { kind: String, payload: String },
Custom {
kind: String,
payload: String,
},
}
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
@@ -108,36 +128,45 @@ impl FfiResult {
}
}
pub type SendCallback = unsafe extern "C" fn(envelope_json: FfiStr);
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
#[repr(C)]
pub struct PluginVTable {
pub create: unsafe extern "C" fn() -> PluginHandle,
pub get_info: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
pub init: unsafe extern "C" fn(handle: PluginHandle, config_json: FfiStr, send_cb: SendCallback) -> FfiResult,
pub init: unsafe extern "C" fn(
handle: PluginHandle,
config_json: FfiStr,
send_ctx: *mut c_void,
send_cb: SendCallback,
) -> FfiResult,
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub handle_message: unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub handle_message:
unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
}
// ── 高级接口:插件作者实现此 trait ──
/// 消息发送器 — 封装 SendCallback提供安全的 Rust API
pub struct MessageSender {
ctx: *mut c_void,
cb: SendCallback,
}
impl MessageSender {
pub fn new(cb: SendCallback) -> Self {
Self { cb }
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
Self { ctx, cb }
}
/// 发送消息信封到主程序
pub fn send(&self, envelope: &Envelope) {
if let Ok(json) = serde_json::to_string(envelope) {
if let Ok(cstr) = CString::new(json) {
unsafe { (self.cb)(cstr.as_ptr()) };
unsafe { (self.cb)(self.ctx, cstr.as_ptr()) };
}
}
}
@@ -179,6 +208,24 @@ pub trait ShowenPlugin: Send {
/// 插件信息
fn info(&self) -> PluginInfo;
/// 声明插件支持的功能列表(默认空)
fn capabilities(&self) -> Vec<String> {
vec![]
}
/// 运行自测,返回每项功能的测试结果
/// 默认实现:所有声明的 capability 均标记为通过
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
self.capabilities()
.into_iter()
.map(|c| CapabilityTestResult {
capability: c,
passed: true,
message: "no test defined".into(),
})
.collect()
}
/// 初始化,收到配置 JSON 和消息发送器
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
@@ -206,16 +253,37 @@ 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 {
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,
};
};
}