diff --git a/UPDATELOG.md b/UPDATELOG.md index c43f559ce..973785cc1 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -4,6 +4,7 @@ - 修复系统代理端口不同步问题 - 修复自定义 `css` 背景图无法生效问题 +- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题 ### ✨ 新增功能 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 173f7a46f..659f5dabc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "reqwest", "reqwest_dav", "runas", + "scopeguard", "serde", "serde_json", "serde_yaml", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6908bba03..843ce2051 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -80,6 +80,7 @@ gethostname = "1.0.2" hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" +scopeguard = "1.2.0" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 070a049bb..33c8fa965 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,10 +1,6 @@ use crate::{ - config::Config, - core::handle, - feat, logging, logging_error, - module::lightweight::entry_lightweight_mode, - process::AsyncHandler, - utils::{logging::Type, resolve}, + config::Config, core::handle, feat, logging, logging_error, + module::lightweight::entry_lightweight_mode, utils::logging::Type, }; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; @@ -14,7 +10,7 @@ use tauri::Manager; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; pub struct Hotkey { - current: Arc>>, // 保存当前的热键设置 + current: Arc>>, } impl Hotkey { @@ -38,7 +34,6 @@ impl Hotkey { enable_global_hotkey ); - // 如果全局热键被禁用,则不注册热键 if !enable_global_hotkey { return Ok(()); } @@ -153,76 +148,14 @@ impl Hotkey { "=== Hotkey Dashboard Window Operation Start ===" ); - // 检查是否在轻量模式下,如果是,需要同步处理 - if crate::module::lightweight::is_in_lightweight_mode() { - logging!( - info, - Type::Hotkey, - true, - "In lightweight mode, calling open_or_close_dashboard directly" - ); - crate::feat::open_or_close_dashboard(); - } else { - AsyncHandler::spawn(move || async move { - logging!( - debug, - Type::Hotkey, - true, - "Toggle dashboard window visibility (async)" - ); + logging!( + info, + Type::Hotkey, + true, + "Using unified WindowManager for hotkey operation (bypass debounce)" + ); - // 检查窗口是否存在 - if let Some(window) = handle::Handle::global().get_window() { - // 如果窗口可见,则隐藏 - match window.is_visible() { - Ok(visible) => { - if visible { - logging!( - info, - Type::Window, - true, - "Window is visible, hiding it" - ); - let _ = window.hide(); - } else { - // 如果窗口不可见,则显示 - logging!( - info, - Type::Window, - true, - "Window is hidden, showing it" - ); - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - } - } - Err(e) => { - logging!( - warn, - Type::Window, - true, - "Failed to check window visibility: {}", - e - ); - let _ = window.show(); - let _ = window.set_focus(); - } - } - } else { - // 如果窗口不存在,创建一个新窗口 - logging!( - info, - Type::Window, - true, - "Window does not exist, creating a new one" - ); - resolve::create_window(true); - } - }); - } + crate::feat::open_or_close_dashboard_hotkey(); logging!( debug, @@ -261,10 +194,8 @@ impl Hotkey { } } } else { - // 直接执行函数,不做任何状态检查 logging!(debug, Type::Hotkey, "Executing function directly"); - // 获取全局热键状态 let is_enable_global_hotkey = Config::verge() .latest() .enable_global_hotkey @@ -274,7 +205,6 @@ impl Hotkey { f(); } else { use crate::utils::window_manager::WindowManager; - // 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键 let is_visible = WindowManager::is_main_window_visible(); let is_focused = WindowManager::is_main_window_focused(); diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index de10715e0..98c46004e 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -39,6 +39,29 @@ use super::handle; #[derive(Clone)] struct TrayState {} +// 托盘点击防抖机制 +static TRAY_CLICK_DEBOUNCE: OnceCell> = OnceCell::new(); +const TRAY_CLICK_DEBOUNCE_MS: u64 = 300; + +fn get_tray_click_debounce() -> &'static Mutex { + TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_tray_click() -> bool { + let debounce_lock = get_tray_click_debounce(); + let mut last_click = debounce_lock.lock(); + let now = Instant::now(); + + if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) { + *last_click = now; + true + } else { + log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", + now.duration_since(*last_click).as_millis()); + false + } +} + #[cfg(target_os = "macos")] pub struct Tray { pub speed_rate: Arc>>, @@ -664,6 +687,11 @@ impl Tray { .. } = event { + // 添加防抖检查,防止快速连击 + if !should_handle_tray_click() { + return; + } + match tray_event.as_str() { "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), @@ -949,12 +977,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "open_window" => { use crate::utils::window_manager::WindowManager; log::info!(target: "app", "托盘菜单点击: 打开窗口"); - // 如果在轻量模式中,先退出轻量模式 + + if !should_handle_tray_click() { + return; + } + if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "当前在轻量模式,正在退出"); crate::module::lightweight::exit_lightweight_mode(); } - // 使用统一的窗口管理器显示窗口 let result = WindowManager::show_main_window(); log::info!(target: "app", "窗口显示结果: {:?}", result); } @@ -977,7 +1008,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "restart_clash" => feat::restart_clash_core(), "restart_app" => feat::restart_app(), "entry_lightweight_mode" => { - // 处理轻量模式的切换 + if !should_handle_tray_click() { + return; + } + let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); if was_lightweight { crate::module::lightweight::exit_lightweight_mode(); @@ -985,7 +1019,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { crate::module::lightweight::entry_lightweight_mode(); } - // 退出轻量模式后显示主窗口 if was_lightweight { use crate::utils::window_manager::WindowManager; let result = WindowManager::show_main_window(); @@ -1002,7 +1035,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { _ => {} } - // 统一调用状态更新 if let Err(e) = Tray::global().update_all_states() { log::warn!(target: "app", "更新托盘状态失败: {}", e); } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 4ac750581..9d43ff4d5 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -11,11 +11,43 @@ use crate::{ /// Open or close the dashboard window #[allow(dead_code)] pub fn open_or_close_dashboard() { + open_or_close_dashboard_internal(false) +} + +/// Open or close the dashboard window (hotkey call, dispatched to main thread) +#[allow(dead_code)] +pub fn open_or_close_dashboard_hotkey() { + open_or_close_dashboard_internal(true) +} + +/// Internal implementation for opening/closing dashboard +fn open_or_close_dashboard_internal(bypass_debounce: bool) { + use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; - log::info!(target: "app", "Attempting to open/close dashboard"); + log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {})", bypass_debounce); - // 检查是否在轻量模式下 + // 热键调用调度到主线程执行,避免 WebView 创建死锁 + if bypass_debounce { + log::info!(target: "app", "热键调用,调度到主线程执行窗口操作"); + + AsyncHandler::spawn(move || async move { + log::info!(target: "app", "主线程中执行热键窗口操作"); + + if crate::module::lightweight::is_in_lightweight_mode() { + log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); + crate::module::lightweight::exit_lightweight_mode(); + log::info!(target: "app", "Creating new window after exiting lightweight mode"); + let result = WindowManager::show_main_window(); + log::info!(target: "app", "Window operation result: {:?}", result); + return; + } + + let result = WindowManager::toggle_main_window(); + log::info!(target: "app", "Window toggle result: {:?}", result); + }); + return; + } if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); crate::module::lightweight::exit_lightweight_mode(); @@ -25,7 +57,6 @@ pub fn open_or_close_dashboard() { return; } - // 使用统一的窗口管理器切换窗口状态 let result = WindowManager::toggle_main_window(); log::info!(target: "app", "Window toggle result: {:?}", result); } diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index b6ebed7cf..f93cc170c 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -13,11 +13,17 @@ use crate::AppHandleManager; use anyhow::{Context, Result}; use delay_timer::prelude::TaskBuilder; -use std::sync::Mutex; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; use tauri::{Listener, Manager}; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; +// 添加退出轻量模式的锁,防止并发调用 +static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); + fn with_lightweight_status(f: F) -> R where F: FnOnce(&mut LightWeightState) -> R, @@ -131,6 +137,25 @@ pub fn entry_lightweight_mode() { // 添加从轻量模式恢复的函数 pub fn exit_lightweight_mode() { + // 使用原子操作检查是否已经在退出过程中,防止并发调用 + if EXITING_LIGHTWEIGHT + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + logging!( + info, + Type::Lightweight, + true, + "轻量模式退出操作已在进行中,跳过重复调用" + ); + return; + } + + // 使用defer确保无论如何都会重置标志 + let _guard = scopeguard::guard((), |_| { + EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst); + }); + // 确保当前确实处于轻量模式才执行退出操作 if !is_in_lightweight_mode() { logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 8e1c0ca7a..dada58ec6 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -13,6 +13,7 @@ use anyhow::{bail, Result}; use once_cell::sync::OnceCell; use parking_lot::{Mutex, RwLock}; use percent_encoding::percent_decode_str; +use scopeguard; use serde_yaml::Mapping; use std::{ sync::Arc, @@ -337,6 +338,12 @@ pub fn create_window(is_show: bool) -> bool { *creating = (true, Instant::now()); + // ScopeGuard 确保创建状态重置,防止 webview 卡死 + let _guard = scopeguard::guard(creating, |mut creating_guard| { + *creating_guard = (false, Instant::now()); + logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置"); + }); + match tauri::WebviewWindowBuilder::new( &handle::Handle::global().app_handle().unwrap(), "main", /* the unique window label */ @@ -419,8 +426,6 @@ pub fn create_window(is_show: bool) -> bool { Ok(newly_created_window) => { logging!(debug, Type::Window, true, "主窗口实例创建成功"); - *creating = (false, Instant::now()); - update_ui_ready_stage(UiReadyStage::NotStarted); AsyncHandler::spawn(move || async move { @@ -534,7 +539,6 @@ pub fn create_window(is_show: bool) -> bool { } Err(e) => { logging!(error, Type::Window, true, "主窗口构建失败: {}", e); - *creating = (false, Instant::now()); // Reset the creating state if window creation failed false } } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index d00656f64..c06cb5190 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -4,6 +4,14 @@ use tauri::{Manager, WebviewWindow, Wry}; #[cfg(target_os = "macos")] use crate::AppHandleManager; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use scopeguard; +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, +}; + /// 窗口操作结果 #[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowOperationResult { @@ -34,6 +42,45 @@ pub enum WindowState { NotExist, } +// 窗口操作防抖机制 +static WINDOW_OPERATION_DEBOUNCE: OnceCell> = OnceCell::new(); +static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false); +const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500; + +fn get_window_operation_debounce() -> &'static Mutex { + WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_window_operation() -> bool { + if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { + log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用"); + return false; + } + + let debounce_lock = get_window_operation_debounce(); + let mut last_operation = debounce_lock.lock(); + let now = Instant::now(); + let elapsed = now.duration_since(*last_operation); + + log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + + if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { + *last_operation = now; + WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); + log::info!(target: "app", "[防抖] 窗口操作被允许执行"); + true + } else { + log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + false + } +} + +fn finish_window_operation() { + WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release); +} + /// 统一的窗口管理器 pub struct WindowManager; @@ -65,6 +112,14 @@ impl WindowManager { /// 智能显示主窗口 pub fn show_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始智能显示主窗口"); logging!( debug, @@ -80,8 +135,11 @@ impl WindowManager { WindowState::NotExist => { logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); if Self::create_new_window() { + logging!(info, Type::Window, true, "窗口创建成功"); + std::thread::sleep(std::time::Duration::from_millis(100)); WindowOperationResult::Created } else { + logging!(warn, Type::Window, true, "窗口创建失败"); WindowOperationResult::Failed } } @@ -91,6 +149,16 @@ impl WindowManager { } WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { if let Some(window) = Self::get_main_window() { + let state_after_check = Self::get_main_window_state(); + if state_after_check == WindowState::VisibleFocused { + logging!( + info, + Type::Window, + true, + "窗口在检查期间已变为可见和有焦点状态" + ); + return WindowOperationResult::NoAction; + } Self::activate_window(&window) } else { WindowOperationResult::Failed @@ -101,6 +169,14 @@ impl WindowManager { /// 切换主窗口显示状态(显示/隐藏) pub fn toggle_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始切换主窗口显示状态"); let current_state = Self::get_main_window_state(); @@ -108,37 +184,61 @@ impl WindowManager { info, Type::Window, true, - "当前窗口状态: {:?}", - current_state + "当前窗口状态: {:?} | 详细状态: {}", + current_state, + Self::get_window_status_info() ); match current_state { WindowState::NotExist => { // 窗口不存在,创建新窗口 + logging!(info, Type::Window, true, "窗口不存在,将创建新窗口"); + // 由于已经有防抖保护,直接调用内部方法 if Self::create_new_window() { WindowOperationResult::Created } else { WindowOperationResult::Failed } } - WindowState::VisibleFocused => { - // 窗口可见且有焦点,隐藏它 - if let Some(window) = Self::get_main_window() { - if window.hide().is_ok() { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden + WindowState::VisibleFocused | WindowState::VisibleUnfocused => { + logging!( + info, + Type::Window, + true, + "窗口可见(焦点状态: {}),将隐藏窗口", + if current_state == WindowState::VisibleFocused { + "有焦点" } else { - WindowOperationResult::Failed + "无焦点" + } + ); + if let Some(window) = Self::get_main_window() { + match window.hide() { + Ok(_) => { + logging!(info, Type::Window, true, "窗口已成功隐藏"); + WindowOperationResult::Hidden + } + Err(e) => { + logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); + WindowOperationResult::Failed + } } } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } - WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { - // 窗口存在但不可见或无焦点,激活它 + WindowState::Minimized | WindowState::Hidden => { + logging!( + info, + Type::Window, + true, + "窗口存在但被隐藏或最小化,将激活窗口" + ); if let Some(window) = Self::get_main_window() { Self::activate_window(&window) } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } @@ -251,7 +351,7 @@ impl WindowManager { .unwrap_or(false) } - /// 创建新窗口现有的实现 + /// 创建新窗口,防抖避免重复调用 fn create_new_window() -> bool { use crate::utils::resolve; resolve::create_window(true)