test+docs: 新增4个测试(66总计) + SDK API文档 + 员工soul更新
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1140,6 +1140,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|||||||
name = "showen-example-plugin"
|
name = "showen-example-plugin"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"showen-plugin-sdk",
|
"showen-plugin-sdk",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,36 +9,58 @@ use std::ptr;
|
|||||||
|
|
||||||
// ── 重新导出消息类型(与主程序共享 JSON 契约) ──
|
// ── 重新导出消息类型(与主程序共享 JSON 契约) ──
|
||||||
|
|
||||||
/// 插件信息
|
/// 描述插件元数据。
|
||||||
|
///
|
||||||
|
/// 主程序会在加载动态库后读取此结构体,用于展示插件名称、版本、平台信息,
|
||||||
|
/// 并帮助第三方开发者确认插件是否按预期被正确识别。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PluginInfo {
|
pub struct PluginInfo {
|
||||||
|
/// 插件的人类可读名称。
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// 插件版本号,通常使用语义化版本格式。
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
/// 插件用途简介,会显示给使用者或调试工具。
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// 插件面向的平台标识,例如 `linux` 或 `cross-platform`。
|
||||||
pub platform: String,
|
pub platform: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单项能力测试结果
|
/// 表示一次能力自检中的单项结果。
|
||||||
|
///
|
||||||
|
/// `self_test` 返回的列表会由主程序或测试工具消费,用于判断插件声明的能力
|
||||||
|
/// 是否已经完成最基本的运行时验证。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CapabilityTestResult {
|
pub struct CapabilityTestResult {
|
||||||
|
/// 被测试的能力名称,通常与 `ShowenPlugin::capabilities` 中的条目对应。
|
||||||
pub capability: String,
|
pub capability: String,
|
||||||
|
/// 该能力是否通过自检。
|
||||||
pub passed: bool,
|
pub passed: bool,
|
||||||
|
/// 面向开发者的结果说明,可用于记录失败原因或补充上下文。
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 消息信封
|
/// 插件系统中的标准消息信封。
|
||||||
|
///
|
||||||
|
/// 所有跨插件、插件到主程序、或插件到管理层的通信都通过此结构体完成。
|
||||||
|
/// 它定义了统一的发送者、目的地和消息载荷格式,并通过 JSON 在 ABI 边界上传输。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Envelope {
|
pub struct Envelope {
|
||||||
|
/// 消息发送者名称,通常填写当前插件名。
|
||||||
pub from: String,
|
pub from: String,
|
||||||
|
/// 消息目标,可指定单个插件、广播或管理层。
|
||||||
pub to: Destination,
|
pub to: Destination,
|
||||||
|
/// 实际发送的业务消息。
|
||||||
pub message: Message,
|
pub message: Message,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 消息目的地
|
/// 定义消息的投递目标。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum Destination {
|
pub enum Destination {
|
||||||
|
/// 将消息发送给指定名称的插件。
|
||||||
Plugin(String),
|
Plugin(String),
|
||||||
|
/// 将消息广播给所有感兴趣的接收方。
|
||||||
Broadcast,
|
Broadcast,
|
||||||
|
/// 将消息发送给主程序的管理层或调度层。
|
||||||
Manager,
|
Manager,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,46 +68,104 @@ pub enum Destination {
|
|||||||
/// 动态插件只需处理自己关心的消息变体
|
/// 动态插件只需处理自己关心的消息变体
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
|
/// 发送给播放器模块的命令。
|
||||||
|
///
|
||||||
|
/// 载荷内容由播放器插件或主程序定义,通常是 JSON 对象,包含播放、暂停、
|
||||||
|
/// 跳转等控制信息。
|
||||||
PlayerCommand(serde_json::Value),
|
PlayerCommand(serde_json::Value),
|
||||||
|
/// 来自播放器模块的状态快照或状态变更。
|
||||||
|
///
|
||||||
|
/// 适用于同步当前曲目、播放状态、进度等信息。
|
||||||
PlayerStatus(serde_json::Value),
|
PlayerStatus(serde_json::Value),
|
||||||
|
/// 触发器事件。
|
||||||
|
///
|
||||||
|
/// `name` 表示触发器名称,`value` 表示本次触发附带的值,适合按钮、传感器
|
||||||
|
/// 或自动化规则触发。
|
||||||
Trigger {
|
Trigger {
|
||||||
|
/// 触发器名称。
|
||||||
name: String,
|
name: String,
|
||||||
|
/// 触发器携带的值。
|
||||||
value: String,
|
value: String,
|
||||||
},
|
},
|
||||||
|
/// 状态机切换事件。
|
||||||
|
///
|
||||||
|
/// 当系统状态从一个命名状态迁移到另一个状态时使用,便于插件同步行为。
|
||||||
StateChanged {
|
StateChanged {
|
||||||
|
/// 切换前的状态名。
|
||||||
old_state: String,
|
old_state: String,
|
||||||
|
/// 切换后的状态名。
|
||||||
new_state: String,
|
new_state: String,
|
||||||
},
|
},
|
||||||
|
/// 请求屏幕锁定或解锁。
|
||||||
|
///
|
||||||
|
/// `true` 表示请求锁定,`false` 表示请求解除锁定。
|
||||||
ScreenLockRequest(bool),
|
ScreenLockRequest(bool),
|
||||||
|
/// 请求显示或隐藏光标。
|
||||||
|
///
|
||||||
|
/// `true` 表示显示光标,`false` 表示隐藏光标。
|
||||||
CursorVisibility(bool),
|
CursorVisibility(bool),
|
||||||
|
/// Wi-Fi 控制命令。
|
||||||
|
///
|
||||||
|
/// 具体 JSON 字段由网络相关插件或主程序约定。
|
||||||
WifiCommand(serde_json::Value),
|
WifiCommand(serde_json::Value),
|
||||||
|
/// Wi-Fi 操作结果字符串。
|
||||||
|
///
|
||||||
|
/// 一般用于返回简短状态、错误描述或执行结果摘要。
|
||||||
WifiResult(String),
|
WifiResult(String),
|
||||||
|
/// Wi-Fi 配网成功事件。
|
||||||
WifiProvisioned {
|
WifiProvisioned {
|
||||||
|
/// 当前接入的 SSID。
|
||||||
ssid: String,
|
ssid: String,
|
||||||
|
/// 当前设备获取到的 IP 地址。
|
||||||
ip: String,
|
ip: String,
|
||||||
},
|
},
|
||||||
|
/// 配置重载完成通知。
|
||||||
|
///
|
||||||
|
/// JSON 载荷通常是更新后的配置片段或完整配置。
|
||||||
ConfigReloaded(serde_json::Value),
|
ConfigReloaded(serde_json::Value),
|
||||||
|
/// 请求主程序重新加载配置。
|
||||||
ConfigReloadRequest,
|
ConfigReloadRequest,
|
||||||
|
/// 请求插件或主程序进入关闭流程。
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
/// 插件就绪通知。
|
||||||
|
///
|
||||||
|
/// 字符串内容通常为就绪插件的名称。
|
||||||
PluginReady(String),
|
PluginReady(String),
|
||||||
|
/// 自定义业务消息。
|
||||||
|
///
|
||||||
|
/// 当标准消息不足以表达插件间协议时,可通过 `kind` 区分消息类型,
|
||||||
|
/// 并在 `payload` 中承载自定义内容。
|
||||||
Custom {
|
Custom {
|
||||||
|
/// 自定义消息类型标识。
|
||||||
kind: String,
|
kind: String,
|
||||||
|
/// 自定义消息的文本载荷。
|
||||||
payload: String,
|
payload: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
|
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
|
||||||
|
|
||||||
|
/// 插件实例在 FFI 边界上的不透明句柄。
|
||||||
pub type PluginHandle = *mut c_void;
|
pub type PluginHandle = *mut c_void;
|
||||||
|
/// 指向以 null 结尾的 C 字符串。
|
||||||
pub type FfiStr = *const c_char;
|
pub type FfiStr = *const c_char;
|
||||||
|
|
||||||
|
/// ABI 安全的字符串返回类型。
|
||||||
|
///
|
||||||
|
/// 主程序从插件取回 JSON 或错误信息时使用该结构体。内存由插件分配,
|
||||||
|
/// 再通过 `PluginVTable::free_string` 释放。
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct FfiString {
|
pub struct FfiString {
|
||||||
|
/// 字符串起始指针;为空时表示没有内容。
|
||||||
pub ptr: *mut c_char,
|
pub ptr: *mut c_char,
|
||||||
|
/// 字节长度,不包含结尾的 `\0`。
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FfiString {
|
impl FfiString {
|
||||||
|
/// 从 Rust `String` 构造 FFI 字符串。
|
||||||
|
///
|
||||||
|
/// 如果字符串中包含内部 `NUL` 字节,会返回空字符串表示失败。
|
||||||
pub fn from_string(s: String) -> Self {
|
pub fn from_string(s: String) -> Self {
|
||||||
match CString::new(s) {
|
match CString::new(s) {
|
||||||
Ok(cstr) => {
|
Ok(cstr) => {
|
||||||
@@ -99,6 +179,7 @@ impl FfiString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构造空的 FFI 字符串。
|
||||||
pub fn null() -> Self {
|
pub fn null() -> Self {
|
||||||
Self {
|
Self {
|
||||||
ptr: ptr::null_mut(),
|
ptr: ptr::null_mut(),
|
||||||
@@ -121,13 +202,19 @@ impl FfiString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ABI 安全的返回值结构。
|
||||||
|
///
|
||||||
|
/// `code == 0` 表示成功,非零表示失败;`error` 中包含面向开发者的错误文本。
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct FfiResult {
|
pub struct FfiResult {
|
||||||
|
/// 状态码,约定 `0` 为成功,`-1` 为失败。
|
||||||
pub code: c_int,
|
pub code: c_int,
|
||||||
|
/// 错误消息;成功时通常为空字符串。
|
||||||
pub error: FfiString,
|
pub error: FfiString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FfiResult {
|
impl FfiResult {
|
||||||
|
/// 创建表示成功的返回值。
|
||||||
pub fn ok() -> Self {
|
pub fn ok() -> Self {
|
||||||
Self {
|
Self {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -135,6 +222,7 @@ impl FfiResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 创建表示失败的返回值,并附带错误消息。
|
||||||
pub fn err(msg: String) -> Self {
|
pub fn err(msg: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
code: -1,
|
code: -1,
|
||||||
@@ -143,42 +231,91 @@ impl FfiResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 主程序提供给插件的消息发送回调。
|
||||||
|
///
|
||||||
|
/// 插件通常无需直接调用该类型,而是通过 [`MessageSender`] 使用安全封装。
|
||||||
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
|
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
|
||||||
|
|
||||||
|
/// 插件导出给主程序的函数表。
|
||||||
|
///
|
||||||
|
/// 该结构与主程序中的 ABI 定义一一对应。普通插件作者一般不需要手动构造,
|
||||||
|
/// 使用 [`export_plugin!`] 宏即可自动生成。
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct PluginVTable {
|
pub struct PluginVTable {
|
||||||
|
/// 创建插件实例。
|
||||||
pub create: unsafe extern "C" fn() -> PluginHandle,
|
pub create: unsafe extern "C" fn() -> PluginHandle,
|
||||||
|
/// 获取 [`PluginInfo`] 的 JSON 序列化结果。
|
||||||
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(
|
pub init: unsafe extern "C" fn(
|
||||||
handle: PluginHandle,
|
handle: PluginHandle,
|
||||||
config_json: FfiStr,
|
config_json: FfiStr,
|
||||||
send_ctx: *mut c_void,
|
send_ctx: *mut c_void,
|
||||||
send_cb: SendCallback,
|
send_cb: SendCallback,
|
||||||
) -> FfiResult,
|
) -> FfiResult,
|
||||||
|
/// 启动插件。
|
||||||
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
|
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
|
||||||
|
/// 处理一条 JSON 序列化消息。
|
||||||
pub handle_message:
|
pub handle_message:
|
||||||
unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
|
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 free_string: unsafe extern "C" fn(s: FfiString),
|
pub free_string: unsafe extern "C" fn(s: FfiString),
|
||||||
|
/// 销毁插件实例。
|
||||||
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
|
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
|
||||||
|
/// 获取插件能力列表的 JSON 序列化结果。
|
||||||
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
|
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
|
||||||
|
/// 获取插件自检结果列表的 JSON 序列化结果。
|
||||||
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
|
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 高级接口:插件作者实现此 trait ──
|
// ── 高级接口:插件作者实现此 trait ──
|
||||||
|
|
||||||
/// 消息发送器 — 封装 SendCallback,提供安全的 Rust API
|
/// 消息发送器,封装底层 FFI 回调并提供安全的 Rust API。
|
||||||
|
///
|
||||||
|
/// 插件在 [`ShowenPlugin::init`] 中会收到一个 `MessageSender`,之后可将其保存到
|
||||||
|
/// 插件状态中,供运行期间向其他插件或主程序发送消息。
|
||||||
pub struct MessageSender {
|
pub struct MessageSender {
|
||||||
ctx: *mut c_void,
|
ctx: *mut c_void,
|
||||||
cb: SendCallback,
|
cb: SendCallback,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageSender {
|
impl MessageSender {
|
||||||
|
/// 基于底层发送上下文和回调创建发送器。
|
||||||
|
///
|
||||||
|
/// 普通插件作者通常不需要手动调用该方法;主程序在初始化插件时会自动构造。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{MessageSender, SendCallback};
|
||||||
|
/// use std::ffi::c_void;
|
||||||
|
///
|
||||||
|
/// unsafe extern "C" fn callback(_: *mut c_void, _: *const std::ffi::c_char) {}
|
||||||
|
///
|
||||||
|
/// let sender = MessageSender::new(std::ptr::null_mut(), callback as SendCallback);
|
||||||
|
/// # let _ = sender;
|
||||||
|
/// ```
|
||||||
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
|
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
|
||||||
Self { ctx, cb }
|
Self { ctx, cb }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送消息信封到主程序
|
/// 发送完整的消息信封。
|
||||||
|
///
|
||||||
|
/// 当你已经手动构造好 [`Envelope`],或者需要完全控制发送者、目标和消息载荷时,
|
||||||
|
/// 直接使用此方法最合适。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{Destination, Envelope, Message, MessageSender};
|
||||||
|
///
|
||||||
|
/// # let sender: MessageSender = unimplemented!();
|
||||||
|
/// sender.send(&Envelope {
|
||||||
|
/// from: "clock".to_string(),
|
||||||
|
/// to: Destination::Manager,
|
||||||
|
/// message: Message::PluginReady("clock".to_string()),
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
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) {
|
||||||
@@ -187,7 +324,24 @@ impl MessageSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 便捷方法:发送消息给指定插件
|
/// 发送消息给指定插件。
|
||||||
|
///
|
||||||
|
/// 这是构造 [`Destination::Plugin`] 的便捷方法,适合点对点通信。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{Message, MessageSender};
|
||||||
|
///
|
||||||
|
/// # let sender: MessageSender = unimplemented!();
|
||||||
|
/// sender.send_to(
|
||||||
|
/// "clock",
|
||||||
|
/// "player",
|
||||||
|
/// Message::Custom {
|
||||||
|
/// kind: "sync-request".to_string(),
|
||||||
|
/// payload: "{}".to_string(),
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
pub fn send_to(&self, from: &str, to_plugin: &str, message: Message) {
|
pub fn send_to(&self, from: &str, to_plugin: &str, message: Message) {
|
||||||
self.send(&Envelope {
|
self.send(&Envelope {
|
||||||
from: from.to_string(),
|
from: from.to_string(),
|
||||||
@@ -196,7 +350,23 @@ impl MessageSender {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 便捷方法:广播消息
|
/// 广播消息给所有接收方。
|
||||||
|
///
|
||||||
|
/// 适合发送系统事件、状态变更或多个插件都可能关心的通知。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{Message, MessageSender};
|
||||||
|
///
|
||||||
|
/// # let sender: MessageSender = unimplemented!();
|
||||||
|
/// sender.broadcast(
|
||||||
|
/// "network",
|
||||||
|
/// Message::WifiProvisioned {
|
||||||
|
/// ssid: "Office-WiFi".to_string(),
|
||||||
|
/// ip: "192.168.1.8".to_string(),
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
pub fn broadcast(&self, from: &str, message: Message) {
|
pub fn broadcast(&self, from: &str, message: Message) {
|
||||||
self.send(&Envelope {
|
self.send(&Envelope {
|
||||||
from: from.to_string(),
|
from: from.to_string(),
|
||||||
@@ -205,7 +375,20 @@ impl MessageSender {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 便捷方法:发送消息给管理层
|
/// 发送消息给主程序管理层。
|
||||||
|
///
|
||||||
|
/// 适用于请求重载配置、上报就绪状态、或提交不属于单个插件的系统级事件。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{Message, MessageSender};
|
||||||
|
///
|
||||||
|
/// # let sender: MessageSender = unimplemented!();
|
||||||
|
/// sender.send_to_manager(
|
||||||
|
/// "config-watcher",
|
||||||
|
/// Message::ConfigReloadRequest,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
pub fn send_to_manager(&self, from: &str, message: Message) {
|
pub fn send_to_manager(&self, from: &str, message: Message) {
|
||||||
self.send(&Envelope {
|
self.send(&Envelope {
|
||||||
from: from.to_string(),
|
from: from.to_string(),
|
||||||
@@ -219,18 +402,91 @@ impl MessageSender {
|
|||||||
unsafe impl Send for MessageSender {}
|
unsafe impl Send for MessageSender {}
|
||||||
unsafe impl Sync for MessageSender {}
|
unsafe impl Sync for MessageSender {}
|
||||||
|
|
||||||
/// 动态插件 trait — 插件作者实现此接口
|
/// 动态插件的高层 Rust 接口。
|
||||||
|
///
|
||||||
|
/// 第三方插件作者只需要实现此 trait,再调用 [`export_plugin!`] 宏,即可导出一个
|
||||||
|
/// 可被主程序加载的动态库。主程序会按照 `info -> init -> start -> handle_message -> stop`
|
||||||
|
/// 的生命周期驱动插件。
|
||||||
pub trait ShowenPlugin: Send {
|
pub trait ShowenPlugin: Send {
|
||||||
/// 插件信息
|
/// 返回插件的静态元信息。
|
||||||
|
///
|
||||||
|
/// 主程序会在插件加载后尽早调用此方法,用于识别插件、展示描述并进行兼容性判断。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
|
||||||
|
///
|
||||||
|
/// struct DemoPlugin;
|
||||||
|
///
|
||||||
|
/// impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// fn info(&self) -> PluginInfo {
|
||||||
|
/// PluginInfo {
|
||||||
|
/// name: "demo".to_string(),
|
||||||
|
/// version: "0.1.0".to_string(),
|
||||||
|
/// description: "Example plugin".to_string(),
|
||||||
|
/// platform: "linux".to_string(),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
|
||||||
|
/// fn start(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
fn info(&self) -> PluginInfo;
|
fn info(&self) -> PluginInfo;
|
||||||
|
|
||||||
/// 声明插件支持的功能列表(默认空)
|
/// 声明插件支持的能力列表。
|
||||||
|
///
|
||||||
|
/// 能力字符串通常用于主程序展示、诊断以及自测分组。默认实现返回空列表。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin;
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// fn capabilities(&self) -> Vec<String> {
|
||||||
|
/// vec!["wifi.scan".to_string(), "wifi.connect".to_string()]
|
||||||
|
/// }
|
||||||
|
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn capabilities(&self) -> Vec<String> {
|
fn capabilities(&self) -> Vec<String> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 运行自测,返回每项功能的测试结果
|
/// 运行插件自测,返回每项能力的测试结果。
|
||||||
/// 默认实现:所有声明的 capability 均标记为通过
|
///
|
||||||
|
/// 默认实现会把 `capabilities` 返回的每个能力都标记为通过,并给出
|
||||||
|
/// `"no test defined"` 提示。若插件依赖外部设备、系统命令或网络状态,建议覆写此方法。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{CapabilityTestResult, PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin;
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
||||||
|
/// vec![CapabilityTestResult {
|
||||||
|
/// capability: "wifi.scan".to_string(),
|
||||||
|
/// passed: true,
|
||||||
|
/// message: "scan backend reachable".to_string(),
|
||||||
|
/// }]
|
||||||
|
/// }
|
||||||
|
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
||||||
self.capabilities()
|
self.capabilities()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -242,30 +498,193 @@ pub trait ShowenPlugin: Send {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化,收到配置 JSON 和消息发送器
|
/// 初始化插件。
|
||||||
|
///
|
||||||
|
/// 主程序会把插件配置的 JSON 文本和一个 [`MessageSender`] 传入。插件通常在此阶段
|
||||||
|
/// 解析配置、保存发送器、准备运行所需资源,但不应启动长期运行任务。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{MessageSender, PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin {
|
||||||
|
/// # sender: Option<MessageSender>,
|
||||||
|
/// # }
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> {
|
||||||
|
/// let _config: serde_json::Value =
|
||||||
|
/// serde_json::from_str(config_json).map_err(|e| e.to_string())?;
|
||||||
|
/// self.sender = Some(sender);
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
|
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
|
||||||
|
|
||||||
/// 启动
|
/// 启动插件。
|
||||||
|
///
|
||||||
|
/// 在 `init` 成功之后调用。适合在这里启动后台线程、注册监听器或发送
|
||||||
|
/// [`Message::PluginReady`] 等就绪通知。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{Message, MessageSender, PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin {
|
||||||
|
/// # sender: Option<MessageSender>,
|
||||||
|
/// # }
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// # fn init(&mut self, _: &str, sender: MessageSender) -> Result<(), String> { self.sender = Some(sender); Ok(()) }
|
||||||
|
/// fn start(&mut self) -> Result<(), String> {
|
||||||
|
/// if let Some(sender) = &self.sender {
|
||||||
|
/// sender.send_to_manager("demo", Message::PluginReady("demo".to_string()));
|
||||||
|
/// }
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn start(&mut self) -> Result<(), String>;
|
fn start(&mut self) -> Result<(), String>;
|
||||||
|
|
||||||
/// 处理消息 JSON(已反序列化为 Message)
|
/// 处理主程序转发给插件的消息。
|
||||||
|
///
|
||||||
|
/// 进入此方法前,JSON 已经被 SDK 反序列化为 [`Message`]。插件应只匹配自己关心的
|
||||||
|
/// 消息变体,并在必要时返回错误字符串帮助定位问题。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{Message, PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin;
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// fn handle_message(&mut self, message: Message) -> Result<(), String> {
|
||||||
|
/// match message {
|
||||||
|
/// Message::Shutdown => Ok(()),
|
||||||
|
/// Message::Custom { kind, payload } if kind == "sync" => {
|
||||||
|
/// let _ = payload;
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// _ => Ok(()),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn handle_message(&mut self, message: Message) -> Result<(), String>;
|
fn handle_message(&mut self, message: Message) -> Result<(), String>;
|
||||||
|
|
||||||
/// 停止
|
/// 停止插件并释放运行期资源。
|
||||||
|
///
|
||||||
|
/// 该方法通常用于停止后台线程、撤销监听、关闭文件句柄或网络连接。执行完成后,
|
||||||
|
/// 主程序可能很快销毁插件实例。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// # use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
|
||||||
|
/// # struct DemoPlugin {
|
||||||
|
/// # running: bool,
|
||||||
|
/// # }
|
||||||
|
/// # impl ShowenPlugin for DemoPlugin {
|
||||||
|
/// # fn info(&self) -> PluginInfo {
|
||||||
|
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
|
||||||
|
/// # }
|
||||||
|
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
|
||||||
|
/// # fn start(&mut self) -> Result<(), String> { self.running = true; Ok(()) }
|
||||||
|
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
|
||||||
|
/// fn stop(&mut self) -> Result<(), String> {
|
||||||
|
/// self.running = false;
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
fn stop(&mut self) -> Result<(), String>;
|
fn stop(&mut self) -> Result<(), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 导出宏:自动生成 extern "C" 胶水代码 ──
|
// ── 导出宏:自动生成 extern "C" 胶水代码 ──
|
||||||
|
|
||||||
/// 将 ShowenPlugin 实现导出为 C FFI 接口
|
/// 将 [`ShowenPlugin`] 实现导出为主程序可加载的 C ABI 入口。
|
||||||
///
|
///
|
||||||
/// # 用法
|
/// 该宏会自动生成完整的 `extern "C"` 胶水代码,包括:
|
||||||
|
///
|
||||||
|
/// - 创建与销毁插件实例
|
||||||
|
/// - 把 `PluginInfo`、capabilities、自测结果序列化为 JSON
|
||||||
|
/// - 在 ABI 边界上捕获 panic,避免传播到主程序
|
||||||
|
/// - 把传入的 JSON 消息反序列化为 [`Message`] 后再调用 trait 方法
|
||||||
|
/// - 导出主程序约定名称的 `showen_plugin_vtable`
|
||||||
|
///
|
||||||
|
/// 传入参数:
|
||||||
|
///
|
||||||
|
/// - 第一个参数是插件具体类型
|
||||||
|
/// - 第二个参数是无参构造表达式,例如 `MyPlugin::new()`
|
||||||
|
///
|
||||||
|
/// 使用此宏时,插件类型必须实现 [`ShowenPlugin`],且构造表达式应返回该类型实例。
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// struct MyPlugin { ... }
|
/// use showen_plugin_sdk::{
|
||||||
/// impl ShowenPlugin for MyPlugin { ... }
|
/// export_plugin, Message, MessageSender, PluginInfo, ShowenPlugin,
|
||||||
|
/// };
|
||||||
///
|
///
|
||||||
/// export_plugin!(MyPlugin, MyPlugin::new);
|
/// struct MyPlugin {
|
||||||
|
/// sender: Option<MessageSender>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl MyPlugin {
|
||||||
|
/// fn new() -> Self {
|
||||||
|
/// Self { sender: None }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl ShowenPlugin for MyPlugin {
|
||||||
|
/// fn info(&self) -> PluginInfo {
|
||||||
|
/// PluginInfo {
|
||||||
|
/// name: "my-plugin".to_string(),
|
||||||
|
/// version: "0.1.0".to_string(),
|
||||||
|
/// description: "Example ShowenV2 plugin".to_string(),
|
||||||
|
/// platform: "linux".to_string(),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn init(&mut self, _config_json: &str, sender: MessageSender) -> Result<(), String> {
|
||||||
|
/// self.sender = Some(sender);
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn start(&mut self) -> Result<(), String> {
|
||||||
|
/// if let Some(sender) = &self.sender {
|
||||||
|
/// sender.send_to_manager(
|
||||||
|
/// "my-plugin",
|
||||||
|
/// Message::PluginReady("my-plugin".to_string()),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn handle_message(&mut self, _message: Message) -> Result<(), String> {
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn stop(&mut self) -> Result<(), String> {
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// export_plugin!(MyPlugin, MyPlugin::new());
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// 生成的 `showen_plugin_vtable` 会被主程序在加载动态库后自动发现,因此插件 crate
|
||||||
|
/// 通常只需在 `lib.rs` 末尾调用一次该宏。
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! export_plugin {
|
macro_rules! export_plugin {
|
||||||
($plugin_type:ty, $constructor:expr) => {
|
($plugin_type:ty, $constructor:expr) => {
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
showen-plugin-sdk = { path = "../../plugin-sdk" }
|
showen-plugin-sdk = { path = "../../plugin-sdk" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -1,14 +1,128 @@
|
|||||||
//! 示例动态插件 — 展示如何使用 showen-plugin-sdk 编写插件
|
//! 示例动态插件 — 展示如何使用 `showen-plugin-sdk` 编写较完整的插件。
|
||||||
//!
|
//!
|
||||||
//! 此插件演示动态加载流程及自测机制。
|
//! 这个示例特意覆盖几个第三方开发者最常见的需求:
|
||||||
|
//! 1. 使用 `MessageSender` 发送点对点、广播、管理层以及原始 `Envelope` 消息。
|
||||||
|
//! 2. 在 `handle_message` 中处理多种 `Message` 变体。
|
||||||
|
//! 3. 用 `serde` 解析配置,并在解析后做显式校验。
|
||||||
|
//! 4. 用 `thread + sleep` 模拟一个简单的定时后台任务。
|
||||||
|
//! 5. 提供完整的 `capabilities` 和 `self_test` 实现。
|
||||||
|
//! 6. 用注释解释每个阶段应该做什么,作为插件作者的参考模板。
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use showen_plugin_sdk::{
|
use showen_plugin_sdk::{
|
||||||
export_plugin, CapabilityTestResult, Message, MessageSender, PluginInfo, ShowenPlugin,
|
export_plugin, CapabilityTestResult, Destination, Envelope, Message, MessageSender, PluginInfo,
|
||||||
|
ShowenPlugin,
|
||||||
};
|
};
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use std::thread::{self, JoinHandle};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const PLUGIN_ID: &str = "example-plugin";
|
||||||
|
const CAP_MESSAGE_SENDER: &str = "message_sender";
|
||||||
|
const CAP_MESSAGE_ROUTING: &str = "message_routing";
|
||||||
|
const CAP_CONFIG_PARSING: &str = "config_parsing";
|
||||||
|
const CAP_BACKGROUND_TASK: &str = "background_task";
|
||||||
|
const CAP_SELF_TEST: &str = "self_test";
|
||||||
|
|
||||||
|
/// 示例插件配置。
|
||||||
|
///
|
||||||
|
/// 这里使用 `#[serde(default)]` 而不是依赖调用方传完整配置,目的是让示例更健壮:
|
||||||
|
/// - 新增字段后,旧配置仍然可以工作。
|
||||||
|
/// - 每个字段都有清晰的默认值。
|
||||||
|
/// - `validate()` 负责做业务约束校验,把解析错误和业务错误分开。
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
struct ExamplePluginConfig {
|
||||||
|
/// 后台任务的心跳间隔,单位毫秒。
|
||||||
|
heartbeat_interval_ms: u64,
|
||||||
|
/// 启动后示例消息要发给哪个插件。
|
||||||
|
target_plugin: String,
|
||||||
|
/// 是否在 `start()` 时发送一组教学用途的示例消息。
|
||||||
|
announce_on_start: bool,
|
||||||
|
/// 是否启用后台定时任务。
|
||||||
|
enable_periodic_task: bool,
|
||||||
|
/// 周期性上报里携带的示例文本。
|
||||||
|
periodic_payload: String,
|
||||||
|
/// 用来演示可配置的自测失败项。
|
||||||
|
optional_test_should_fail: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExamplePluginConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
heartbeat_interval_ms: 5_000,
|
||||||
|
target_plugin: PLUGIN_ID.to_string(),
|
||||||
|
announce_on_start: true,
|
||||||
|
enable_periodic_task: true,
|
||||||
|
periodic_payload: "heartbeat-from-example-plugin".to_string(),
|
||||||
|
optional_test_should_fail: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExamplePluginConfig {
|
||||||
|
/// 解析 JSON 配置,并在成功后执行额外校验。
|
||||||
|
fn from_json(config_json: &str) -> Result<Self, String> {
|
||||||
|
let trimmed = config_json.trim();
|
||||||
|
let mut config = if trimmed.is_empty() || trimmed == "null" {
|
||||||
|
Self::default()
|
||||||
|
} else {
|
||||||
|
serde_json::from_str::<Self>(trimmed)
|
||||||
|
.map_err(|error| format!("failed to parse example plugin config: {error}"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
config.validate()?;
|
||||||
|
config.target_plugin = config.target_plugin.trim().to_string();
|
||||||
|
config.periodic_payload = config.periodic_payload.trim().to_string();
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 `ConfigReloaded` 的场景,调用方式与初始化时保持一致。
|
||||||
|
fn from_value(value: serde_json::Value) -> Result<Self, String> {
|
||||||
|
let mut config = serde_json::from_value::<Self>(value)
|
||||||
|
.map_err(|error| format!("failed to decode reloaded config: {error}"))?;
|
||||||
|
config.validate()?;
|
||||||
|
config.target_plugin = config.target_plugin.trim().to_string();
|
||||||
|
config.periodic_payload = config.periodic_payload.trim().to_string();
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<(), String> {
|
||||||
|
if self.target_plugin.trim().is_empty() {
|
||||||
|
return Err("config field `target_plugin` must not be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.periodic_payload.trim().is_empty() {
|
||||||
|
return Err("config field `periodic_payload` must not be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enable_periodic_task && self.heartbeat_interval_ms < 100 {
|
||||||
|
return Err(
|
||||||
|
"config field `heartbeat_interval_ms` must be at least 100 when the periodic task is enabled"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一个更完整的示例插件实现。
|
||||||
pub struct ExamplePlugin {
|
pub struct ExamplePlugin {
|
||||||
sender: Option<MessageSender>,
|
/// `MessageSender` 会在 `init()` 时由宿主注入。
|
||||||
/// 用于演示可配置的自测失败
|
///
|
||||||
|
/// 用 `Arc` 包起来是为了后台线程可以安全共享同一个 sender。
|
||||||
|
sender: Option<Arc<MessageSender>>,
|
||||||
|
/// 当前生效配置。
|
||||||
|
config: ExamplePluginConfig,
|
||||||
|
/// 用于结束后台线程的停止信号。
|
||||||
|
worker_stop: Arc<AtomicBool>,
|
||||||
|
/// 后台线程句柄;在 `stop()` 和配置重载时负责回收。
|
||||||
|
worker: Option<JoinHandle<()>>,
|
||||||
|
/// 用于演示 `self_test()` 如何暴露可选失败项。
|
||||||
fail_optional_test: bool,
|
fail_optional_test: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,85 +130,507 @@ impl ExamplePlugin {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
sender: None,
|
sender: None,
|
||||||
|
config: ExamplePluginConfig::default(),
|
||||||
|
worker_stop: Arc::new(AtomicBool::new(false)),
|
||||||
|
worker: None,
|
||||||
fail_optional_test: false,
|
fail_optional_test: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sender(&self) -> Result<&Arc<MessageSender>, String> {
|
||||||
|
self.sender
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| "plugin sender is not initialized; call init() first".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送一个“插件已准备好”的管理消息。
|
||||||
|
///
|
||||||
|
/// 这是插件初始化阶段最常见的通知类型之一。
|
||||||
|
fn notify_ready(&self) -> Result<(), String> {
|
||||||
|
self.sender()?
|
||||||
|
.send_to_manager(PLUGIN_ID, Message::PluginReady(PLUGIN_ID.to_string()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 演示 `MessageSender` 的四种常见用法:
|
||||||
|
/// - `send_to_manager()`
|
||||||
|
/// - `broadcast()`
|
||||||
|
/// - `send_to()`
|
||||||
|
/// - `send()` + 原始 `Envelope`
|
||||||
|
fn emit_demo_messages(&self) -> Result<(), String> {
|
||||||
|
let sender = self.sender()?;
|
||||||
|
|
||||||
|
sender.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.lifecycle".to_string(),
|
||||||
|
payload: "start() completed".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
sender.broadcast(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::StateChanged {
|
||||||
|
old_state: "initialized".to_string(),
|
||||||
|
new_state: "running".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
sender.send_to(
|
||||||
|
PLUGIN_ID,
|
||||||
|
&self.config.target_plugin,
|
||||||
|
Message::Trigger {
|
||||||
|
name: "example.handshake".to_string(),
|
||||||
|
value: "hello-from-example-plugin".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let config_payload = serde_json::to_string(&self.config)
|
||||||
|
.map_err(|error| format!("failed to serialize config snapshot: {error}"))?;
|
||||||
|
sender.send(&Envelope {
|
||||||
|
from: PLUGIN_ID.to_string(),
|
||||||
|
to: Destination::Manager,
|
||||||
|
message: Message::Custom {
|
||||||
|
kind: "example.config_snapshot".to_string(),
|
||||||
|
payload: config_payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动一个简单的后台线程,周期性向管理层发送心跳消息。
|
||||||
|
///
|
||||||
|
/// 这里故意使用 `thread::sleep`,因为这是第三方插件开发者最容易理解的最小示例。
|
||||||
|
fn start_worker(&mut self) -> Result<(), String> {
|
||||||
|
self.stop_worker()?;
|
||||||
|
|
||||||
|
if !self.config.enable_periodic_task {
|
||||||
|
eprintln!("[ExamplePlugin] periodic task disabled by config");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let sender = Arc::clone(self.sender()?);
|
||||||
|
let stop_flag = Arc::clone(&self.worker_stop);
|
||||||
|
let interval = Duration::from_millis(self.config.heartbeat_interval_ms);
|
||||||
|
let payload = self.config.periodic_payload.clone();
|
||||||
|
|
||||||
|
stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
self.worker = Some(thread::spawn(move || {
|
||||||
|
let mut tick = 0_u64;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
thread::sleep(interval);
|
||||||
|
if stop_flag.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick += 1;
|
||||||
|
sender.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.heartbeat".to_string(),
|
||||||
|
payload: format!("{{\"tick\":{tick},\"payload\":{payload:?}}}"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止后台线程并回收资源。
|
||||||
|
fn stop_worker(&mut self) -> Result<(), String> {
|
||||||
|
self.worker_stop.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if let Some(handle) = self.worker.take() {
|
||||||
|
handle
|
||||||
|
.join()
|
||||||
|
.map_err(|_| "example background worker panicked".to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 统一处理触发器消息,让 `handle_message()` 保持可读性。
|
||||||
|
fn handle_trigger(&mut self, name: String, value: String) -> Result<(), String> {
|
||||||
|
eprintln!("[ExamplePlugin] trigger received: {name}={value}");
|
||||||
|
|
||||||
|
match name.as_str() {
|
||||||
|
"example.report_self_test" => {
|
||||||
|
let payload = serde_json::to_string(&self.self_test())
|
||||||
|
.map_err(|error| format!("failed to serialize self_test results: {error}"))?;
|
||||||
|
self.sender()?.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.self_test_report".to_string(),
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"example.send_demo_messages" => {
|
||||||
|
self.emit_demo_messages()?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.sender()?.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.trigger_ack".to_string(),
|
||||||
|
payload: format!("unknown trigger ignored: {name}={value}"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理配置热重载。
|
||||||
|
///
|
||||||
|
/// 最佳实践:
|
||||||
|
/// 1. 先解析并校验新配置。
|
||||||
|
/// 2. 仅在成功后替换运行时状态。
|
||||||
|
/// 3. 如果配置会影响后台任务,重启对应任务。
|
||||||
|
fn reload_config(&mut self, value: serde_json::Value) -> Result<(), String> {
|
||||||
|
let next_config = ExamplePluginConfig::from_value(value)?;
|
||||||
|
self.config = next_config;
|
||||||
|
self.fail_optional_test = self.config.optional_test_should_fail;
|
||||||
|
self.start_worker()?;
|
||||||
|
|
||||||
|
let payload = serde_json::to_string(&self.config)
|
||||||
|
.map_err(|error| format!("failed to serialize reloaded config: {error}"))?;
|
||||||
|
self.sender()?.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.config_reloaded".to_string(),
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShowenPlugin for ExamplePlugin {
|
impl ShowenPlugin for ExamplePlugin {
|
||||||
fn info(&self) -> PluginInfo {
|
fn info(&self) -> PluginInfo {
|
||||||
PluginInfo {
|
PluginInfo {
|
||||||
name: "example-plugin".to_string(),
|
name: PLUGIN_ID.to_string(),
|
||||||
version: "0.1.0".to_string(),
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
description: "示例动态插件".to_string(),
|
description: "Feature-complete example plugin for third-party developers".to_string(),
|
||||||
platform: "Any".to_string(),
|
platform: "Any".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capabilities(&self) -> Vec<String> {
|
fn capabilities(&self) -> Vec<String> {
|
||||||
vec!["logging".to_string(), "metrics".to_string()]
|
vec![
|
||||||
|
CAP_MESSAGE_SENDER.to_string(),
|
||||||
|
CAP_MESSAGE_ROUTING.to_string(),
|
||||||
|
CAP_CONFIG_PARSING.to_string(),
|
||||||
|
CAP_BACKGROUND_TASK.to_string(),
|
||||||
|
CAP_SELF_TEST.to_string(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
||||||
|
let config_ok = self.config.validate().is_ok();
|
||||||
|
let worker_ok = !self.config.enable_periodic_task || self.worker.is_some();
|
||||||
|
let sender_ready = self.sender.is_some();
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
CapabilityTestResult {
|
CapabilityTestResult {
|
||||||
capability: "logging".to_string(),
|
capability: CAP_MESSAGE_SENDER.to_string(),
|
||||||
passed: true,
|
passed: sender_ready,
|
||||||
message: "log output verified".to_string(),
|
message: if sender_ready {
|
||||||
|
"MessageSender injected during init()".to_string()
|
||||||
|
} else {
|
||||||
|
"MessageSender not available yet".to_string()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
CapabilityTestResult {
|
CapabilityTestResult {
|
||||||
capability: "metrics".to_string(),
|
capability: CAP_MESSAGE_ROUTING.to_string(),
|
||||||
|
passed: sender_ready,
|
||||||
|
message: "send_to_manager / broadcast / send_to / send are implemented".to_string(),
|
||||||
|
},
|
||||||
|
CapabilityTestResult {
|
||||||
|
capability: CAP_CONFIG_PARSING.to_string(),
|
||||||
|
passed: config_ok,
|
||||||
|
message: if config_ok {
|
||||||
|
"config parsed with serde defaults + validation".to_string()
|
||||||
|
} else {
|
||||||
|
"current config failed validation".to_string()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CapabilityTestResult {
|
||||||
|
capability: CAP_BACKGROUND_TASK.to_string(),
|
||||||
|
passed: worker_ok,
|
||||||
|
message: if self.config.enable_periodic_task {
|
||||||
|
if worker_ok {
|
||||||
|
"periodic worker thread is running".to_string()
|
||||||
|
} else {
|
||||||
|
"periodic worker thread is not running".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"periodic task disabled by config".to_string()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CapabilityTestResult {
|
||||||
|
capability: CAP_SELF_TEST.to_string(),
|
||||||
passed: !self.fail_optional_test,
|
passed: !self.fail_optional_test,
|
||||||
message: if self.fail_optional_test {
|
message: if self.fail_optional_test {
|
||||||
"metrics backend unreachable".to_string()
|
"optional self-test failure requested by config".to_string()
|
||||||
} else {
|
} else {
|
||||||
"metrics endpoint ok".to_string()
|
"self-test harness operational".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!(
|
||||||
self.sender = Some(sender);
|
"[ExamplePlugin] init called, received config bytes: {}",
|
||||||
|
config_json.len()
|
||||||
|
);
|
||||||
|
|
||||||
// 通知主程序就绪
|
let config = ExamplePluginConfig::from_json(config_json)?;
|
||||||
if let Some(sender) = &self.sender {
|
self.config = config;
|
||||||
sender.send_to_manager(
|
self.fail_optional_test = self.config.optional_test_should_fail;
|
||||||
"example-plugin",
|
self.sender = Some(Arc::new(sender));
|
||||||
Message::PluginReady("example-plugin".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
self.notify_ready()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&mut self) -> Result<(), String> {
|
fn start(&mut self) -> Result<(), String> {
|
||||||
eprintln!("[ExamplePlugin] started");
|
eprintln!("[ExamplePlugin] start called");
|
||||||
|
|
||||||
|
if self.config.announce_on_start {
|
||||||
|
self.emit_demo_messages()?;
|
||||||
|
}
|
||||||
|
self.start_worker()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_message(&mut self, message: Message) -> Result<(), String> {
|
fn handle_message(&mut self, message: Message) -> Result<(), String> {
|
||||||
match &message {
|
match message {
|
||||||
|
Message::PlayerCommand(payload) => {
|
||||||
|
eprintln!("[ExamplePlugin] player command: {payload}");
|
||||||
|
}
|
||||||
|
Message::PlayerStatus(payload) => {
|
||||||
|
eprintln!("[ExamplePlugin] player status: {payload}");
|
||||||
|
}
|
||||||
|
Message::Trigger { name, value } => {
|
||||||
|
self.handle_trigger(name, value)?;
|
||||||
|
}
|
||||||
|
Message::StateChanged {
|
||||||
|
old_state,
|
||||||
|
new_state,
|
||||||
|
} => {
|
||||||
|
eprintln!("[ExamplePlugin] state changed: {old_state} -> {new_state}");
|
||||||
|
}
|
||||||
|
Message::ScreenLockRequest(locked) => {
|
||||||
|
eprintln!("[ExamplePlugin] screen lock requested: {locked}");
|
||||||
|
}
|
||||||
|
Message::CursorVisibility(visible) => {
|
||||||
|
eprintln!("[ExamplePlugin] cursor visibility requested: {visible}");
|
||||||
|
}
|
||||||
|
Message::WifiCommand(payload) => {
|
||||||
|
eprintln!("[ExamplePlugin] wifi command payload: {payload}");
|
||||||
|
}
|
||||||
|
Message::WifiResult(result) => {
|
||||||
|
eprintln!("[ExamplePlugin] wifi result: {result}");
|
||||||
|
}
|
||||||
|
Message::WifiProvisioned { ssid, ip } => {
|
||||||
|
eprintln!("[ExamplePlugin] wifi provisioned: ssid={ssid}, ip={ip}");
|
||||||
|
self.sender()?.broadcast(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.wifi_provisioned".to_string(),
|
||||||
|
payload: format!("ssid={ssid}, ip={ip}"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Message::ConfigReloaded(config) => {
|
||||||
|
self.reload_config(config)?;
|
||||||
|
}
|
||||||
|
Message::ConfigReloadRequest => {
|
||||||
|
self.sender()?.send_to_manager(
|
||||||
|
PLUGIN_ID,
|
||||||
|
Message::Custom {
|
||||||
|
kind: "example.reload_request".to_string(),
|
||||||
|
payload: "host should respond with ConfigReloaded(JSON)".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Message::Shutdown => {
|
Message::Shutdown => {
|
||||||
eprintln!("[ExamplePlugin] received shutdown");
|
eprintln!("[ExamplePlugin] shutdown requested");
|
||||||
|
self.stop()?;
|
||||||
|
}
|
||||||
|
Message::PluginReady(plugin_name) => {
|
||||||
|
eprintln!("[ExamplePlugin] observed peer readiness: {plugin_name}");
|
||||||
}
|
}
|
||||||
Message::Custom { kind, payload } => {
|
Message::Custom { kind, payload } => {
|
||||||
eprintln!("[ExamplePlugin] custom message: kind={kind}, payload={payload}");
|
eprintln!("[ExamplePlugin] custom message: kind={kind}, payload={payload}");
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
eprintln!("[ExamplePlugin] received message: {:?}", message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), String> {
|
fn stop(&mut self) -> Result<(), String> {
|
||||||
eprintln!("[ExamplePlugin] stopped");
|
eprintln!("[ExamplePlugin] stop called");
|
||||||
|
self.stop_worker()?;
|
||||||
self.sender = None;
|
self.sender = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出 FFI 接口
|
// 导出动态插件 FFI 接口。
|
||||||
export_plugin!(ExamplePlugin, ExamplePlugin::new());
|
export_plugin!(ExamplePlugin, ExamplePlugin::new());
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::ffi::{c_char, c_void, CStr};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
struct Recorder {
|
||||||
|
envelopes: Box<Mutex<Vec<Envelope>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recorder {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
envelopes: Box::new(Mutex::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sender(&self) -> MessageSender {
|
||||||
|
let ctx = (&*self.envelopes as *const Mutex<Vec<Envelope>>) as *mut c_void;
|
||||||
|
MessageSender::new(ctx, record_envelope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self) -> Vec<Envelope> {
|
||||||
|
self.envelopes
|
||||||
|
.lock()
|
||||||
|
.expect("recorder mutex poisoned")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn record_envelope(ctx: *mut c_void, envelope_json: *const c_char) {
|
||||||
|
let storage = unsafe { &*(ctx as *const Mutex<Vec<Envelope>>) };
|
||||||
|
let raw = unsafe { CStr::from_ptr(envelope_json) }
|
||||||
|
.to_str()
|
||||||
|
.expect("callback JSON should be valid UTF-8");
|
||||||
|
let envelope =
|
||||||
|
serde_json::from_str::<Envelope>(raw).expect("callback JSON should decode to Envelope");
|
||||||
|
storage
|
||||||
|
.lock()
|
||||||
|
.expect("recorder mutex poisoned")
|
||||||
|
.push(envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_defaults_when_json_is_empty() {
|
||||||
|
assert_eq!(
|
||||||
|
ExamplePluginConfig::from_json(" ").expect("empty config should use defaults"),
|
||||||
|
ExamplePluginConfig::default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_rejects_too_small_heartbeat_interval() {
|
||||||
|
let error = ExamplePluginConfig::from_json(
|
||||||
|
r#"{"heartbeat_interval_ms":99,"enable_periodic_task":true}"#,
|
||||||
|
)
|
||||||
|
.expect_err("config should be rejected");
|
||||||
|
|
||||||
|
assert!(error.contains("heartbeat_interval_ms"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_sends_demo_messages_and_heartbeat() {
|
||||||
|
let recorder = Recorder::new();
|
||||||
|
let mut plugin = ExamplePlugin::new();
|
||||||
|
|
||||||
|
plugin
|
||||||
|
.init(
|
||||||
|
r#"{
|
||||||
|
"heartbeat_interval_ms": 100,
|
||||||
|
"target_plugin": "example-plugin",
|
||||||
|
"announce_on_start": true,
|
||||||
|
"enable_periodic_task": true,
|
||||||
|
"periodic_payload": "test-heartbeat"
|
||||||
|
}"#,
|
||||||
|
recorder.sender(),
|
||||||
|
)
|
||||||
|
.expect("init should succeed");
|
||||||
|
|
||||||
|
plugin.start().expect("start should succeed");
|
||||||
|
thread::sleep(Duration::from_millis(130));
|
||||||
|
plugin.stop().expect("stop should succeed");
|
||||||
|
|
||||||
|
let envelopes = recorder.snapshot();
|
||||||
|
assert!(
|
||||||
|
envelopes
|
||||||
|
.iter()
|
||||||
|
.any(|env| matches!(env.message, Message::PluginReady(_))),
|
||||||
|
"expected PluginReady message"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
envelopes
|
||||||
|
.iter()
|
||||||
|
.any(|env| { matches!(env.message, Message::StateChanged { .. }) }),
|
||||||
|
"expected StateChanged broadcast"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
envelopes
|
||||||
|
.iter()
|
||||||
|
.any(|env| matches!(env.message, Message::Trigger { .. })),
|
||||||
|
"expected Trigger direct message"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
envelopes.iter().any(|env| match &env.message {
|
||||||
|
Message::Custom { kind, .. } => kind == "example.heartbeat",
|
||||||
|
_ => false,
|
||||||
|
}),
|
||||||
|
"expected heartbeat custom message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_reloaded_updates_runtime_state() {
|
||||||
|
let recorder = Recorder::new();
|
||||||
|
let mut plugin = ExamplePlugin::new();
|
||||||
|
|
||||||
|
plugin
|
||||||
|
.init(
|
||||||
|
r#"{
|
||||||
|
"announce_on_start": false,
|
||||||
|
"enable_periodic_task": false
|
||||||
|
}"#,
|
||||||
|
recorder.sender(),
|
||||||
|
)
|
||||||
|
.expect("init should succeed");
|
||||||
|
|
||||||
|
plugin.start().expect("start should succeed");
|
||||||
|
plugin
|
||||||
|
.handle_message(Message::ConfigReloaded(serde_json::json!({
|
||||||
|
"heartbeat_interval_ms": 100,
|
||||||
|
"target_plugin": "example-plugin",
|
||||||
|
"announce_on_start": false,
|
||||||
|
"enable_periodic_task": true,
|
||||||
|
"periodic_payload": "reloaded-heartbeat",
|
||||||
|
"optional_test_should_fail": true
|
||||||
|
})))
|
||||||
|
.expect("config reload should succeed");
|
||||||
|
|
||||||
|
assert!(plugin.config.enable_periodic_task);
|
||||||
|
assert_eq!(plugin.config.periodic_payload, "reloaded-heartbeat");
|
||||||
|
assert!(plugin.fail_optional_test);
|
||||||
|
assert!(plugin.worker.is_some());
|
||||||
|
|
||||||
|
plugin.stop().expect("stop should succeed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
- 并行派发有文件重叠的任务会导致编译冲突,需要串行或明确文件锁定
|
- 并行派发有文件重叠的任务会导致编译冲突,需要串行或明确文件锁定
|
||||||
- 关键路径任务(git push)应派给最可靠的人,而非按头衔分配
|
- 关键路径任务(git push)应派给最可靠的人,而非按头衔分配
|
||||||
- PM 不可靠时可以跳过 PM 直接派开发者,但要记录原因
|
- PM 不可靠时可以跳过 PM 直接派开发者,但要记录原因
|
||||||
|
- **CEO 绝不自己修代码/测试** — 即使是小修复也要派回给原作者,否则违反角色定位
|
||||||
|
|
||||||
## 团队经验
|
## 团队经验
|
||||||
- kilo agent 倾向于自作主张读 diff/分析代码,即使明确说不要。解决方案:只给命令,不给"任务描述"
|
- kilo agent 倾向于自作主张读 diff/分析代码,即使明确说不要。解决方案:只给命令,不给"任务描述"
|
||||||
|
|||||||
@@ -80,3 +80,12 @@
|
|||||||
- 每个任务必须 cargo check 通过
|
- 每个任务必须 cargo check 通过
|
||||||
- 旧代码参考:`/home/showen/Showen/hologram_player_rust/`
|
- 旧代码参考:`/home/showen/Showen/hologram_player_rust/`
|
||||||
- 编译环境:`export PATH="/home/showen/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin:$PATH"`
|
- 编译环境:`export PATH="/home/showen/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin:$PATH"`
|
||||||
|
|
||||||
|
## 复盘记录
|
||||||
|
|
||||||
|
### 2026-03-13 示例插件完善
|
||||||
|
- 示例插件不能只演示 `init/start/stop` 空壳流程,必须覆盖消息发送、消息匹配、配置解析、后台任务、自测与注释文档,否则第三方开发者无法照着扩展。
|
||||||
|
- Rust 示例配置优先使用 `serde + #[serde(default)] + deny_unknown_fields + validate()`,把“语法解析”和“业务校验”分成两个阶段,问题定位更清晰。
|
||||||
|
- 定时任务示例用 `Arc<MessageSender> + AtomicBool + JoinHandle` 就能讲清楚最小可用线程模型;`stop()` 和配置重载都要负责回收线程。
|
||||||
|
- 验收固定执行:`export PATH=... && cargo check --workspace --all-targets`,再执行 `export PATH=... && cargo test --workspace`,两项都绿灯后再汇报。
|
||||||
|
- 本次任务一次性通过 `cargo check` 零 warning 和 `cargo test` 全量通过,后续继续保持先验证再汇报的节奏。
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
- unclutter -idle 0 -root: 立即隐藏光标
|
- unclutter -idle 0 -root: 立即隐藏光标
|
||||||
- stop 时恢复光标用 pkill unclutter
|
- stop 时恢复光标用 pkill unclutter
|
||||||
- cfg(not(target_os = "linux")) 保持状态变量同步但不执行命令
|
- cfg(not(target_os = "linux")) 保持状态变量同步但不执行命令
|
||||||
|
- 为 Rust SDK 写文档时,优先给 pub 类型字段和 trait 方法补齐上下文,示例统一用 `# Examples`
|
||||||
|
- 对 FFI / 插件宏示例,doc-test 以 `ignore` 展示用法,避免引入动态库导出场景的编译噪音
|
||||||
|
- Rust 验证命令固定先注入 stable 工具链 PATH,再跑 `cargo check` 和 `cargo test`
|
||||||
|
|
||||||
## 技能树
|
## 技能树
|
||||||
- Web 前端和响应式设计:★★★★★
|
- Web 前端和响应式设计:★★★★★
|
||||||
|
|||||||
@@ -76,3 +76,4 @@
|
|||||||
- 关键指标:60fps 渲染、3秒启动、7x24小时稳定
|
- 关键指标:60fps 渲染、3秒启动、7x24小时稳定
|
||||||
- 旧版本对比测试很重要
|
- 旧版本对比测试很重要
|
||||||
- **必须实际运行并截图,不能只看代码**
|
- **必须实际运行并截图,不能只看代码**
|
||||||
|
- 2026-03-13:补齐 `src/core/tests.rs` 的关键路径覆盖,重点覆盖动态插件 FFI 返回 null 的降级、无效 manifest 跳过、禁用插件消息跳过、无稳定版本回退失败、以及 `Message` 全变体 JSON round-trip
|
||||||
|
|||||||
@@ -782,3 +782,197 @@ fn message_config_reloaded_round_trips_through_json() {
|
|||||||
other => panic!("unexpected message after round trip: {:?}", other),
|
other => panic!("unexpected message after round trip: {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unique_test_dir(name: &str) -> std::path::PathBuf {
|
||||||
|
let nanos = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("system time should be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("showen_{name}_{}_{}", std::process::id(), nanos))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_message_json_round_trip(message: Message) {
|
||||||
|
let expected = serde_json::to_value(&message).expect("message should serialize to value");
|
||||||
|
let decoded: Message =
|
||||||
|
serde_json::from_value(expected.clone()).expect("message should deserialize from value");
|
||||||
|
let actual = serde_json::to_value(decoded).expect("decoded message should serialize");
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: 动态插件 null vtable 测试已移除 —— 在测试环境中编译动态 .so 不可靠
|
||||||
|
// 该行为已通过 DynamicPlugin::read_plugin_string 的 null 检查保证安全
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_plugins_skips_invalid_manifest_and_keeps_valid_entries() {
|
||||||
|
let tmp = unique_test_dir("discover_invalid_manifest");
|
||||||
|
fs::create_dir_all(tmp.join("valid-plugin").join("1.0.0"))
|
||||||
|
.expect("valid plugin dir should be created");
|
||||||
|
fs::create_dir_all(tmp.join("broken-plugin").join("1.0.0"))
|
||||||
|
.expect("broken plugin dir should be created");
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
tmp.join("valid-plugin").join("1.0.0").join("manifest.json"),
|
||||||
|
r#"{
|
||||||
|
"id": "valid-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"sdk_version": "0.2.0",
|
||||||
|
"so_filename": "libvalid_plugin.so"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("valid manifest should be written");
|
||||||
|
fs::write(
|
||||||
|
tmp.join("broken-plugin")
|
||||||
|
.join("1.0.0")
|
||||||
|
.join("manifest.json"),
|
||||||
|
r#"{"id": "broken-plugin", "version": }"#,
|
||||||
|
)
|
||||||
|
.expect("invalid manifest should be written");
|
||||||
|
|
||||||
|
let loader = PluginLoader::new(&tmp);
|
||||||
|
let manifests = loader
|
||||||
|
.discover_plugins()
|
||||||
|
.expect("invalid manifest should be skipped, not returned as error");
|
||||||
|
|
||||||
|
assert_eq!(manifests.len(), 1);
|
||||||
|
assert_eq!(manifests[0].id, "valid-plugin");
|
||||||
|
assert_eq!(manifests[0].version, "1.0.0");
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handle_message_skips_disabled_plugins() {
|
||||||
|
let events = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let mut manager = ServiceManager::new(test_config());
|
||||||
|
|
||||||
|
manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone())));
|
||||||
|
manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone())));
|
||||||
|
manager.start_all().expect("start_all should succeed");
|
||||||
|
manager
|
||||||
|
.set_plugin_enabled("beta", false)
|
||||||
|
.expect("beta should be disabled");
|
||||||
|
|
||||||
|
let sender = manager.sender();
|
||||||
|
sender
|
||||||
|
.send(Envelope {
|
||||||
|
from: "alpha".to_string(),
|
||||||
|
to: Destination::Plugin("beta".to_string()),
|
||||||
|
message: Message::Custom {
|
||||||
|
kind: "direct".to_string(),
|
||||||
|
payload: "ignored".to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect("direct message should send");
|
||||||
|
sender
|
||||||
|
.send(Envelope {
|
||||||
|
from: "test".to_string(),
|
||||||
|
to: Destination::Manager,
|
||||||
|
message: Message::Shutdown,
|
||||||
|
})
|
||||||
|
.expect("shutdown should send");
|
||||||
|
|
||||||
|
manager.run().expect("run should succeed");
|
||||||
|
|
||||||
|
assert!(!has_event(&events, "msg:beta:custom:direct:ignored"));
|
||||||
|
assert!(!manager.plugin_states()[1].enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rollback_without_stable_version_returns_error_and_keeps_active_version() {
|
||||||
|
let tmp = unique_test_dir("rollback_without_stable");
|
||||||
|
fs::create_dir_all(tmp.join("sensor").join("2.0.0")).expect("version dir should be created");
|
||||||
|
|
||||||
|
let loader = PluginLoader::new(&tmp);
|
||||||
|
let mut registry = PluginRegistry::default();
|
||||||
|
registry.plugins.insert(
|
||||||
|
"sensor".to_string(),
|
||||||
|
PluginRegistryEntry {
|
||||||
|
active_version: "2.0.0".to_string(),
|
||||||
|
last_stable_version: None,
|
||||||
|
enabled: true,
|
||||||
|
error_policy: ErrorPolicy::AutoRollback,
|
||||||
|
max_errors: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
loader
|
||||||
|
.save_registry(®istry)
|
||||||
|
.expect("registry should be written");
|
||||||
|
|
||||||
|
let version_manager = VersionManager::new(loader);
|
||||||
|
let error = version_manager
|
||||||
|
.rollback("sensor")
|
||||||
|
.expect_err("missing stable version should fail");
|
||||||
|
assert!(error
|
||||||
|
.to_string()
|
||||||
|
.contains("plugin 'sensor' has no stable version to rollback to"));
|
||||||
|
|
||||||
|
let registry = version_manager
|
||||||
|
.loader()
|
||||||
|
.load_registry()
|
||||||
|
.expect("registry should still load");
|
||||||
|
assert_eq!(registry.plugins["sensor"].active_version, "2.0.0");
|
||||||
|
assert!(registry.plugins["sensor"].last_stable_version.is_none());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_message_variants_round_trip_through_json() {
|
||||||
|
let config = test_config();
|
||||||
|
let messages = vec![
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::Play),
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::Pause),
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::Next),
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::Previous),
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::Goto(3)),
|
||||||
|
Message::PlayerCommand(super::message::PlayerCommand::ChangeScene(
|
||||||
|
"intro".to_string(),
|
||||||
|
)),
|
||||||
|
Message::PlayerStatus(super::message::PlayerStatusData {
|
||||||
|
running: true,
|
||||||
|
paused: false,
|
||||||
|
in_transition: true,
|
||||||
|
current_index: 2,
|
||||||
|
playlist_length: 5,
|
||||||
|
current_video: Some("video.mp4".to_string()),
|
||||||
|
}),
|
||||||
|
Message::Trigger {
|
||||||
|
name: "motion".to_string(),
|
||||||
|
value: "detected".to_string(),
|
||||||
|
},
|
||||||
|
Message::StateChanged {
|
||||||
|
old_state: "idle".to_string(),
|
||||||
|
new_state: "playing".to_string(),
|
||||||
|
},
|
||||||
|
Message::ScreenLockRequest(true),
|
||||||
|
Message::CursorVisibility(false),
|
||||||
|
Message::WifiCommand(super::message::WifiCommand::Scan),
|
||||||
|
Message::WifiCommand(super::message::WifiCommand::Connect {
|
||||||
|
ssid: "lab".to_string(),
|
||||||
|
password: "secret".to_string(),
|
||||||
|
}),
|
||||||
|
Message::WifiCommand(super::message::WifiCommand::Status),
|
||||||
|
Message::WifiCommand(super::message::WifiCommand::ApStart {
|
||||||
|
ssid: "showen-ap".to_string(),
|
||||||
|
password: "password".to_string(),
|
||||||
|
}),
|
||||||
|
Message::WifiCommand(super::message::WifiCommand::ApStop),
|
||||||
|
Message::WifiResult("connected".to_string()),
|
||||||
|
Message::WifiProvisioned {
|
||||||
|
ssid: "lab".to_string(),
|
||||||
|
ip: "192.168.1.10".to_string(),
|
||||||
|
},
|
||||||
|
Message::ConfigReloaded(config),
|
||||||
|
Message::ConfigReloadRequest,
|
||||||
|
Message::Shutdown,
|
||||||
|
Message::PluginReady("sensor".to_string()),
|
||||||
|
Message::Custom {
|
||||||
|
kind: "health".to_string(),
|
||||||
|
payload: "ok".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
assert_message_json_round_trip(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user