From 6067c3f0a2afac05054ecff01110f3007e358911 Mon Sep 17 00:00:00 2001 From: showen Date: Fri, 13 Mar 2026 05:15:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D3=E4=B8=AAP0=E9=81=97?= =?UTF-8?q?=E7=95=99=20=E2=80=94=20AutoRollback=E5=9B=9E=E9=80=80/ConfigRe?= =?UTF-8?q?loaded=E5=BA=8F=E5=88=97=E5=8C=96/FfiString=E8=B7=A8allocator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin-sdk/src/lib.rs | 23 +++++ src/core/dynamic_plugin.rs | 27 ++++- src/core/message.rs | 6 +- src/core/plugin_abi.rs | 21 ++-- src/core/service_manager.rs | 168 +++++++++++++++++++++++++++---- src/core/tests.rs | 179 ++++++++++++++++++++++++++++++++- src/main.rs | 14 ++- src/plugins/http/mod.rs | 4 +- src/plugins/video/mod.rs | 2 +- src/plugins/video/processor.rs | 6 +- 10 files changed, 393 insertions(+), 57 deletions(-) diff --git a/plugin-sdk/src/lib.rs b/plugin-sdk/src/lib.rs index 1ec37bd..45530ee 100644 --- a/plugin-sdk/src/lib.rs +++ b/plugin-sdk/src/lib.rs @@ -64,6 +64,7 @@ pub enum Message { ssid: String, ip: String, }, + ConfigReloaded(serde_json::Value), ConfigReloadRequest, Shutdown, PluginReady(String), @@ -104,6 +105,20 @@ impl FfiString { len: 0, } } + + /// 复制为 Rust String(不释放底层内存) + /// + /// # Safety + /// ptr 必须指向有效的 null-terminated C 字符串 + pub unsafe fn to_string(&self) -> Option { + if self.ptr.is_null() { + return None; + } + unsafe { std::ffi::CStr::from_ptr(self.ptr) } + .to_str() + .ok() + .map(str::to_owned) + } } #[repr(C)] @@ -144,6 +159,7 @@ pub struct PluginVTable { pub handle_message: unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult, pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult, + pub free_string: unsafe extern "C" fn(s: FfiString), 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, @@ -371,6 +387,12 @@ macro_rules! export_plugin { } } + unsafe extern "C" fn __showen_free_string(s: $crate::FfiString) { + if !s.ptr.is_null() { + drop(unsafe { std::ffi::CString::from_raw(s.ptr) }); + } + } + unsafe extern "C" fn __showen_get_capabilities( handle: $crate::PluginHandle, ) -> $crate::FfiString { @@ -409,6 +431,7 @@ macro_rules! export_plugin { start: __showen_start, handle_message: __showen_handle_message, stop: __showen_stop, + free_string: __showen_free_string, destroy: __showen_destroy, get_capabilities: __showen_get_capabilities, self_test: __showen_self_test, diff --git a/src/core/dynamic_plugin.rs b/src/core/dynamic_plugin.rs index 09ca3f8..3a3dcb0 100644 --- a/src/core/dynamic_plugin.rs +++ b/src/core/dynamic_plugin.rs @@ -64,7 +64,7 @@ impl DynamicPlugin { // 获取插件信息 let info_ffi: FfiString = unsafe { (vtable.get_info)(handle) }; - let info_json = unsafe { info_ffi.into_string() } + let info_json = unsafe { Self::read_plugin_string(vtable, info_ffi) } .ok_or_else(|| anyhow!("plugin get_info() returned null for {so_path}"))?; let info: PluginInfo = serde_json::from_str(&info_json) .with_context(|| format!("invalid plugin info JSON from {so_path}"))?; @@ -95,8 +95,25 @@ impl DynamicPlugin { /// 将 FfiResult 转为 anyhow::Result unsafe fn check_result(&self, result: FfiResult, operation: &str) -> Result<()> { - unsafe { result.into_result() } - .map_err(|e| anyhow!("plugin '{}' {} failed: {}", self.id, operation, e)) + if result.code == 0 { + return Ok(()); + } + + let error = unsafe { Self::read_plugin_string(self.vtable, result.error) } + .unwrap_or_else(|| "unknown plugin error".to_string()); + Err(anyhow!( + "plugin '{}' {} failed: {}", + self.id, + operation, + error + )) + } + + /// 读取插件返回的字符串,并通过插件提供的 free_string 释放 + unsafe fn read_plugin_string(vtable: &PluginVTable, ffi_str: FfiString) -> Option { + let string = unsafe { ffi_str.to_string() }; + unsafe { (vtable.free_string)(ffi_str) }; + string } } @@ -115,7 +132,7 @@ impl Plugin for DynamicPlugin { fn capabilities(&self) -> Vec { let ffi_str: FfiString = unsafe { (self.vtable.get_capabilities)(self.handle) }; - let json = match unsafe { ffi_str.into_string() } { + let json = match unsafe { Self::read_plugin_string(self.vtable, ffi_str) } { Some(s) => s, None => return vec![], }; @@ -124,7 +141,7 @@ impl Plugin for DynamicPlugin { fn self_test(&mut self) -> Vec { let ffi_str: FfiString = unsafe { (self.vtable.self_test)(self.handle) }; - let json = match unsafe { ffi_str.into_string() } { + let json = match unsafe { Self::read_plugin_string(self.vtable, ffi_str) } { Some(s) => s, None => return vec![], }; diff --git a/src/core/message.rs b/src/core/message.rs index 0d8c11f..292513b 100644 --- a/src/core/message.rs +++ b/src/core/message.rs @@ -1,6 +1,5 @@ use crate::core::config::AppConfig; use serde::{Deserialize, Serialize}; -use std::sync::Arc; /// 消息信封:包含来源、目的地、消息体 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -49,9 +48,8 @@ pub enum Message { }, // ── 配置 ── - /// Arc 无法跨 FFI 序列化,动态插件通过 init 时传入的 JSON 获取配置 - #[serde(skip)] - ConfigReloaded(Arc), + /// 配置重载广播需要经过 JSON/FFI 路径,因此这里保存可序列化的 AppConfig。 + ConfigReloaded(AppConfig), ConfigReloadRequest, // ── 系统 ── diff --git a/src/core/plugin_abi.rs b/src/core/plugin_abi.rs index 0faa3c5..0c5a073 100644 --- a/src/core/plugin_abi.rs +++ b/src/core/plugin_abi.rs @@ -10,7 +10,7 @@ use std::ptr; pub type PluginHandle = *mut c_void; /// FFI 安全的字符串:指向 C 字符串 + 长度 -/// 调用方负责释放(通过对应的 free 函数) +/// 调用方读取内容后,必须通过分配方提供的 free 函数释放 #[repr(C)] pub struct FfiString { pub ptr: *mut c_char, @@ -40,16 +40,18 @@ impl FfiString { } } - /// 转换回 Rust String(消耗 FfiString) + /// 复制为 Rust String(不释放底层内存) /// /// # Safety /// ptr 必须是由 CString::into_raw 产生的有效指针 - pub unsafe fn into_string(self) -> Option { + pub unsafe fn to_string(&self) -> Option { if self.ptr.is_null() { return None; } - let cstr = unsafe { CString::from_raw(self.ptr) }; - cstr.into_string().ok() + unsafe { CStr::from_ptr(self.ptr) } + .to_str() + .ok() + .map(str::to_owned) } } @@ -80,15 +82,15 @@ impl FfiResult { } } - /// 转换为 Rust Result + /// 转换为 Rust Result(不释放 error 底层内存) /// /// # Safety /// 如果 error 非 null,必须是由 CString::into_raw 产生的有效指针 - pub unsafe fn into_result(self) -> Result<(), String> { + pub unsafe fn to_result(&self) -> Result<(), String> { if self.code == 0 { Ok(()) } else { - let msg = unsafe { self.error.into_string() } + let msg = unsafe { self.error.to_string() } .unwrap_or_else(|| "unknown plugin error".to_string()); Err(msg) } @@ -129,6 +131,9 @@ pub struct PluginVTable { /// 停止插件 pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult, + /// 释放插件分配的 FfiString + pub free_string: unsafe extern "C" fn(s: FfiString), + /// 销毁插件实例,释放资源 pub destroy: unsafe extern "C" fn(handle: PluginHandle), diff --git a/src/core/service_manager.rs b/src/core/service_manager.rs index 356aabc..7d6d845 100644 --- a/src/core/service_manager.rs +++ b/src/core/service_manager.rs @@ -2,6 +2,7 @@ use crate::core::config::AppConfig; use crate::core::message::{Destination, Envelope, Message}; use crate::core::plugin::{CapabilityTestResult, Plugin, PluginContext}; use crate::core::plugin_loader::ErrorPolicy; +use crate::core::version_manager::VersionManager; use anyhow::{anyhow, Result}; use std::collections::{HashMap, HashSet}; use std::sync::{mpsc, Arc}; @@ -27,6 +28,8 @@ struct PluginState { required_capabilities: Vec, /// 是否自动测试 auto_test: bool, + /// 是否需要在后续生命周期中执行回退 + needs_rollback: bool, } impl PluginState { @@ -42,6 +45,7 @@ impl PluginState { capabilities: vec![], required_capabilities: vec![], auto_test: false, // 静态插件默认不自测 + needs_rollback: false, } } @@ -57,6 +61,7 @@ impl PluginState { capabilities: vec![], required_capabilities: vec![], auto_test: true, + needs_rollback: false, } } @@ -83,6 +88,7 @@ pub struct ServiceManager { tx: mpsc::Sender, rx: mpsc::Receiver, running: bool, + version_manager: Option, } impl ServiceManager { @@ -94,9 +100,14 @@ impl ServiceManager { tx, rx, running: false, + version_manager: None, } } + pub fn set_version_manager(&mut self, version_manager: VersionManager) { + self.version_manager = Some(version_manager); + } + /// 注册静态插件(编译时链接的插件) pub fn register(&mut self, plugin: Box) { println!("[ServiceManager] 注册插件: {}", plugin.id()); @@ -351,10 +362,49 @@ impl ServiceManager { enabled: s.enabled, test_results: s.test_results.clone(), capabilities: s.capabilities.clone(), + needs_rollback: s.needs_rollback, }) .collect() } + fn replace_dynamic_plugin_at_index( + &mut self, + idx: usize, + plugin_id: &str, + new_plugin: Box, + error_policy: ErrorPolicy, + max_errors: u32, + required_capabilities: Vec, + capabilities: Vec, + auto_test: bool, + ) -> Result<()> { + if !self.plugins[idx].is_dynamic { + return Err(anyhow!( + "plugin '{plugin_id}' is not dynamic and cannot be replaced" + )); + } + + let mut new_state = PluginState::new_dynamic(new_plugin, error_policy, max_errors); + new_state.required_capabilities = required_capabilities; + new_state.capabilities = capabilities; + new_state.auto_test = auto_test; + + let ctx = PluginContext { + tx: self.tx.clone(), + config: Arc::clone(&self.config), + }; + new_state.plugin.init(ctx)?; + new_state.plugin.start()?; + + if self.plugins[idx].enabled { + let _ = self.plugins[idx].plugin.stop(); + } + + self.plugins[idx] = new_state; + println!("[ServiceManager] 插件 '{plugin_id}' 热替换成功"); + Ok(()) + } + /// 热替换动态插件(stop 旧的 → 替换 → init → start 新的) pub fn replace_dynamic_plugin( &mut self, @@ -374,22 +424,21 @@ impl ServiceManager { "plugin '{plugin_id}' is not dynamic and cannot be replaced" )); } - let mut new_state = PluginState::new_dynamic(new_plugin, error_policy, max_errors); - let ctx = PluginContext { - tx: self.tx.clone(), - config: Arc::clone(&self.config), - }; - new_state.plugin.init(ctx)?; - new_state.plugin.start()?; + let required_capabilities = self.plugins[idx].required_capabilities.clone(); + let capabilities = self.plugins[idx].capabilities.clone(); + let auto_test = self.plugins[idx].auto_test; - if self.plugins[idx].enabled { - let _ = self.plugins[idx].plugin.stop(); - } - - self.plugins[idx] = new_state; - println!("[ServiceManager] 插件 '{plugin_id}' 热替换成功"); - Ok(()) + self.replace_dynamic_plugin_at_index( + idx, + plugin_id, + new_plugin, + error_policy, + max_errors, + required_capabilities, + capabilities, + auto_test, + ) } /// 处理发给管理层自身的消息 @@ -424,7 +473,7 @@ impl ServiceManager { let new_config = Arc::new(new_config); self.config = Arc::clone(&new_config); println!("[ServiceManager] 配置重载成功,广播 ConfigReloaded"); - self.broadcast_message(Message::ConfigReloaded(new_config)); + self.broadcast_message(Message::ConfigReloaded((*new_config).clone())); } Err(e) => { eprintln!("[ServiceManager] 配置重载失败: {}", e); @@ -588,28 +637,102 @@ impl ServiceManager { /// 插件错误达到阈值时的处理 fn handle_error_threshold(&mut self, plugin_id: &str) { - let state = match self.plugins.iter_mut().find(|s| s.id() == plugin_id) { - Some(s) => s, + let idx = match self.plugins.iter().position(|s| s.id() == plugin_id) { + Some(idx) => idx, None => return, }; - match state.error_policy { + match self.plugins[idx].error_policy.clone() { ErrorPolicy::DisableAndLog => { eprintln!( "[ServiceManager] 插件 '{}' 错误次数达到阈值,已禁用", plugin_id ); + let state = &mut self.plugins[idx]; let _ = state.plugin.stop(); state.enabled = false; + state.needs_rollback = false; } ErrorPolicy::AutoRollback => { + { + let state = &mut self.plugins[idx]; + let _ = state.plugin.stop(); + state.enabled = false; + state.needs_rollback = false; + } + eprintln!( - "[ServiceManager] 插件 '{}' 错误次数达到阈值,需要回退 (由外部 VersionManager 处理)", + "[ServiceManager] 插件 '{}' 错误次数达到阈值,尝试自动回退到稳定版本", plugin_id ); - // 先禁用,等待外部 (main.rs / HTTP API) 调用 VersionManager 执行回退 - let _ = state.plugin.stop(); - state.enabled = false; + + let rollback_result = { + let Some(version_manager) = self.version_manager.as_ref() else { + eprintln!( + "[ServiceManager] 插件 '{}' 未配置 VersionManager,标记为待回退", + plugin_id + ); + self.plugins[idx].needs_rollback = true; + return; + }; + + match version_manager.rollback(plugin_id) { + Ok(version) => match version_manager + .loader() + .load_plugin(plugin_id, Some(&version)) + { + Ok((plugin, manifest)) => { + Ok((version, Box::new(plugin) as Box, manifest)) + } + Err(e) => Err((Some(version), e)), + }, + Err(e) => Err((None, e)), + } + }; + + match rollback_result { + Ok((version, plugin, manifest)) => { + let max_errors = self.plugins[idx].max_errors; + match self.replace_dynamic_plugin_at_index( + idx, + plugin_id, + plugin, + manifest.error_policy, + max_errors, + manifest.required_capabilities, + manifest.capabilities, + manifest.auto_test, + ) { + Ok(()) => { + println!( + "[ServiceManager] 插件 '{}' 已回退并重新加载稳定版本 {}", + plugin_id, version + ); + } + Err(e) => { + eprintln!( + "[ServiceManager] 插件 '{}' 已切换到稳定版本 {},但热替换失败: {}", + plugin_id, version, e + ); + self.plugins[idx].needs_rollback = true; + } + } + } + Err((Some(version), e)) => { + eprintln!( + "[ServiceManager] 插件 '{}' 已切换到稳定版本 {},但加载回退版本失败: {}", + plugin_id, version, e + ); + self.plugins[idx].needs_rollback = true; + } + Err((None, e)) => { + eprintln!( + "[ServiceManager] 插件 '{}' 自动回退失败,标记为待回退: {}", + plugin_id, e + ); + self.plugins[idx].needs_rollback = true; + } + } } } } @@ -632,4 +755,5 @@ pub struct PluginStateInfo { pub enabled: bool, pub test_results: Vec, pub capabilities: Vec, + pub needs_rollback: bool, } diff --git a/src/core/tests.rs b/src/core/tests.rs index cd751e6..78e1621 100644 --- a/src/core/tests.rs +++ b/src/core/tests.rs @@ -1,9 +1,12 @@ use super::config::{parse_str, AppConfig}; use super::message::{Destination, Envelope, Message}; use super::plugin::{CapabilityTestResult, Platform, Plugin, PluginContext, PluginInfo}; +use super::plugin_loader::{ErrorPolicy, PluginLoader, PluginRegistry, PluginRegistryEntry}; use super::service_manager::ServiceManager; -use super::plugin_loader::ErrorPolicy; +use super::version_manager::VersionManager; use anyhow::Result; +use std::fs; +use std::path::Path; use std::sync::{Arc, Mutex}; fn test_config() -> AppConfig { @@ -385,7 +388,11 @@ fn all_plugin_ids_must_be_unique() { let mut ids = HashSet::new(); for plugin in plugins { let id = plugin.id().to_string(); - assert!(ids.insert(id.clone()), "duplicate plugin id detected: '{}'", id); + assert!( + ids.insert(id.clone()), + "duplicate plugin id detected: '{}'", + id + ); } } @@ -496,6 +503,81 @@ impl Plugin for TestPluginWithSelfTest { } } +struct FailingPlugin { + id: String, + events: Arc>>, +} + +impl FailingPlugin { + fn new(id: &str, events: Arc>>) -> Self { + Self { + id: id.to_string(), + events, + } + } + + fn record(&self, entry: impl Into) { + lock_events(&self.events).push(entry.into()); + } +} + +impl Plugin for FailingPlugin { + fn id(&self) -> &str { + &self.id + } + + fn info(&self) -> PluginInfo { + PluginInfo { + name: self.id.clone(), + version: "test".to_string(), + description: "failing test plugin".to_string(), + platform: Platform::Any, + } + } + + fn init(&mut self, _ctx: PluginContext) -> Result<()> { + self.record(format!("init:{}", self.id)); + Ok(()) + } + + fn start(&mut self) -> Result<()> { + self.record(format!("start:{}", self.id)); + Ok(()) + } + + fn handle_message(&mut self, _msg: Message) -> Result<()> { + self.record(format!("error:{}", self.id)); + Err(anyhow::anyhow!("simulated failure")) + } + + fn stop(&mut self) -> Result<()> { + self.record(format!("stop:{}", self.id)); + Ok(()) + } +} + +fn setup_rollback_store(base: &Path, plugin_id: &str) -> VersionManager { + let _ = fs::remove_dir_all(base); + fs::create_dir_all(base.join(plugin_id).join("1.0.0")).unwrap(); + fs::create_dir_all(base.join(plugin_id).join("2.0.0")).unwrap(); + + let loader = PluginLoader::new(base); + let mut registry = PluginRegistry::default(); + registry.plugins.insert( + plugin_id.to_string(), + PluginRegistryEntry { + active_version: "2.0.0".to_string(), + last_stable_version: Some("1.0.0".to_string()), + enabled: true, + error_policy: ErrorPolicy::AutoRollback, + max_errors: 1, + }, + ); + loader.save_registry(®istry).unwrap(); + + VersionManager::new(loader) +} + #[test] fn self_test_all_pass_allows_normal_start() { let events = Arc::new(Mutex::new(Vec::new())); @@ -521,7 +603,9 @@ fn self_test_all_pass_allows_normal_start() { true, ); - manager.start_all().expect("start_all should succeed when all tests pass"); + manager + .start_all() + .expect("start_all should succeed when all tests pass"); let log = lock_events(&events); assert!(log.contains(&"init:sensor".to_string())); @@ -565,7 +649,10 @@ fn self_test_required_capability_fails_disables_dynamic_plugin() { // Plugin should be disabled let states = manager.plugin_states(); - assert!(!states[0].enabled, "plugin should be disabled after required capability failure"); + assert!( + !states[0].enabled, + "plugin should be disabled after required capability failure" + ); } #[test] @@ -605,7 +692,10 @@ fn self_test_optional_capability_fails_still_starts() { let log = lock_events(&events); assert!(log.contains(&"self_test:sensor".to_string())); - assert!(log.contains(&"start:sensor".to_string()), "plugin should start despite optional failure"); + assert!( + log.contains(&"start:sensor".to_string()), + "plugin should start despite optional failure" + ); // Test results should be recorded let states = manager.plugin_states(); @@ -613,3 +703,82 @@ fn self_test_optional_capability_fails_still_starts() { assert!(states[0].test_results[0].passed); assert!(!states[0].test_results[1].passed); } + +#[test] +fn auto_rollback_updates_registry_and_marks_pending_when_reload_fails() { + let tmp = std::env::temp_dir().join("showen_test_service_manager_autorollback"); + let events = Arc::new(Mutex::new(Vec::new())); + let mut manager = ServiceManager::new(test_config()); + manager.set_version_manager(setup_rollback_store(&tmp, "sensor")); + + manager.register_dynamic( + Box::new(FailingPlugin::new("sensor", events.clone())), + ErrorPolicy::AutoRollback, + 1, + ); + + manager.start_all().expect("start_all should succeed"); + let sender = manager.sender(); + sender + .send(Envelope { + from: "test".to_string(), + to: Destination::Plugin("sensor".to_string()), + message: Message::Custom { + kind: "tick".to_string(), + payload: "1".to_string(), + }, + }) + .expect("failing 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"); + + let registry = PluginLoader::new(&tmp).load_registry().unwrap(); + assert_eq!(registry.plugins["sensor"].active_version, "1.0.0"); + + let states = manager.plugin_states(); + assert!( + !states[0].enabled, + "plugin should be disabled after rollback failure" + ); + assert!( + states[0].needs_rollback, + "plugin should be marked for restart-time reload" + ); + assert!(has_event(&events, "stop:sensor")); + + let _ = fs::remove_dir_all(&tmp); +} + +#[test] +fn message_config_reload_request_round_trips_through_json() { + let json = serde_json::to_string(&Message::ConfigReloadRequest) + .expect("ConfigReloadRequest should serialize"); + let message: Message = + serde_json::from_str(&json).expect("ConfigReloadRequest should deserialize"); + + assert!(matches!(message, Message::ConfigReloadRequest)); +} + +#[test] +fn message_config_reloaded_round_trips_through_json() { + let config = test_config(); + let json = serde_json::to_string(&Message::ConfigReloaded(config.clone())) + .expect("ConfigReloaded should serialize"); + let message: Message = serde_json::from_str(&json).expect("ConfigReloaded should deserialize"); + + match message { + Message::ConfigReloaded(decoded) => { + assert_eq!(decoded.display.window_title, config.display.window_title); + assert_eq!(decoded.playlist.len(), config.playlist.len()); + assert_eq!(decoded.remote_control.port, config.remote_control.port); + } + other => panic!("unexpected message after round trip: {:?}", other), + } +} diff --git a/src/main.rs b/src/main.rs index 64b1835..a9a5b6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use anyhow::Result; use showen_v2::core::config::AppConfig; use showen_v2::core::plugin_loader::PluginLoader; use showen_v2::core::service_manager::ServiceManager; +#[cfg(not(test))] +use showen_v2::core::version_manager::VersionManager; use showen_v2::plugins::{ ble::BlePlugin, http::HttpPlugin, screen::ScreenPlugin, video::VideoPlugin, wifi::WifiPlugin, }; @@ -69,6 +71,8 @@ fn main() -> Result<()> { let plugin_store = std::path::Path::new("plugin_store"); if plugin_store.exists() { println!("扫描动态插件..."); + #[cfg(not(test))] + manager.set_version_manager(VersionManager::new(PluginLoader::new(plugin_store))); let loader = PluginLoader::new(plugin_store); match loader.load_registry() { Ok(registry) => { @@ -88,16 +92,10 @@ fn main() -> Result<()> { manifest.capabilities, manifest.auto_test, ); - println!( - " ✓ {} v{} (动态)", - plugin_id, entry.active_version - ); + println!(" ✓ {} v{} (动态)", plugin_id, entry.active_version); } Err(e) => { - eprintln!( - " ✗ {} v{} 加载失败: {e}", - plugin_id, entry.active_version - ); + eprintln!(" ✗ {} v{} 加载失败: {e}", plugin_id, entry.active_version); } } } diff --git a/src/plugins/http/mod.rs b/src/plugins/http/mod.rs index 1cbfcac..b700f3b 100644 --- a/src/plugins/http/mod.rs +++ b/src/plugins/http/mod.rs @@ -316,8 +316,8 @@ impl Plugin for HttpPlugin { } } Message::ConfigReloaded(config) => { - state.replace_config(Arc::clone(&config)); - if let Some(payload) = encode_ws_event("config_update", config.as_ref()) { + state.replace_config(Arc::new(config.clone())); + if let Some(payload) = encode_ws_event("config_update", &config) { state.publish_ws(payload); } } diff --git a/src/plugins/video/mod.rs b/src/plugins/video/mod.rs index 6213761..b64e7b3 100644 --- a/src/plugins/video/mod.rs +++ b/src/plugins/video/mod.rs @@ -194,7 +194,7 @@ impl Plugin for VideoPlugin { self.publish_status(); } Message::ConfigReloaded(config) => { - let processor = Arc::new(Mutex::new(VideoProcessor::new((*config).clone())?)); + let processor = Arc::new(Mutex::new(VideoProcessor::new(config)?)); if let Some(old) = self.processor.replace(Arc::clone(&processor)) { if let Ok(mut old) = old.lock() { let _ = old.stop(); diff --git a/src/plugins/video/processor.rs b/src/plugins/video/processor.rs index d9aa562..345ceaf 100644 --- a/src/plugins/video/processor.rs +++ b/src/plugins/video/processor.rs @@ -929,8 +929,10 @@ impl VideoProcessor { if let Some(resolution) = parts.first() { let dims: Vec<&str> = resolution.split('x').collect(); if dims.len() == 2 { - let w_str = dims[0].trim_end_matches(|c: char| !c.is_ascii_digit()); - let h_str = dims[1].trim_end_matches(|c: char| !c.is_ascii_digit()); + let w_str = + dims[0].trim_end_matches(|c: char| !c.is_ascii_digit()); + let h_str = + dims[1].trim_end_matches(|c: char| !c.is_ascii_digit()); if let (Ok(w), Ok(h)) = (w_str.parse::(), h_str.parse::()) {