//! 示例动态插件 — 展示如何使用 `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::{ export_plugin, CapabilityTestResult, Destination, DeviceCommand, DeviceResponse, 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"; const CAP_REQUEST_RESPONSE: &str = "request_response"; /// 示例插件配置。 /// /// 这里使用 `#[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, /// DevicePlugin 的插件 ID,用于演示请求/响应模式。 device_plugin: String, /// 是否在 `start()` 时请求一次显示信息。 request_display_info_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, device_plugin: "device".to_string(), request_display_info_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 { let trimmed = config_json.trim(); let mut config = if trimmed.is_empty() || trimmed == "null" { Self::default() } else { serde_json::from_str::(trimmed) .map_err(|error| format!("failed to parse example plugin config: {error}"))? }; config.normalize(); config.validate()?; Ok(config) } /// 处理 `ConfigReloaded` 的场景,调用方式与初始化时保持一致。 fn from_value(value: serde_json::Value) -> Result { let mut config = serde_json::from_value::(value) .map_err(|error| format!("failed to decode reloaded config: {error}"))?; config.normalize(); config.validate()?; Ok(config) } fn normalize(&mut self) { self.target_plugin = self.target_plugin.trim().to_string(); self.device_plugin = self.device_plugin.trim().to_string(); self.periodic_payload = self.periodic_payload.trim().to_string(); } 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.request_display_info_on_start && self.device_plugin.trim().is_empty() { return Err( "config field `device_plugin` must not be empty when `request_display_info_on_start` is enabled" .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 { /// `MessageSender` 会在 `init()` 时由宿主注入。 /// /// 用 `Arc` 包起来是为了后台线程可以安全共享同一个 sender。 sender: Option>, /// 当前生效配置。 config: ExamplePluginConfig, /// 用于结束后台线程的停止信号。 worker_stop: Arc, /// 后台线程句柄;在 `stop()` 和配置重载时负责回收。 worker: Option>, /// 用于演示 `self_test()` 如何暴露可选失败项。 fail_optional_test: bool, } impl ExamplePlugin { pub fn new() -> Self { Self { sender: None, config: ExamplePluginConfig::default(), worker_stop: Arc::new(AtomicBool::new(false)), worker: None, fail_optional_test: false, } } fn sender(&self) -> Result<&Arc, 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(()) } /// 演示典型的请求/响应模式:向 DevicePlugin 请求一次显示信息,随后在 /// `handle_message()` 里处理 `Message::DeviceResponse`。 fn request_display_info(&self) -> Result<(), String> { let sender = self.sender()?; sender.send_to( PLUGIN_ID, &self.config.device_plugin, Message::DeviceCommand(DeviceCommand::GetDisplayInfo), ); sender.send_to_manager( PLUGIN_ID, Message::Custom { kind: "example.request_sent".to_string(), payload: format!( "requested DeviceCommand::GetDisplayInfo from {}", self.config.device_plugin ), }, ); 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()?; } "example.request_display_info" => { self.request_display_info()?; } _ => { self.sender()?.send_to_manager( PLUGIN_ID, Message::Custom { kind: "example.trigger_ack".to_string(), payload: format!("unknown trigger ignored: {name}={value}"), }, ); } } Ok(()) } /// 演示如何为自定义消息协议做显式解析和友好错误提示。 /// /// 这里把 `payload` 约定为 JSON,模拟一个“订阅更新”场景;真实业务可以换成 /// 自己的协议名称和字段。 fn handle_custom_message(&self, kind: String, payload: String) -> Result<(), String> { eprintln!("[ExamplePlugin] custom message: kind={kind}, payload={payload}"); if kind != "example.subscription.update" { return Ok(()); } let parsed: serde_json::Value = serde_json::from_str(&payload).map_err(|error| { format!( "custom message `example.subscription.update` expects JSON payload, got parse error: {error}" ) })?; let topic = parsed .get("topic") .and_then(serde_json::Value::as_str) .map(str::trim) .filter(|topic| !topic.is_empty()) .ok_or_else(|| { "custom message `example.subscription.update` requires non-empty string field `topic`" .to_string() })?; self.sender()?.send_to_manager( PLUGIN_ID, Message::Custom { kind: "example.subscription.ack".to_string(), payload: format!("subscription update processed for topic `{topic}`"), }, ); 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 { fn info(&self) -> PluginInfo { PluginInfo { name: PLUGIN_ID.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), description: "Feature-complete example plugin for third-party developers".to_string(), platform: "Any".to_string(), } } fn capabilities(&self) -> Vec { 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(), CAP_REQUEST_RESPONSE.to_string(), ] } fn self_test(&mut self) -> Vec { 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![ CapabilityTestResult { capability: CAP_MESSAGE_SENDER.to_string(), passed: sender_ready, message: if sender_ready { "MessageSender injected during init()".to_string() } else { "MessageSender not available yet".to_string() }, }, CapabilityTestResult { 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, message: if self.fail_optional_test { "optional self-test failure requested by config".to_string() } else { "self-test harness operational".to_string() }, }, CapabilityTestResult { capability: CAP_REQUEST_RESPONSE.to_string(), passed: !self.config.request_display_info_on_start || !self.config.device_plugin.is_empty(), message: if self.config.request_display_info_on_start { format!( "request/response demo targets DevicePlugin `{}`", self.config.device_plugin ) } else { "request/response demo disabled by config".to_string() }, }, ] } fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> { eprintln!( "[ExamplePlugin] init called, received config bytes: {}", config_json.len() ); let config = ExamplePluginConfig::from_json(config_json)?; self.config = config; self.fail_optional_test = self.config.optional_test_should_fail; self.sender = Some(Arc::new(sender)); self.notify_ready()?; Ok(()) } fn start(&mut self) -> Result<(), String> { eprintln!("[ExamplePlugin] start called"); if self.config.announce_on_start { self.emit_demo_messages()?; } if self.config.request_display_info_on_start { self.request_display_info()?; } self.start_worker()?; Ok(()) } fn handle_message(&mut self, message: Message) -> Result<(), String> { 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 => { eprintln!("[ExamplePlugin] shutdown requested"); self.stop()?; } Message::PluginReady(plugin_name) => { eprintln!("[ExamplePlugin] observed peer readiness: {plugin_name}"); } Message::DeviceCommand(command) => { eprintln!("[ExamplePlugin] device command received (not handled): {command:?}"); } Message::DeviceResponse(response) => { eprintln!("[ExamplePlugin] device response received: {response:?}"); let summary = match response { DeviceResponse::DisplayInfo { width, height, format, } => format!( "display info received: width={width}, height={height}, format={format:?}" ), DeviceResponse::BatteryLevel(level) => { format!("battery level response received: {level}%") } DeviceResponse::SensorData { sensor, value } => { format!("sensor response received: {sensor:?}={value}") } DeviceResponse::Ok => "device command completed successfully".to_string(), DeviceResponse::Error(error) => format!("device command failed: {error}"), DeviceResponse::Custom(value) => format!("custom device response: {value}"), }; self.sender()?.send_to_manager( PLUGIN_ID, Message::Custom { kind: "example.device_response".to_string(), payload: summary, }, ); } Message::DeviceEvent(_event) => { eprintln!("[ExamplePlugin] device event received (not handled)"); } Message::Custom { kind, payload } => { self.handle_custom_message(kind, payload)?; } } Ok(()) } fn stop(&mut self) -> Result<(), String> { eprintln!("[ExamplePlugin] stop called"); self.stop_worker()?; self.sender = None; Ok(()) } } // 导出动态插件 FFI 接口。 // // `export_plugin!` 会生成主程序约定的完整 ABI: // - `create` / `destroy`: 创建和销毁插件实例 // - `get_info`: 返回 `PluginInfo` 的 JSON // - `init`: 接收配置 JSON 和宿主注入的消息发送回调 // - `start` / `stop`: 驱动插件生命周期 // - `handle_message`: 接收宿主转发的 JSON 消息并反序列化为 `Message` // - `get_capabilities` / `self_test`: 向宿主暴露能力声明和自检结果 // - `free_string`: 释放由当前插件分配给宿主的 `FfiString` 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>>, } impl Recorder { fn new() -> Self { Self { envelopes: Box::new(Mutex::new(Vec::new())), } } fn sender(&self) -> MessageSender { let ctx = (&*self.envelopes as *const Mutex>) as *mut c_void; MessageSender::new(ctx, record_envelope) } fn snapshot(&self) -> Vec { 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>) }; let raw = unsafe { CStr::from_ptr(envelope_json) } .to_str() .expect("callback JSON should be valid UTF-8"); let envelope = serde_json::from_str::(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 config_rejects_missing_device_plugin_when_request_response_is_enabled() { let error = ExamplePluginConfig::from_json( r#"{"device_plugin":" ","request_display_info_on_start":true}"#, ) .expect_err("config should be rejected"); assert!(error.contains("device_plugin")); } #[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", "device_plugin": "device", "announce_on_start": false, "request_display_info_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"); } #[test] fn start_requests_display_info_when_enabled() { let recorder = Recorder::new(); let mut plugin = ExamplePlugin::new(); plugin .init( r#"{ "announce_on_start": false, "request_display_info_on_start": true, "device_plugin": "device" }"#, recorder.sender(), ) .expect("init should succeed"); plugin.start().expect("start should succeed"); plugin.stop().expect("stop should succeed"); let envelopes = recorder.snapshot(); assert!( envelopes.iter().any(|env| { matches!( (&env.to, &env.message), ( Destination::Plugin(plugin_id), Message::DeviceCommand(DeviceCommand::GetDisplayInfo) ) if plugin_id == "device" ) }), "expected GetDisplayInfo request to device plugin" ); } #[test] fn custom_subscription_update_requires_valid_payload() { let recorder = Recorder::new(); let mut plugin = ExamplePlugin::new(); plugin .init("{}", recorder.sender()) .expect("init should succeed"); let error = plugin .handle_message(Message::Custom { kind: "example.subscription.update".to_string(), payload: "not-json".to_string(), }) .expect_err("invalid payload should be rejected"); assert!(error.contains("expects JSON payload")); } }