From 8cae9d4e0a8f02e00be1eabbc7a9bc29d21ef6dc Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 10 May 2025 01:25:50 +0800 Subject: [PATCH] refactor: use async approach to restructure UI startup logic and resolve various freeze issues during launch --- UPDATELOG.md | 2 + src-tauri/src/cmd/app.rs | 32 +- src-tauri/src/core/tray/mod.rs | 242 ++++++++----- src-tauri/src/lib.rs | 67 +++- src-tauri/src/module/lightweight.rs | 18 - src-tauri/src/utils/dirs.rs | 75 +++- src-tauri/src/utils/error.rs | 40 --- src-tauri/src/utils/mod.rs | 1 - src-tauri/src/utils/network.rs | 196 +++++----- src-tauri/src/utils/resolve.rs | 532 +++++++++++----------------- src/pages/_layout.tsx | 59 ++- 11 files changed, 648 insertions(+), 616 deletions(-) delete mode 100644 src-tauri/src/utils/error.rs diff --git a/UPDATELOG.md b/UPDATELOG.md index 030ee3375..b53843b5f 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -18,6 +18,7 @@ - 切换自定义代理地址导致系统代理状态异常 - Macos TUN 默认无效网卡名称 - 托盘更改订阅后 UI 不同步的问题 + - 修复提权漏洞,改用带认证的 IPC 通信(后续还可以加强完善认证密钥创建和管理机制) #### 新增了: - 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好 @@ -47,6 +48,7 @@ - 重构前端通知系统分离通知线程防止前端卡死 - 优化网络请求和错误处理 - 重构通知系统 + - 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题 ## v2.2.3 diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 76cf9829e..f44928ce7 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -87,23 +87,18 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache"); let icon_path = icon_cache_dir.join(&name); - // 如果文件已存在,直接返回路径 if icon_path.exists() { return Ok(icon_path.to_string_lossy().to_string()); } - // 确保缓存目录存在 if !icon_cache_dir.exists() { let _ = std::fs::create_dir_all(&icon_cache_dir); } - // 使用临时文件名来下载 let temp_path = icon_cache_dir.join(format!("{}.downloading", &name)); - // 下载文件到临时位置 let response = wrap_err!(reqwest::get(&url).await)?; - // 检查内容类型是否为图片 let content_type = response .headers() .get(reqwest::header::CONTENT_TYPE) @@ -112,16 +107,13 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult let is_image = content_type.starts_with("image/"); - // 获取响应内容 let content = wrap_err!(response.bytes().await)?; - // 检查内容是否为HTML (针对CDN错误页面) let is_html = content.len() > 15 && (content.starts_with(b" CmdResult wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?; } - // 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件 if !icon_path.exists() { match std::fs::rename(&temp_path, &icon_path) { Ok(_) => {} @@ -223,6 +214,29 @@ pub fn notify_ui_ready() -> CmdResult<()> { Ok(()) } +/// UI加载阶段 +#[tauri::command] +pub fn update_ui_stage(stage: String) -> CmdResult<()> { + log::info!(target: "app", "UI加载阶段更新: {}", stage); + + use crate::utils::resolve::UiReadyStage; + + let stage_enum = match stage.as_str() { + "NotStarted" => UiReadyStage::NotStarted, + "Loading" => UiReadyStage::Loading, + "DomReady" => UiReadyStage::DomReady, + "ResourcesLoaded" => UiReadyStage::ResourcesLoaded, + "Ready" => UiReadyStage::Ready, + _ => { + log::warn!(target: "app", "未知的UI加载阶段: {}", stage); + return Err(format!("未知的UI加载阶段: {}", stage)); + } + }; + + crate::utils::resolve::update_ui_ready_stage(stage_enum); + Ok(()) +} + /// 重置UI就绪状态 #[tauri::command] pub fn reset_ui_ready_state() -> CmdResult<()> { diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index f7e12fe98..79fcc0878 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -11,7 +11,7 @@ use crate::{ mihomo::Rate, }, resolve, - utils::{dirs::find_target_icons, i18n::t, logging::Type, resolve::VERSION}, + utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, }; use anyhow::Result; @@ -29,7 +29,7 @@ use std::sync::Arc; use tauri::{ menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, tray::{MouseButton, MouseButtonState, TrayIconEvent}, - App, AppHandle, Wry, + AppHandle, Wry, }; #[cfg(target_os = "macos")] use tokio::sync::broadcast; @@ -178,52 +178,6 @@ impl Tray { Ok(()) } - pub fn create_systray(&self, app: &App) -> Result<()> { - let mut builder = TrayIconBuilder::with_id("main") - .icon(app.default_window_icon().unwrap().clone()) - .icon_as_template(false); - - #[cfg(any(target_os = "macos", target_os = "windows"))] - { - let tray_event = { Config::verge().latest().tray_event.clone() }; - let tray_event: String = tray_event.unwrap_or("main_window".into()); - if tray_event.as_str() != "tray_menu" { - builder = builder.show_menu_on_left_click(false); - } - } - - let tray = builder.build(app)?; - - tray.on_tray_icon_event(|_, event| { - let tray_event = { Config::verge().latest().tray_event.clone() }; - let tray_event: String = tray_event.unwrap_or("main_window".into()); - log::debug!(target: "app","tray event: {:?}", tray_event); - - if let TrayIconEvent::Click { - button: MouseButton::Left, - button_state: MouseButtonState::Down, - .. - } = event - { - match tray_event.as_str() { - "system_proxy" => feat::toggle_system_proxy(), - "tun_mode" => feat::toggle_tun_mode(None), - "main_window" => { - // 如果在轻量模式中,先退出轻量模式 - if crate::module::lightweight::is_in_lightweight_mode() { - crate::module::lightweight::exit_lightweight_mode(); - } - // 然后创建窗口 - resolve::create_window(true) - } - _ => {} - } - } - }); - tray.on_menu_event(on_menu_event); - Ok(()) - } - /// 更新托盘点击行为 pub fn update_click_behavior(&self) -> Result<()> { let app_handle = handle::Handle::global().app_handle().unwrap(); @@ -239,7 +193,14 @@ impl Tray { /// 更新托盘菜单 pub fn update_menu(&self) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在"); + return Ok(()); // 早期返回,避免panic + } + }; + let verge = Config::verge().latest().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -258,27 +219,47 @@ impl Tray { .unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); - let tray = app_handle.tray_by_id("main").unwrap(); - let _ = tray.set_menu(Some(create_tray_menu( - &app_handle, - Some(mode.as_str()), - *system_proxy, - *tun_mode, - profile_uid_and_name, - is_lightweight_mode, - )?)); - Ok(()) + match app_handle.tray_by_id("main") { + Some(tray) => { + let _ = tray.set_menu(Some(create_tray_menu( + &app_handle, + Some(mode.as_str()), + *system_proxy, + *tun_mode, + profile_uid_and_name, + is_lightweight_mode, + )?)); + Ok(()) + } + None => { + log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在"); + Ok(()) + } + } } /// 更新托盘图标 pub fn update_icon(&self, rate: Option) -> Result<()> { + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::warn!(target: "app", "更新托盘图标失败: app_handle不存在"); + return Ok(()); + } + }; + + let tray = match app_handle.tray_by_id("main") { + Some(tray) => tray, + None => { + log::warn!(target: "app", "更新托盘图标失败: 托盘不存在"); + return Ok(()); + } + }; + let verge = Config::verge().latest().clone(); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let app_handle = handle::Handle::global().app_handle().unwrap(); - let tray = app_handle.tray_by_id("main").unwrap(); - let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { (true, true) => TrayState::get_tun_tray_icon(), (true, false) => TrayState::get_sysproxy_tray_icon(), @@ -302,8 +283,12 @@ impl Tray { Some(rate) } else { let guard = self.speed_rate.lock(); - if let Some(rate) = guard.as_ref().unwrap().get_curent_rate() { - Some(rate) + if let Some(guard) = guard.as_ref() { + if let Some(rate) = guard.get_curent_rate() { + Some(rate) + } else { + Some(Rate::default()) + } } else { Some(Rate::default()) } @@ -320,9 +305,10 @@ impl Tray { }; let rate = rate_guard.as_ref(); - let rate_bytes = SpeedRate::add_speed_text(is_custom_icon, bytes, rate).unwrap(); - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); - let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful); + if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) { + let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); + let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful); + } } Ok(()) } @@ -336,8 +322,21 @@ impl Tray { /// 更新托盘提示 pub fn update_tooltip(&self) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let version = VERSION.get().unwrap(); + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::warn!(target: "app", "更新托盘提示失败: app_handle不存在"); + return Ok(()); + } + }; + + let version = match VERSION.get() { + Some(v) => v, + None => { + log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在"); + return Ok(()); + } + }; let verge = Config::verge().latest().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); @@ -354,23 +353,28 @@ impl Tray { let profiles = Config::profiles(); let profiles = profiles.latest(); if let Some(current_profile_uid) = profiles.get_current() { - let current_profile = profiles.get_item(¤t_profile_uid); - current_profile_name = match ¤t_profile.unwrap().name { - Some(profile_name) => profile_name.to_string(), - None => current_profile_name, - }; + if let Ok(profile) = profiles.get_item(¤t_profile_uid) { + current_profile_name = match &profile.name { + Some(profile_name) => profile_name.to_string(), + None => current_profile_name, + }; + } }; - let tray = app_handle.tray_by_id("main").unwrap(); - let _ = tray.set_tooltip(Some(&format!( - "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", - t("SysProxy"), - switch_map[system_proxy], - t("TUN"), - switch_map[tun_mode], - t("Profile"), - current_profile_name - ))); + if let Some(tray) = app_handle.tray_by_id("main") { + let _ = tray.set_tooltip(Some(&format!( + "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", + t("SysProxy"), + switch_map[system_proxy], + t("TUN"), + switch_map[tun_mode], + t("Profile"), + current_profile_name + ))); + } else { + log::warn!(target: "app", "更新托盘提示失败: 托盘不存在"); + } + Ok(()) } @@ -532,6 +536,57 @@ impl Tray { drop(tx); } } + + pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { + log::info!(target: "app", "正在从AppHandle创建系统托盘"); + + // 获取图标 + let icon_bytes = TrayState::get_common_tray_icon().1; + let icon = tauri::image::Image::from_bytes(&icon_bytes)?; + + let mut builder = TrayIconBuilder::with_id("main") + .icon(icon) + .icon_as_template(false); + + #[cfg(any(target_os = "macos", target_os = "windows"))] + { + let tray_event = { Config::verge().latest().tray_event.clone() }; + let tray_event: String = tray_event.unwrap_or("main_window".into()); + if tray_event.as_str() != "tray_menu" { + builder = builder.show_menu_on_left_click(false); + } + } + + let tray = builder.build(app_handle)?; + + tray.on_tray_icon_event(|_, event| { + let tray_event = { Config::verge().latest().tray_event.clone() }; + let tray_event: String = tray_event.unwrap_or("main_window".into()); + log::debug!(target: "app","tray event: {:?}", tray_event); + + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Down, + .. + } = event + { + match tray_event.as_str() { + "system_proxy" => feat::toggle_system_proxy(), + "tun_mode" => feat::toggle_tun_mode(None), + "main_window" => { + if crate::module::lightweight::is_in_lightweight_mode() { + crate::module::lightweight::exit_lightweight_mode(); + } + let _ = resolve::create_window(true); + } + _ => {} + } + } + }); + tray.on_menu_event(on_menu_event); + log::info!(target: "app", "系统托盘创建成功"); + Ok(()) + } } fn create_tray_menu( @@ -543,7 +598,10 @@ fn create_tray_menu( is_lightweight_mode: bool, ) -> Result> { let mode = mode.unwrap_or(""); - let version = VERSION.get().unwrap(); + + let unknown_version = String::from("unknown"); + let version = VERSION.get().unwrap_or(&unknown_version); + let hotkeys = Config::verge() .latest() .hotkeys @@ -779,14 +837,20 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { crate::module::lightweight::exit_lightweight_mode(); } // 然后创建窗口 - resolve::create_window(true) + let _ = resolve::create_window(true); } "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), "copy_env" => feat::copy_clash_env(), - "open_app_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_app_dir()), - "open_core_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_core_dir()), - "open_logs_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_logs_dir()), + "open_app_dir" => { + let _ = cmd::open_app_dir(); + } + "open_core_dir" => { + let _ = cmd::open_core_dir(); + } + "open_logs_dir" => { + let _ = cmd::open_logs_dir(); + } "restart_clash" => feat::restart_clash_core(), "restart_app" => feat::restart_app(), "entry_lightweight_mode" => entry_lightweight_mode(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2d0430cd0..fae1ff1a0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -86,23 +86,30 @@ impl AppHandleManager { #[allow(clippy::panic)] pub fn run() { - // 初始化网络管理器 utils::network::NetworkManager::global().init(); - // 单例检测 - 使用超时机制防止阻塞 + let _ = utils::dirs::init_portable_flag(); + + // 单例检测 let app_exists: bool = AsyncHandler::block_on(move || async move { + logging!(info, Type::Setup, true, "开始检查单例实例..."); match timeout(Duration::from_secs(3), server::check_singleton()).await { Ok(result) => { if result.is_err() { - println!("app exists"); + logging!(info, Type::Setup, true, "检测到已有应用实例运行"); true } else { + logging!(info, Type::Setup, true, "未检测到其他应用实例"); false } } Err(_) => { - // 超时处理 - println!("singleton check timeout, assuming app doesn't exist"); + logging!( + warn, + Type::Setup, + true, + "单例检查超时,假定没有其他实例运行" + ); false } } @@ -136,9 +143,11 @@ pub fn run() { .build(), ) .setup(|app| { + logging!(info, Type::Setup, true, "开始应用初始化..."); #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { use tauri_plugin_deep_link::DeepLinkExt; + logging!(info, Type::Setup, true, "注册深层链接..."); logging_error!(Type::System, true, app.deep_link().register_all()); } app.deep_link().on_open_url(|event| { @@ -152,23 +161,49 @@ pub fn run() { }); }); - // 使用 block_on 但增加超时保护 - AsyncHandler::block_on(|| async { - match timeout(Duration::from_secs(30), resolve::resolve_setup(app)).await { + // 异步处理 + let app_handle = app.app_handle().clone(); + AsyncHandler::spawn(move || async move { + logging!(info, Type::Setup, true, "异步执行应用设置..."); + match timeout( + Duration::from_secs(30), + resolve::resolve_setup_async(&app_handle), + ) + .await + { Ok(_) => { - logging!(info, Type::Setup, true, "App setup completed successfully"); + logging!(info, Type::Setup, true, "应用设置成功完成"); } Err(_) => { logging!( error, Type::Setup, true, - "App setup timed out, proceeding anyway" + "应用设置超时(30秒),继续执行后续流程" ); } } }); + logging!(info, Type::Setup, true, "执行主要设置操作..."); + + logging!(info, Type::Setup, true, "初始化AppHandleManager..."); + AppHandleManager::global().init(app.app_handle().clone()); + + logging!(info, Type::Setup, true, "初始化核心句柄..."); + core::handle::Handle::global().init(app.app_handle()); + + logging!(info, Type::Setup, true, "初始化配置..."); + if let Err(e) = utils::init::init_config() { + logging!(error, Type::Setup, true, "初始化配置失败: {}", e); + } + + logging!(info, Type::Setup, true, "初始化资源..."); + if let Err(e) = utils::init::init_resources() { + logging!(error, Type::Setup, true, "初始化资源失败: {}", e); + } + + logging!(info, Type::Setup, true, "初始化完成,继续执行"); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -184,8 +219,9 @@ pub fn run() { cmd::get_system_hostname, cmd::restart_core, cmd::restart_app, - // 添加新的命令 + // 启动命令 cmd::notify_ui_ready, + cmd::update_ui_stage, cmd::reset_ui_ready_state, cmd::get_running_mode, cmd::get_app_uptime, @@ -279,6 +315,7 @@ pub fn run() { app.run(|app_handle, e| match e { tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { + logging!(info, Type::System, true, "应用就绪或恢复"); AppHandleManager::global().init(app_handle.clone()); #[cfg(target_os = "macos")] { @@ -286,6 +323,7 @@ pub fn run() { .get_handle() .get_webview_window("main") { + logging!(info, Type::Window, true, "设置macOS窗口标题"); let _ = window.set_title("Clash Verge"); } } @@ -323,8 +361,11 @@ pub fn run() { } println!("closing window..."); api.prevent_close(); - let window = core::handle::Handle::global().get_window().unwrap(); - let _ = window.hide(); + if let Some(window) = core::handle::Handle::global().get_window() { + let _ = window.hide(); + } else { + logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); + } } tauri::WindowEvent::Focused(true) => { #[cfg(target_os = "macos")] diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 690a2ad1e..d8033f17b 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -16,8 +16,6 @@ use std::{ }; use tauri::{Listener, Manager}; -pub static AUTO_LIGHT_WEIGHT_MODE_INIT: OnceCell<()> = OnceCell::new(); - const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; // 轻量模式状态标志 @@ -217,19 +215,3 @@ fn cancel_light_weight_timer() -> Result<()> { Ok(()) } - -pub fn run_once_auto_lightweight() { - AUTO_LIGHT_WEIGHT_MODE_INIT.get_or_init(|| { - let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false); - let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false); - if enable_auto && is_silent_start { - logging!( - info, - Type::Lightweight, - true, - "Add timer listener when creating window in silent start mode" - ); - enable_auto_light_weight_mode(); - } - }); -} diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index ec54b37c8..5669a5dbd 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -49,12 +49,60 @@ pub fn app_home_dir() -> Result { .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?; return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID)); } - let app_handle = handle::Handle::global().app_handle().unwrap(); + + // 避免在Handle未初始化时崩溃 + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::warn!(target: "app", "app_handle not initialized, using default path"); + // 使用可执行文件目录作为备用 + let exe_path = tauri::utils::platform::current_exe()?; + let exe_dir = exe_path + .parent() + .ok_or(anyhow::anyhow!("failed to get executable directory"))?; + + // 使用系统临时目录 + 应用ID + #[cfg(target_os = "windows")] + { + if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") { + let path = PathBuf::from(local_app_data).join(APP_ID); + return Ok(path); + } + } + + #[cfg(target_os = "macos")] + { + if let Some(home) = std::env::var_os("HOME") { + let path = PathBuf::from(home) + .join("Library") + .join("Application Support") + .join(APP_ID); + return Ok(path); + } + } + + #[cfg(target_os = "linux")] + { + if let Some(home) = std::env::var_os("HOME") { + let path = PathBuf::from(home) + .join(".local") + .join("share") + .join(APP_ID); + return Ok(path); + } + } + + // 如果无法获取系统目录,则回退到可执行文件目录 + let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID); + log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir); + return Ok(fallback_dir); + } + }; match app_handle.path().data_dir() { Ok(dir) => Ok(dir.join(APP_ID)), Err(e) => { - log::error!(target:"app", "Failed to get the app home directory: {}", e); + log::error!(target: "app", "Failed to get the app home directory: {}", e); Err(anyhow::anyhow!("Failed to get the app homedirectory")) } } @@ -62,11 +110,24 @@ pub fn app_home_dir() -> Result { /// get the resources dir pub fn app_resources_dir() -> Result { - let app_handle = handle::Handle::global().app_handle().unwrap(); + // 避免在Handle未初始化时崩溃 + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::warn!(target: "app", "app_handle not initialized in app_resources_dir, using fallback"); + // 使用可执行文件目录作为备用 + let exe_dir = tauri::utils::platform::current_exe()? + .parent() + .ok_or(anyhow::anyhow!("failed to get executable directory"))? + .to_path_buf(); + return Ok(exe_dir.join("resources")); + } + }; + match app_handle.path().resource_dir() { Ok(dir) => Ok(dir.join("resources")), Err(e) => { - log::error!(target:"app", "Failed to get the resource directory: {}", e); + log::error!(target: "app", "Failed to get the resource directory: {}", e); Err(anyhow::anyhow!("Failed to get the resource directory")) } } @@ -126,12 +187,14 @@ pub fn profiles_path() -> Result { #[cfg(target_os = "macos")] pub fn service_path() -> Result { - Ok(app_resources_dir()?.join("clash-verge-service")) + let res_dir = app_resources_dir()?; + Ok(res_dir.join("clash-verge-service")) } #[cfg(windows)] pub fn service_path() -> Result { - Ok(app_resources_dir()?.join("clash-verge-service.exe")) + let res_dir = app_resources_dir()?; + Ok(res_dir.join("clash-verge-service.exe")) } pub fn service_log_file() -> Result { diff --git a/src-tauri/src/utils/error.rs b/src-tauri/src/utils/error.rs deleted file mode 100644 index d661b6099..000000000 --- a/src-tauri/src/utils/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::log_err; -use anyhow; -use std::{ - backtrace::{Backtrace, BacktraceStatus}, - thread, -}; - -pub fn redirect_panic_to_log() { - std::panic::set_hook(Box::new(move |panic_info| { - let thread = thread::current(); - let thread_name = thread.name().unwrap_or(""); - let payload = panic_info.payload(); - - let payload = if let Some(s) = payload.downcast_ref::<&str>() { - &**s - } else if let Some(s) = payload.downcast_ref::() { - s - } else { - &format!("{:?}", payload) - }; - - let location = panic_info - .location() - .map(|l| l.to_string()) - .unwrap_or("unknown location".to_string()); - - let backtrace = Backtrace::capture(); - let backtrace = if backtrace.status() == BacktraceStatus::Captured { - &format!("stack backtrace:\n{}", backtrace) - } else { - "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace" - }; - - let err: Result<(), anyhow::Error> = Err(anyhow::anyhow!(format!( - "thread '{}' panicked at {}:\n{}\n{}", - thread_name, location, payload, backtrace - ))); - log_err!(err); - })); -} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 698b499a9..6a0a09fb5 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod autostart; pub mod dirs; -pub mod error; pub mod help; pub mod i18n; pub mod init; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index 9a4cce381..542e2ef9e 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -128,118 +128,118 @@ impl NetworkManager { *error_count = 0; } } + /* + /// 获取或创建自代理客户端 + fn get_or_create_self_proxy_client(&self) -> Client { + if self.should_reset_clients() { + self.reset_clients(); + } - /// 获取或创建自代理客户端 - fn get_or_create_self_proxy_client(&self) -> Client { - if self.should_reset_clients() { - self.reset_clients(); - } + let mut client_guard = self.self_proxy_client.lock().unwrap(); - let mut client_guard = self.self_proxy_client.lock().unwrap(); + if client_guard.is_none() { + let port = Config::verge() + .latest() + .verge_mixed_port + .unwrap_or(Config::clash().data().get_mixed_port()); - if client_guard.is_none() { - let port = Config::verge() - .latest() - .verge_mixed_port - .unwrap_or(Config::clash().data().get_mixed_port()); + let proxy_scheme = format!("http://127.0.0.1:{port}"); - let proxy_scheme = format!("http://127.0.0.1:{port}"); + let mut builder = ClientBuilder::new() + .use_rustls_tls() + .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) + .pool_idle_timeout(POOL_IDLE_TIMEOUT) + .connect_timeout(DEFAULT_CONNECT_TIMEOUT) + .timeout(DEFAULT_REQUEST_TIMEOUT) + .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) + .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) + .http2_adaptive_window(true) + .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) + .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) + .http2_max_frame_size(H2_MAX_FRAME_SIZE) + .tcp_keepalive(Some(Duration::from_secs(10))) + .http2_prior_knowledge() + .http2_max_header_list_size(16 * 1024); - let mut builder = ClientBuilder::new() - .use_rustls_tls() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(DEFAULT_CONNECT_TIMEOUT) - .timeout(DEFAULT_REQUEST_TIMEOUT) - .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) - .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) - .http2_adaptive_window(true) - .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) - .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) - .http2_max_frame_size(H2_MAX_FRAME_SIZE) - .tcp_keepalive(Some(Duration::from_secs(10))) - .http2_prior_knowledge() - .http2_max_header_list_size(16 * 1024); + if let Ok(proxy) = Proxy::http(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = Proxy::https(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = Proxy::all(&proxy_scheme) { + builder = builder.proxy(proxy); + } - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } + let client = builder.build().expect("Failed to build self_proxy client"); + *client_guard = Some(client); + } - let client = builder.build().expect("Failed to build self_proxy client"); - *client_guard = Some(client); - } + client_guard.as_ref().unwrap().clone() + } - client_guard.as_ref().unwrap().clone() - } + /// 获取或创建系统代理客户端 + fn get_or_create_system_proxy_client(&self) -> Client { + if self.should_reset_clients() { + self.reset_clients(); + } - /// 获取或创建系统代理客户端 - fn get_or_create_system_proxy_client(&self) -> Client { - if self.should_reset_clients() { - self.reset_clients(); - } + let mut client_guard = self.system_proxy_client.lock().unwrap(); - let mut client_guard = self.system_proxy_client.lock().unwrap(); + if client_guard.is_none() { + use sysproxy::Sysproxy; - if client_guard.is_none() { - use sysproxy::Sysproxy; + let mut builder = ClientBuilder::new() + .use_rustls_tls() + .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) + .pool_idle_timeout(POOL_IDLE_TIMEOUT) + .connect_timeout(DEFAULT_CONNECT_TIMEOUT) + .timeout(DEFAULT_REQUEST_TIMEOUT) + .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) + .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) + .http2_adaptive_window(true) + .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) + .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) + .http2_max_frame_size(H2_MAX_FRAME_SIZE) + .tcp_keepalive(Some(Duration::from_secs(10))) + .http2_prior_knowledge() + .http2_max_header_list_size(16 * 1024); - let mut builder = ClientBuilder::new() - .use_rustls_tls() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(DEFAULT_CONNECT_TIMEOUT) - .timeout(DEFAULT_REQUEST_TIMEOUT) - .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) - .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) - .http2_adaptive_window(true) - .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) - .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) - .http2_max_frame_size(H2_MAX_FRAME_SIZE) - .tcp_keepalive(Some(Duration::from_secs(10))) - .http2_prior_knowledge() - .http2_max_header_list_size(16 * 1024); + if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { + let proxy_scheme = format!("http://{}:{}", p.host, p.port); - if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { - let proxy_scheme = format!("http://{}:{}", p.host, p.port); + if let Ok(proxy) = Proxy::http(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = Proxy::https(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = Proxy::all(&proxy_scheme) { + builder = builder.proxy(proxy); + } + } - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } - } + let client = builder + .build() + .expect("Failed to build system_proxy client"); + *client_guard = Some(client); + } - let client = builder - .build() - .expect("Failed to build system_proxy client"); - *client_guard = Some(client); - } - - client_guard.as_ref().unwrap().clone() - } - - /// 根据代理设置选择合适的客户端 - pub fn get_client(&self, proxy_type: ProxyType) -> Client { - match proxy_type { - ProxyType::NoProxy => { - let client_guard = self.no_proxy_client.lock().unwrap(); - client_guard.as_ref().unwrap().clone() - } - ProxyType::SelfProxy => self.get_or_create_self_proxy_client(), - ProxyType::SystemProxy => self.get_or_create_system_proxy_client(), - } - } + client_guard.as_ref().unwrap().clone() + } + /// 根据代理设置选择合适的客户端 + pub fn get_client(&self, proxy_type: ProxyType) -> Client { + match proxy_type { + ProxyType::NoProxy => { + let client_guard = self.no_proxy_client.lock().unwrap(); + client_guard.as_ref().unwrap().clone() + } + ProxyType::SelfProxy => self.get_or_create_self_proxy_client(), + ProxyType::SystemProxy => self.get_or_create_system_proxy_client(), + } + } + */ /// 创建带有自定义选项的HTTP请求 pub fn create_request( &self, @@ -335,7 +335,7 @@ impl NetworkManager { client.get(url) } - /// 执行GET请求,添加错误跟踪 + /* /// 执行GET请求,添加错误跟踪 pub async fn get( &self, url: &str, @@ -370,7 +370,7 @@ impl NetworkManager { )) } } - } + } */ pub async fn get_with_interrupt( &self, diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 1bb9b4a33..228572dc4 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -1,12 +1,11 @@ #[cfg(target_os = "macos")] -use crate::AppHandleManager; use crate::{ config::{Config, IVerge, PrfItem}, core::*, logging, logging_error, module::lightweight, process::AsyncHandler, - utils::{dirs, error, init, logging::Type, server}, + utils::{init, logging::Type, server}, wrap_err, }; use anyhow::{bail, Result}; @@ -14,14 +13,13 @@ 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::{ net::TcpListener, sync::Arc, time::{Duration, Instant}, }; -use tauri::{App, Emitter, Manager}; +use tauri::{AppHandle, Emitter, Manager}; use tauri::Url; //#[cfg(not(target_os = "linux"))] @@ -29,10 +27,6 @@ use tauri::Url; 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; @@ -43,6 +37,35 @@ static UI_READY: OnceCell>> = OnceCell::new(); // 窗口创建锁,防止并发创建窗口 static WINDOW_CREATING: OnceCell> = OnceCell::new(); +// UI就绪阶段状态枚举 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UiReadyStage { + NotStarted, + Loading, + DomReady, + ResourcesLoaded, + Ready, +} + +// UI就绪详细状态 +#[derive(Debug)] +struct UiReadyState { + stage: RwLock, + last_update: RwLock, +} + +impl Default for UiReadyState { + fn default() -> Self { + Self { + stage: RwLock::new(UiReadyStage::NotStarted), + last_update: RwLock::new(Instant::now()), + } + } +} + +// 获取UI就绪状态细节 +static UI_READY_STATE: OnceCell> = OnceCell::new(); + // 定义窗口状态结构体 #[derive(Debug, Serialize, Deserialize)] struct WindowState { @@ -58,16 +81,54 @@ fn get_ui_ready() -> &'static Arc> { UI_READY.get_or_init(|| Arc::new(RwLock::new(false))) } +fn get_ui_ready_state() -> &'static Arc { + UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default())) +} + +// 更新UI准备阶段 +pub fn update_ui_ready_stage(stage: UiReadyStage) { + let state = get_ui_ready_state(); + let mut stage_lock = state.stage.write(); + let mut time_lock = state.last_update.write(); + + *stage_lock = stage; + *time_lock = Instant::now(); + + logging!( + info, + Type::Window, + true, + "UI准备阶段更新: {:?}, 耗时: {:?}ms", + stage, + time_lock.elapsed().as_millis() + ); + + // 如果是最终阶段,标记UI完全就绪 + if stage == UiReadyStage::Ready { + mark_ui_ready(); + } +} + // 标记UI已准备就绪 pub fn mark_ui_ready() { let mut ready = get_ui_ready().write(); *ready = true; + logging!(info, Type::Window, true, "UI已标记为完全就绪"); } // 重置UI就绪状态 pub fn reset_ui_ready() { - let mut ready = get_ui_ready().write(); - *ready = false; + { + let mut ready = get_ui_ready().write(); + *ready = false; + } + { + let state = get_ui_ready_state(); + let mut stage = state.stage.write(); + let mut time = state.last_update.write(); + *stage = UiReadyStage::NotStarted; + *time = Instant::now(); + } logging!(info, Type::Window, true, "UI就绪状态已重置"); } @@ -88,54 +149,53 @@ pub fn find_unused_port() -> Result { } } -/// handle something when start app -pub async fn resolve_setup(app: &mut App) { - error::redirect_panic_to_log(); - #[cfg(target_os = "macos")] - { - AppHandleManager::global().init(app.app_handle().clone()); - AppHandleManager::global().set_activation_policy_accessory(); +/// 异步方式处理启动后的额外任务 +pub async fn resolve_setup_async(app_handle: &AppHandle) { + logging!(info, Type::Setup, true, "执行异步设置任务..."); + + if VERSION.get().is_none() { + let version = app_handle.package_info().version.to_string(); + VERSION.get_or_init(|| { + logging!(info, Type::Setup, true, "初始化版本信息: {}", version); + version.clone() + }); } - let version = app.package_info().version.to_string(); - handle::Handle::global().init(app.app_handle()); - VERSION.get_or_init(|| version.clone()); - - logging_error!(Type::Config, true, init::init_config()); - logging_error!(Type::Setup, true, init::init_resources()); logging_error!(Type::Setup, true, init::init_scheme()); + logging_error!(Type::Setup, true, init::startup_script().await); - // 诊断服务状态 - /* logging!(info, Type::Service, true, "执行服务状态诊断"); - { - match crate::core::service::diagnose_service().await { - Ok(_) => { - logging!(info, Type::Service, true, "服务诊断完成"); - }, - Err(e) => { - logging!(error, Type::Service, true, "服务诊断出错: {}", e); - }, - } - } */ - - // 处理随机端口 logging_error!(Type::System, true, resolve_random_port_config()); - // 启动核心 - logging!(trace, Type::Config, true, "Initial config"); + + logging!(trace, Type::Config, true, "初始化配置..."); logging_error!(Type::Config, true, Config::init_config().await); - logging!(trace, Type::Core, "Starting CoreManager"); + logging!(trace, Type::Core, true, "启动核心管理器..."); logging_error!(Type::Core, true, CoreManager::global().init().await); - // setup a simple http server for singleton - log::trace!(target: "app", "launch embed server"); + log::trace!(target: "app", "启动内嵌服务器..."); server::embed_server(); - log::trace!(target: "app", "Initial system tray"); logging_error!(Type::Tray, true, tray::Tray::global().init()); - logging_error!(Type::Tray, true, tray::Tray::global().create_systray(app)); + if let Some(app_handle) = handle::Handle::global().app_handle() { + logging!(info, Type::Tray, true, "创建系统托盘..."); + let result = tray::Tray::global().create_tray_from_handle(&app_handle); + if result.is_ok() { + logging!(info, Type::Tray, true, "系统托盘创建成功"); + } else if let Err(e) = result { + logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e); + } + } else { + logging!( + error, + Type::Tray, + true, + "无法创建系统托盘: app_handle不存在" + ); + } + + // 更新系统代理 logging_error!( Type::System, true, @@ -147,11 +207,14 @@ pub async fn resolve_setup(app: &mut App) { sysopt::Sysopt::global().init_guard_sysproxy() ); + // 创建窗口 let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false); create_window(!is_silent_start); + // 初始化定时器 logging_error!(Type::System, true, timer::Timer::global().init()); + // 自动进入轻量模式 let enable_auto_light_weight_mode = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false); if enable_auto_light_weight_mode && !is_silent_start { @@ -160,12 +223,13 @@ pub async fn resolve_setup(app: &mut App) { logging_error!(Type::Tray, true, tray::Tray::global().update_part()); - // 初始化热键 - logging!(trace, Type::System, true, "Initial hotkeys"); + logging!(trace, Type::System, true, "初始化热键..."); logging_error!(Type::System, true, hotkey::Hotkey::global().init()); + + logging!(info, Type::Setup, true, "异步设置任务完成"); } -/// reset system proxy (异步版) +/// reset system proxy (异步) pub async fn resolve_reset_async() { #[cfg(target_os = "macos")] logging!(info, Type::Tray, true, "Unsubscribing from traffic updates"); @@ -185,321 +249,136 @@ pub async fn resolve_reset_async() { } } -/// 窗口创建锁守卫 -struct WindowCreateGuard; +/// Create the main window +pub fn create_window(is_show: bool) -> bool { + logging!( + info, + Type::Window, + true, + "开始创建主窗口, is_show={}", + is_show + ); -impl Drop for WindowCreateGuard { - fn drop(&mut self) { - let mut lock = get_window_creating_lock().lock(); - lock.0 = false; - logging!(info, Type::Window, true, "窗口创建过程已完成,释放锁"); - } -} + let creating_lock = get_window_creating_lock(); + let mut creating = creating_lock.lock(); -/// create main window -pub fn create_window(is_showup: bool) { - // 尝试获取窗口创建锁 - let mut creating_lock = get_window_creating_lock().lock(); - let (is_creating, last_create_time) = *creating_lock; - let now = Instant::now(); + let (is_creating, last_time) = *creating; + let elapsed = last_time.elapsed(); - // 检查是否有其他线程正在创建窗口,防止短时间内多次创建窗口导致竞态条件 - if is_creating && now.duration_since(last_create_time) < Duration::from_secs(2) { + if is_creating && elapsed < Duration::from_secs(2) { logging!( - warn, + info, Type::Window, true, - "另一个窗口创建过程正在进行中,跳过本次创建请求" + "窗口创建请求被忽略,因为最近创建过 ({:?}ms)", + elapsed.as_millis() ); - return; + return false; } - *creating_lock = (true, now); - drop(creating_lock); + *creating = (true, Instant::now()); - // 创建窗口锁守卫结束时自动释放锁 - let _guard = WindowCreateGuard; - - // 打印 .window-state.json 文件路径 - 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 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!( - debug, - Type::Window, - true, - "读取窗口状态文件内容成功: {} 字节", - content.len() - ); - - match serde_json::from_str::(&content) { - Ok(window_state) => { - logging!( - info, - Type::Window, - true, - "成功解析窗口状态: width={:?}, height={:?}", - window_state.width, - window_state.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) => { - logging!(error, Type::Window, true, "解析窗口状态文件失败: {:?}", e); - } - } - } - Err(e) => { - logging!(error, Type::Window, true, "读取窗口状态文件失败: {:?}", e); - } - } - } else { - logging!( - info, - Type::Window, - true, - "窗口状态文件不存在,将使用默认设置" - ); - } - } - - if !is_showup { - logging!(info, Type::Window, "Not to display create window"); - return; - } - - logging!(info, Type::Window, true, "Creating window"); - - let app_handle = handle::Handle::global().app_handle().unwrap(); - #[cfg(target_os = "macos")] - AppHandleManager::global().set_activation_policy_regular(); - - // 检查是否从轻量模式恢复 - let from_lightweight = crate::module::lightweight::is_in_lightweight_mode(); - if from_lightweight { - logging!(info, Type::Window, true, "从轻量模式恢复窗口"); - crate::module::lightweight::exit_lightweight_mode(); - } - - if let Some(window) = handle::Handle::global().get_window() { - logging!(info, Type::Window, true, "Found existing window"); - - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); - } - - if from_lightweight { - // 从轻量模式恢复需要销毁旧窗口以重建 - logging!(info, Type::Window, true, "销毁旧窗口以重建新窗口"); - let _ = window.close(); - - // 添加短暂延迟确保窗口正确关闭 - std::thread::sleep(std::time::Duration::from_millis(100)); - } else { - // 普通情况直接显示窗口 - let _ = window.show(); - let _ = window.set_focus(); - return; - } - } - - let width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH); - let height = STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT); - - logging!( - info, - Type::Window, - true, - "Initializing new window with size: {}x{}", - width, - height - ); - - // 根据不同平台创建不同配置的窗口 - #[cfg(target_os = "macos")] - 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); - - // 尝试设置标题栏样式 - // 注意:根据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", + match tauri::WebviewWindowBuilder::new( + &handle::Handle::global().app_handle().unwrap(), + "main", /* the unique window label */ 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) + .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .min_inner_size(520.0, 520.0) .visible(false) - .decorations(false); + .build() + { + Ok(_) => { + logging!(info, Type::Window, true, "主窗口创建成功"); - let window = win_builder.build(); + *creating = (false, Instant::now()); - match window { - Ok(window) => { - logging!(info, Type::Window, true, "Window created successfully"); - - // 静默启动模式等窗口初始化再启动自动进入轻量模式的计时监听器,防止初始化的时候找不到窗口对象导致监听器挂载失败 - lightweight::run_once_auto_lightweight(); - - // 标记前端UI已准备就绪,向前端发送启动完成事件 - let app_handle_clone = app_handle.clone(); - - // 获取窗口创建后的初始大小 - if let Ok(size) = window.inner_size() { - let state_width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH); - let state_height = STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT); - - // 输出所有尺寸信息 - logging!( - info, - Type::Window, - true, - "API报告的窗口尺寸: {}x{}, 状态文件尺寸: {}x{}, 默认尺寸: {}x{}", - size.width, - size.height, - state_width, - state_height, - DEFAULT_WIDTH, - DEFAULT_HEIGHT - ); - - // 优化窗口大小设置 - if size.width < state_width || size.height < state_height { - logging!( - info, - Type::Window, - true, - "强制设置窗口尺寸: {}x{}", - state_width, - state_height - ); - - // 尝试不同的方式设置窗口大小 - 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 - ); - } - } - } - - // 标记此窗口是否从轻量模式恢复 - let was_from_lightweight = from_lightweight; + update_ui_ready_stage(UiReadyStage::NotStarted); AsyncHandler::spawn(move || async move { - // 处理启动完成 handle::Handle::global().mark_startup_completed(); + logging!(info, Type::Window, true, "标记启动完成"); - if let Some(window) = app_handle_clone.get_webview_window("main") { - // 发送启动完成事件 - let _ = window.emit("verge://startup-completed", ()); + if let Some(app_handle) = handle::Handle::global().app_handle() { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.emit("verge://startup-completed", ()); + logging!(info, Type::Window, true, "已发送启动完成事件"); - if is_showup { - let window_clone = window.clone(); + if is_show { + let window_clone = window.clone(); - // 从轻量模式恢复时使用较短的超时,避免卡死 - let timeout_seconds = if was_from_lightweight { - // 从轻量模式恢复只等待2秒,确保不会卡死 - 2 - } else { - 5 - }; + let timeout_seconds = + if crate::module::lightweight::is_in_lightweight_mode() { + 2 + } else { + 5 + }; - // 使用普通的等待方式替代事件监听,简化实现 - 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; + logging!( + info, + Type::Window, + true, + "等待UI就绪,最多{}秒", + timeout_seconds + ); + + let wait_result = + tokio::time::timeout(Duration::from_secs(timeout_seconds), async { + let mut check_count = 0; + while !*get_ui_ready().read() { + check_count += 1; + if check_count % 10 == 0 { + let state = get_ui_ready_state(); + let stage = *state.stage.read(); + logging!( + info, + Type::Window, + true, + "等待UI就绪中... 当前阶段: {:?}, 已等待: {}ms", + stage, + check_count * 100 + ); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await; + + match wait_result { + Ok(_) => { + logging!(info, Type::Window, true, "UI就绪,显示窗口"); } - }) - .await; + Err(_) => { + logging!( + warn, + Type::Window, + true, + "等待UI就绪超时({}秒),强制显示窗口", + timeout_seconds + ); - // 根据结果处理 - match wait_result { - Ok(_) => { - logging!(info, Type::Window, true, "UI就绪,显示窗口"); - } - Err(_) => { - logging!( - warn, - Type::Window, - true, - "等待UI就绪超时({}秒),强制显示窗口", - timeout_seconds - ); - // 强制设置UI就绪状态 - *get_ui_ready().write() = true; + *get_ui_ready().write() = true; + } } + + let _ = window_clone.show(); + let _ = window_clone.set_focus(); + + logging!(info, Type::Window, true, "窗口创建和显示流程已完成"); } - - // 显示窗口 - let _ = window_clone.show(); - let _ = window_clone.set_focus(); - - logging!(info, Type::Window, true, "窗口创建和显示流程已完成"); } } }); + true } Err(e) => { logging!(error, Type::Window, true, "Failed to create window: {}", e); + false } } } @@ -529,7 +408,6 @@ pub async fn resolve_scheme(param: String) -> Result<()> { .find(|(key, _)| key == "name") .map(|(_, value)| value.into_owned()); - // 通过直接获取查询部分并解析特定参数来避免 URL 转义问题 let url_param = if let Some(query) = link_parsed.query() { let prefix = "url="; if let Some(pos) = query.find(prefix) { diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index ee8aa321c..44f73e0fe 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -165,7 +165,6 @@ const Layout = () => { useEffect(() => { if (clashInfo) { const { server = "", secret = "" } = clashInfo; - // 使用本地存储中的enableLog值初始化全局日志服务 initGlobalLogService(server, secret, enableLog, "info"); } }, [clashInfo, enableLog]); @@ -173,7 +172,6 @@ const Layout = () => { // 设置监听器 useEffect(() => { const listeners = [ - // 配置更新监听 addListener("verge://refresh-clash-config", async () => { await getAxios(true); mutate("getProxies"); @@ -182,21 +180,17 @@ const Layout = () => { mutate("getProxyProviders"); }), - // verge 配置更新监听 addListener("verge://refresh-verge-config", () => { mutate("getVergeConfig"); - // 添加对系统代理状态的刷新 mutate("getSystemProxy"); mutate("getAutotemProxy"); }), - // 通知消息监听 addListener("verge://notice-message", ({ payload }) => handleNotice(payload as [string, string]), ), ]; - // 设置窗口显示/隐藏监听 const setupWindowListeners = async () => { const [hideUnlisten, showUnlisten] = await Promise.all([ listen("verge://hide-window", () => appWindow.hide()), @@ -209,29 +203,51 @@ const Layout = () => { }; }; - // 初始化 setupCloseListener(); const cleanupWindow = setupWindowListeners(); - // 清理函数 return () => { - // 清理主要监听器 listeners.forEach((listener) => { if (typeof listener.then === "function") { listener.then((unlisten) => unlisten()); } }); - // 清理窗口监听器 cleanupWindow.then((cleanup) => cleanup()); }; }, [handleNotice]); - // 监听启动完成事件并通知UI已加载 useEffect(() => { + const notifyUiStage = async (stage: string) => { + try { + console.log(`UI加载阶段: ${stage}`); + await invoke("update_ui_stage", { stage }); + } catch (err) { + console.error(`通知UI加载阶段(${stage})失败:`, err); + } + }; + + const notifyUiCoreReady = async () => { + try { + console.log("核心组件已加载,通知后端"); + await invoke("update_ui_stage", { stage: "DomReady" }); + } catch (err) { + console.error("通知核心组件加载完成失败:", err); + } + }; + + const notifyUiResourcesLoaded = async () => { + try { + console.log("所有资源已加载,通知后端"); + await invoke("update_ui_stage", { stage: "ResourcesLoaded" }); + } catch (err) { + console.error("通知资源加载完成失败:", err); + } + }; + const notifyUiReady = async () => { try { + console.log("UI完全准备就绪,通知后端"); await invoke("notify_ui_ready"); - console.log("已通知后端UI准备就绪"); } catch (err) { console.error("通知UI准备就绪失败:", err); } @@ -240,6 +256,7 @@ const Layout = () => { // 监听后端发送的启动完成事件 const listenStartupCompleted = async () => { try { + console.log("开始监听启动完成事件"); const unlisten = await listen("verge://startup-completed", () => { console.log("收到启动完成事件,开始通知UI就绪"); notifyUiReady(); @@ -251,9 +268,21 @@ const Layout = () => { } }; - // 初始加载时也通知一次 - console.log("页面初始加载,通知UI就绪"); - notifyUiReady(); + // 初始阶段 - 开始加载 + notifyUiStage("Loading"); + + setTimeout(() => { + notifyUiCoreReady(); + + setTimeout(() => { + notifyUiResourcesLoaded(); + setTimeout(() => { + notifyUiReady(); + }, 100); + }, 100); + }, 100); + + // 启动监听器 const unlistenPromise = listenStartupCompleted(); return () => {