From 55cde385627d7be726c1cef1c47df55b7edd49e0 Mon Sep 17 00:00:00 2001 From: wonfen Date: Fri, 25 Apr 2025 18:24:16 +0800 Subject: [PATCH] feat: improve core functionality to prevent main process blocking, enhance MihomoManager, and optimize window creation process --- UPDATELOG.md | 1 + src-tauri/src/lib.rs | 39 ++++- src-tauri/src/module/mihomo.rs | 105 ++++++++++--- src-tauri/src/utils/resolve.rs | 266 +++++++++++++++++---------------- 4 files changed, 256 insertions(+), 155 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index f6263a23..a3970989 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -37,6 +37,7 @@ - 切换到规则页面时自动刷新规则数据 - 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - 编辑非激活订阅的时候不在触发当前订阅配置重载 + - 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程。减少应用程序可能出现的主进程卡死情况 ## v2.2.3 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 46910c6b..4b96c60a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,6 +19,7 @@ use tauri::AppHandle; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt; +use tokio::time::{timeout, Duration}; use utils::logging::Type; /// A global singleton handle to the application. @@ -85,13 +86,22 @@ impl AppHandleManager { #[allow(clippy::panic)] pub fn run() { - // 单例检测 + // 单例检测 - 使用超时机制防止阻塞 let app_exists: bool = AsyncHandler::block_on(move || async move { - if server::check_singleton().await.is_err() { - println!("app exists"); - true - } else { - false + match timeout(Duration::from_secs(3), server::check_singleton()).await { + Ok(result) => { + if result.is_err() { + println!("app exists"); + true + } else { + false + } + } + Err(_) => { + // 超时处理 + println!("singleton check timeout, assuming app doesn't exist"); + false + } } }); if app_exists { @@ -139,8 +149,21 @@ pub fn run() { }); }); - AsyncHandler::block_on(move || async move { - resolve::resolve_setup(app).await; + // 使用 block_on 但增加超时保护 + AsyncHandler::block_on(|| async { + match timeout(Duration::from_secs(30), resolve::resolve_setup(app)).await { + Ok(_) => { + logging!(info, Type::Setup, true, "App setup completed successfully"); + } + Err(_) => { + logging!( + error, + Type::Setup, + true, + "App setup timed out, proceeding anyway" + ); + } + } }); Ok(()) diff --git a/src-tauri/src/module/mihomo.rs b/src-tauri/src/module/mihomo.rs index 47929b92..79e53104 100644 --- a/src-tauri/src/module/mihomo.rs +++ b/src-tauri/src/module/mihomo.rs @@ -1,43 +1,92 @@ use crate::config::Config; use mihomo_api; -use once_cell::sync::{Lazy, OnceCell}; -use std::sync::Mutex; +use once_cell::sync::Lazy; +use parking_lot::{Mutex, RwLock}; +use std::time::{Duration, Instant}; use tauri::http::{HeaderMap, HeaderValue}; -#[cfg(target_os = "macos")] -use tokio_tungstenite::tungstenite::http; + +// 缓存的最大有效期(5秒) +const CACHE_TTL: Duration = Duration::from_secs(5); #[derive(Debug, Clone, Default, PartialEq)] pub struct Rate { pub up: u64, pub down: u64, } - +// 缓存MihomoManager实例 +struct MihomoCache { + manager: mihomo_api::MihomoManager, + created_at: Instant, + server: String, +} +// 使用RwLock替代Mutex,允许多个读取操作并发进行 pub struct MihomoManager { - mihomo: Mutex>, + mihomo_cache: RwLock>, + create_lock: Mutex<()>, } impl MihomoManager { fn __global() -> &'static MihomoManager { static INSTANCE: Lazy = Lazy::new(|| MihomoManager { - mihomo: Mutex::new(OnceCell::new()), + mihomo_cache: RwLock::new(None), + create_lock: Mutex::new(()), }); &INSTANCE } pub fn global() -> mihomo_api::MihomoManager { let instance = MihomoManager::__global(); - let (current_server, headers) = MihomoManager::get_clash_client_info().unwrap(); - let lock = instance.mihomo.lock().unwrap(); - if let Some(mihomo) = lock.get() { - if mihomo.get_mihomo_server() == current_server { - return mihomo.clone(); + // 尝试从缓存读取(只需读锁) + { + let cache = instance.mihomo_cache.read(); + if let Some(cache_entry) = &*cache { + let (current_server, _) = MihomoManager::get_clash_client_info() + .unwrap_or_else(|| (String::new(), HeaderMap::new())); + + // 检查缓存是否有效 + if cache_entry.server == current_server + && cache_entry.created_at.elapsed() < CACHE_TTL + { + return cache_entry.manager.clone(); + } } } - lock.set(mihomo_api::MihomoManager::new(current_server, headers)) - .ok(); - lock.get().unwrap().clone() + // 缓存无效,获取创建锁 + let _create_guard = instance.create_lock.lock(); + + // 再次检查缓存(双重检查锁定模式) + { + let cache = instance.mihomo_cache.read(); + if let Some(cache_entry) = &*cache { + let (current_server, _) = MihomoManager::get_clash_client_info() + .unwrap_or_else(|| (String::new(), HeaderMap::new())); + + if cache_entry.server == current_server + && cache_entry.created_at.elapsed() < CACHE_TTL + { + return cache_entry.manager.clone(); + } + } + } + + // 创建新实例 + let (current_server, headers) = MihomoManager::get_clash_client_info() + .unwrap_or_else(|| (String::new(), HeaderMap::new())); + let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers); + + // 更新缓存 + { + let mut cache = instance.mihomo_cache.write(); + *cache = Some(MihomoCache { + manager: manager.clone(), + created_at: Instant::now(), + server: current_server, + }); + } + + manager } } @@ -54,17 +103,31 @@ impl MihomoManager { Some((server, headers)) } + + // 提供默认值的版本,避免在connection_info为None时panic + fn get_clash_client_info_or_default() -> (String, HeaderMap) { + Self::get_clash_client_info().unwrap_or_else(|| { + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse().unwrap()); + ("http://127.0.0.1:9090".to_string(), headers) + }) + } + #[cfg(target_os = "macos")] pub fn get_traffic_ws_url() -> (String, HeaderValue) { - let (url, headers) = MihomoManager::get_clash_client_info().unwrap(); + let (url, headers) = MihomoManager::get_clash_client_info_or_default(); let ws_url = url.replace("http://", "ws://") + "/traffic"; let auth = headers .get("Authorization") - .unwrap() - .to_str() - .unwrap() - .to_string(); - let token = http::header::HeaderValue::from_str(&auth).unwrap(); + .map(|val| val.to_str().unwrap_or("").to_string()) + .unwrap_or_default(); + + // 创建默认的空HeaderValue而不是使用unwrap_or_default + let token = match HeaderValue::from_str(&auth) { + Ok(v) => v, + Err(_) => HeaderValue::from_static(""), + }; + (ws_url, token) } } diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 3910e9a2..51298fc3 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -6,13 +6,14 @@ use crate::{ logging, logging_error, module::lightweight, process::AsyncHandler, - utils::{error, init, logging::Type, server}, + utils::{dirs, error, init, logging::Type, server}, wrap_err, }; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; use parking_lot::{Mutex, RwLock}; use percent_encoding::percent_decode_str; +use serde::{Deserialize, Serialize}; use serde_json; use serde_yaml::Mapping; use std::{ @@ -32,12 +33,23 @@ pub static VERSION: OnceCell = OnceCell::new(); static STATE_WIDTH: OnceCell = OnceCell::new(); static STATE_HEIGHT: OnceCell = OnceCell::new(); +// 定义默认窗口尺寸常量 +const DEFAULT_WIDTH: u32 = 900; +const DEFAULT_HEIGHT: u32 = 700; + // 添加全局UI准备就绪标志 static UI_READY: OnceCell>> = OnceCell::new(); // 窗口创建锁,防止并发创建窗口 static WINDOW_CREATING: OnceCell> = OnceCell::new(); +// 定义窗口状态结构体 +#[derive(Debug, Serialize, Deserialize)] +struct WindowState { + width: Option, + height: Option, +} + fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> { WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) } @@ -195,49 +207,47 @@ pub fn create_window(is_showup: bool) { let _guard = WindowCreateGuard; // 打印 .window-state.json 文件路径 - if let Ok(app_dir) = crate::utils::dirs::app_home_dir() { - let window_state_path = app_dir.join(".window-state.json"); - logging!( - info, - Type::Window, - true, - "窗口状态文件路径: {:?}", - window_state_path - ); + let window_state_file = dirs::app_home_dir() + .ok() + .map(|dir| dir.join(".window-state.json")); + logging!( + info, + Type::Window, + true, + "窗口状态文件路径: {:?}", + window_state_file + ); - // 尝试读取窗口状态文件内容 - if window_state_path.exists() { - match std::fs::read_to_string(&window_state_path) { + // 从文件加载窗口状态 + if let Some(window_state_file_path) = window_state_file { + if window_state_file_path.exists() { + match std::fs::read_to_string(&window_state_file_path) { Ok(content) => { - logging!(info, Type::Window, true, "窗口状态文件内容: {}", content); + logging!( + debug, + Type::Window, + true, + "读取窗口状态文件内容成功: {} 字节", + content.len() + ); - // 解析窗口状态文件 - match serde_json::from_str::(&content) { - Ok(state_json) => { - if let Some(main_window) = state_json.get("main") { - let width = main_window - .get("width") - .and_then(|v| v.as_u64()) - .unwrap_or(0) - as u32; - let height = main_window - .get("height") - .and_then(|v| v.as_u64()) - .unwrap_or(0) - as u32; + match serde_json::from_str::(&content) { + Ok(window_state) => { + logging!( + info, + Type::Window, + true, + "成功解析窗口状态: width={:?}, height={:?}", + window_state.width, + window_state.height + ); - logging!( - info, - Type::Window, - true, - "窗口状态文件中的尺寸: {}x{}", - width, - height - ); - - // 保存读取到的尺寸,用于后续检查 - STATE_WIDTH.get_or_init(|| width); - STATE_HEIGHT.get_or_init(|| height); + // 存储窗口状态以供后续使用 + if let Some(width) = window_state.width { + STATE_WIDTH.set(width).ok(); + } + if let Some(height) = window_state.height { + STATE_HEIGHT.set(height).ok(); } } Err(e) => { @@ -299,56 +309,59 @@ pub fn create_window(is_showup: bool) { } } - // 定义默认窗口大小 - const DEFAULT_WIDTH: u32 = 900; - const DEFAULT_HEIGHT: u32 = 700; - const MIN_WIDTH: u32 = 650; - const MIN_HEIGHT: u32 = 580; + let width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH); + let height = STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT); - #[cfg(target_os = "windows")] - let window = tauri::WebviewWindowBuilder::new( - &app_handle, - "main".to_string(), - tauri::WebviewUrl::App("index.html".into()), - ) - .title("Clash Verge") - .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) - .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) - .decorations(false) - .maximizable(true) - .additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling") - .transparent(true) - .shadow(true) - .visible(false) // 初始不可见,等待UI加载完成后再显示 - .build(); + logging!( + info, + Type::Window, + true, + "Initializing new window with size: {}x{}", + width, + height + ); + // 根据不同平台创建不同配置的窗口 #[cfg(target_os = "macos")] - let window = tauri::WebviewWindowBuilder::new( - &app_handle, - "main".to_string(), - tauri::WebviewUrl::App("index.html".into()), - ) - .decorations(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) - .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) - .visible(false) // 初始不可见,等待UI加载完成后再显示 - .build(); + let win_builder = { + // 基本配置 + let builder = tauri::WebviewWindowBuilder::new( + &app_handle, + "main", + tauri::WebviewUrl::App("index.html".into()), + ) + .title("Clash Verge") + .center() + .decorations(true) + .hidden_title(true) // 隐藏标题文本 + .fullscreen(false) + .inner_size(width as f64, height as f64) + .min_inner_size(520.0, 520.0) + .visible(false); - #[cfg(target_os = "linux")] - let window = tauri::WebviewWindowBuilder::new( + // 尝试设置标题栏样式 + // 注意:根据Tauri版本不同,此API可能有变化 + // 如果编译出错,请注释掉下面这行 + let builder = builder.title_bar_style(tauri::TitleBarStyle::Overlay); + + builder + }; + + #[cfg(not(target_os = "macos"))] + let win_builder = tauri::WebviewWindowBuilder::new( &app_handle, - "main".to_string(), + "main", tauri::WebviewUrl::App("index.html".into()), ) .title("Clash Verge") - .decorations(false) - .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) - .min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) - .transparent(true) - .visible(false) // 初始不可见,等待UI加载完成后再显示 - .build(); + .center() + .fullscreen(false) + .inner_size(width as f64, height as f64) + .min_inner_size(520.0, 520.0) + .visible(false) + .decorations(false); + + let window = win_builder.build(); match window { Ok(window) => { @@ -379,33 +392,43 @@ pub fn create_window(is_showup: bool) { DEFAULT_HEIGHT ); - if state_width < DEFAULT_WIDTH || state_height < DEFAULT_HEIGHT { + // 优化窗口大小设置 + if size.width < state_width || size.height < state_height { logging!( info, Type::Window, true, - "状态文件窗口尺寸小于默认值,将使用默认尺寸: {}x{}", - DEFAULT_WIDTH, - DEFAULT_HEIGHT + "强制设置窗口尺寸: {}x{}", + state_width, + state_height ); - let _ = window.set_size(tauri::LogicalSize::new( - DEFAULT_WIDTH as f64, - DEFAULT_HEIGHT as f64, - )); - } else if size.width != state_width || size.height != state_height { - // 如果API报告的尺寸与状态文件不一致,记录日志 - logging!( - warn, - Type::Window, - true, - "API报告的窗口尺寸与状态文件不一致" - ); + // 尝试不同的方式设置窗口大小 + let _ = window.set_size(tauri::PhysicalSize { + width: state_width, + height: state_height, + }); + + // 关键:等待短暂时间让窗口尺寸生效 + std::thread::sleep(std::time::Duration::from_millis(50)); + + // 再次检查窗口尺寸 + if let Ok(new_size) = window.inner_size() { + logging!( + info, + Type::Window, + true, + "设置后API报告的窗口尺寸: {}x{}", + new_size.width, + new_size.height + ); + } } } - // 创建异步任务处理UI就绪和显示窗口 + // 标记此窗口是否从轻量模式恢复 let was_from_lightweight = from_lightweight; + AsyncHandler::spawn(move || async move { // 处理启动完成 handle::Handle::global().mark_startup_completed(); @@ -425,53 +448,44 @@ pub fn create_window(is_showup: bool) { 5 }; - // 等待UI就绪 - async fn wait_for_ui_ready() { - while !*get_ui_ready().read() { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } + // 使用普通的等待方式替代事件监听,简化实现 + let wait_result = + tokio::time::timeout(Duration::from_secs(timeout_seconds), async { + while !*get_ui_ready().read() { + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await; - // 使用超时机制等待UI就绪 - match tokio::time::timeout( - std::time::Duration::from_secs(timeout_seconds), - wait_for_ui_ready(), - ) - .await - { + // 根据结果处理 + match wait_result { Ok(_) => { - logging!(info, Type::Window, true, "UI准备就绪,显示窗口"); + logging!(info, Type::Window, true, "UI就绪,显示窗口"); } Err(_) => { logging!( warn, Type::Window, true, - "等待UI准备就绪超时({}秒),强制显示窗口", + "等待UI就绪超时({}秒),强制显示窗口", timeout_seconds ); + // 强制设置UI就绪状态 + *get_ui_ready().write() = true; } } - // 无论是否超时,都显示窗口 + // 显示窗口 let _ = window_clone.show(); let _ = window_clone.set_focus(); + + logging!(info, Type::Window, true, "窗口创建和显示流程已完成"); } - } else { - logging!(error, Type::Window, true, "无法获取主窗口"); } }); - - logging!(info, Type::Window, true, "异步任务已创建"); } Err(e) => { - logging!( - error, - Type::Window, - true, - "Failed to create window: {:?}", - e - ); + logging!(error, Type::Window, true, "Failed to create window: {}", e); } } }