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,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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user