From c6a6ea48dd23297deb8439f9e414059755330c65 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Fri, 29 Aug 2025 23:51:09 +0800 Subject: [PATCH] refactor: enhance async initialization and streamline setup process (#4560) * feat: Implement DNS management for macOS - Added `set_public_dns` and `restore_public_dns` functions in `dns.rs` to manage system DNS settings. - Introduced `resolve` module to encapsulate DNS and scheme resolution functionalities. - Implemented `resolve_scheme` function in `scheme.rs` to handle deep links and profile imports. - Created UI readiness management in `ui.rs` to track and update UI loading states. - Developed window management logic in `window.rs` to handle window creation and visibility. - Added initial loading overlay script in `window_script.rs` for better user experience during startup. - Updated server handling in `server.rs` to integrate new resolve functionalities. - Refactored window creation calls in `window_manager.rs` to use the new window management logic. * refactor: streamline asynchronous handling in config and resolve setup * Revert "refactor: streamline asynchronous handling in config and resolve setup" This reverts commit 23d7dc86d5b87a3a34df2ae69c2caacef803ef81. * fix: optimize asynchronous memory handling * fix: enhance task logging by adding size check for special cases * refactor: enhance async initialization and streamline setup process * refactor: optimize async setup by consolidating initialization tasks * chore: update changelog for Mihomo(Meta) kernel upgrade to v1.19.13 * fix: improve startup phase initialization performance * refactor: optimize file read/write performance to reduce application wait time * refactor: simplify app instance exit logic and adjust system proxy guard initialization * refactor: change resolve_setup_async to synchronous execution for improved performance * refactor: update resolve_setup_async to accept AppHandle for improved initialization flow * refactor: remove unnecessary initialization of portable flag in run function * refactor: consolidate async initialization tasks into a single blocking call for improved execution flow * refactor: optimize resolve_setup_async by restructuring async tasks for improved concurrency * refactor: streamline resolve_setup_async and embed_server for improved async handling * refactor: separate synchronous and asynchronous setup functions for improved clarity * refactor: simplify async notification handling and remove redundant network manager initialization * refactor: enhance async handling in proxy request cache and window creation logic * refactor: improve code formatting and readability in ProxyRequestCache * refactor: adjust singleton check timeout and optimize trace size conditions * refactor: update TRACE_SPECIAL_SIZE to include additional size condition * refactor: update kode-bridge dependency to version 0.2.1-rc2 * refactor: replace RwLock with AtomicBool for UI readiness and implement event-driven monitoring * refactor: convert async functions to synchronous for window management * Update src-tauri/src/utils/resolve/window.rs * fix: handle missing app_handle in create_window function * Update src-tauri/src/module/lightweight.rs --- UPDATELOG.md | 7 +- src-tauri/Cargo.lock | 4 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/cmd/app.rs | 8 +- src-tauri/src/config/config.rs | 7 +- src-tauri/src/core/timer.rs | 2 - src-tauri/src/core/tray/mod.rs | 24 +- src-tauri/src/enhance/tun.rs | 6 +- src-tauri/src/feat/window.rs | 2 +- src-tauri/src/lib.rs | 79 +- src-tauri/src/module/lightweight.rs | 9 +- src-tauri/src/process/async_handler.rs | 4 +- src-tauri/src/state/proxy.rs | 76 +- src-tauri/src/utils/resolve.rs | 737 ------------------- src-tauri/src/utils/resolve/dns.rs | 94 +++ src-tauri/src/utils/resolve/mod.rs | 213 ++++++ src-tauri/src/utils/resolve/scheme.rs | 75 ++ src-tauri/src/utils/resolve/ui.rs | 106 +++ src-tauri/src/utils/resolve/window.rs | 298 ++++++++ src-tauri/src/utils/resolve/window_script.rs | 71 ++ src-tauri/src/utils/server.rs | 9 +- src-tauri/src/utils/window_manager.rs | 2 +- 22 files changed, 956 insertions(+), 879 deletions(-) delete mode 100644 src-tauri/src/utils/resolve.rs create mode 100644 src-tauri/src/utils/resolve/dns.rs create mode 100644 src-tauri/src/utils/resolve/mod.rs create mode 100644 src-tauri/src/utils/resolve/scheme.rs create mode 100644 src-tauri/src/utils/resolve/ui.rs create mode 100644 src-tauri/src/utils/resolve/window.rs create mode 100644 src-tauri/src/utils/resolve/window_script.rs diff --git a/UPDATELOG.md b/UPDATELOG.md index 9ada17c8c..1428edfad 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -3,7 +3,10 @@ ### 🏆 重大改进 - **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 -- **文件操作性能提升**:优化文件读写性能,减少应用等待时间 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.13** ### 🚀 性能优化 @@ -11,6 +14,8 @@ - 改进服务管理响应性,减少系统服务操作等待时间 - 提升文件和配置处理性能 - 优化任务管理和日志记录效率 +- 优化异步内存管理,减少内存占用并提升多任务处理效率 +- 优化启动阶段初始化性能 ### 🐞 修复问题 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 574ab039f..eac3d165f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3733,9 +3733,9 @@ dependencies = [ [[package]] name = "kode-bridge" -version = "0.2.1-rc1" +version = "0.2.1-rc2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd493b32ae4bae43f8ba30d6d4bfe28675c3468e6676bb5823c0a8b7ab6f4cc" +checksum = "1fb8ef81e3057620f1ccc1b65fb5e26f24b29757dc81b67410d603c7e60b0839" dependencies = [ "bytes", "futures", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5a2365b31..a69d26f5b 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -73,7 +73,7 @@ hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" scopeguard = "1.2.0" -kode-bridge = "0.2.1-rc1" +kode-bridge = "0.2.1-rc2" dashmap = "6.1.0" tauri-plugin-notification = "2.3.1" console-subscriber = { version = "0.4.1", optional = true } diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 37e880ddc..44b2cecf1 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -209,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult { #[tauri::command] pub fn notify_ui_ready() -> CmdResult<()> { log::info!(target: "app", "前端UI已准备就绪"); - crate::utils::resolve::mark_ui_ready(); + crate::utils::resolve::ui::mark_ui_ready(); Ok(()) } @@ -218,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> { pub fn update_ui_stage(stage: String) -> CmdResult<()> { log::info!(target: "app", "UI加载阶段更新: {stage}"); - use crate::utils::resolve::UiReadyStage; + use crate::utils::resolve::ui::UiReadyStage; let stage_enum = match stage.as_str() { "NotStarted" => UiReadyStage::NotStarted, @@ -232,7 +232,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> { } }; - crate::utils::resolve::update_ui_ready_stage(stage_enum); + crate::utils::resolve::ui::update_ui_ready_stage(stage_enum); Ok(()) } @@ -240,6 +240,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> { #[tauri::command] pub fn reset_ui_ready_state() -> CmdResult<()> { log::info!(target: "app", "重置UI就绪状态"); - crate::utils::resolve::reset_ui_ready(); + crate::utils::resolve::ui::reset_ui_ready(); Ok(()) } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index 22e63fc47..947fe52c7 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -3,7 +3,6 @@ use crate::{ config::{profiles_append_item_safe, PrfItem}, core::{handle, CoreManager}, enhance, logging, - process::AsyncHandler, utils::{dirs, help, logging::Type}, }; use anyhow::{anyhow, Result}; @@ -123,10 +122,8 @@ impl Config { // 在单独的任务中发送通知 if let Some((msg_type, msg_content)) = validation_result { - AsyncHandler::spawn(move || async move { - sleep(Duration::from_secs(2)).await; - handle::Handle::notice_message(msg_type, &msg_content); - }); + sleep(Duration::from_secs(2)).await; + handle::Handle::notice_message(msg_type, &msg_content); } Ok(()) diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 503e3a505..8d76705ee 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -60,8 +60,6 @@ impl Timer { return Ok(()); } - logging!(info, Type::Timer, true, "Initializing timer..."); - // Initialize timer tasks if let Err(e) = self.refresh().await { // Reset initialization flag on error diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 6ed15184b..a7f308ea0 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -10,7 +10,7 @@ use crate::{ feat, logging, module::lightweight::is_in_lightweight_mode, singleton_lazy, - utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, + utils::{dirs::find_target_icons, i18n::t}, Type, }; @@ -186,7 +186,11 @@ impl Default for Tray { singleton_lazy!(Tray, TRAY, Tray::default); impl Tray { - pub fn init(&self) -> Result<()> { + pub async fn init(&self) -> Result<()> { + let app_handle = handle::Handle::global() + .app_handle() + .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?; + self.create_tray_from_handle(&app_handle).await?; Ok(()) } @@ -396,14 +400,6 @@ impl Tray { } }; - let version = match VERSION.get() { - Some(v) => v, - None => { - log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在"); - return Ok(()); - } - }; - let verge = Config::verge().await.latest_ref().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); @@ -434,6 +430,7 @@ impl Tray { let tun_text = t("TUN").await; let profile_text = t("Profile").await; + let version = env!("CARGO_PKG_VERSION"); if let Some(tray) = app_handle.tray_by_id("main") { let _ = tray.set_tooltip(Some(&format!( "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", @@ -460,10 +457,6 @@ impl Tray { Ok(()) } - /// 取消订阅 traffic 数据 - #[cfg(target_os = "macos")] - pub fn unsubscribe_traffic(&self) {} - pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { log::info!(target: "app", "正在从AppHandle创建系统托盘"); @@ -567,8 +560,7 @@ async fn create_tray_menu( ) -> Result> { let mode = mode.unwrap_or(""); - let unknown_version = String::from("unknown"); - let version = VERSION.get().unwrap_or(&unknown_version); + let version = env!("CARGO_PKG_VERSION"); let hotkeys = Config::verge() .await diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index c8fec783b..92f7a5eb0 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -63,8 +63,8 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { #[cfg(target_os = "macos")] { AsyncHandler::spawn(move || async move { - crate::utils::resolve::restore_public_dns().await; - crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await; + crate::utils::resolve::dns::restore_public_dns().await; + crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await; }); } } @@ -75,7 +75,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { // TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置 #[cfg(target_os = "macos")] AsyncHandler::spawn(move || async move { - crate::utils::resolve::restore_public_dns().await; + crate::utils::resolve::dns::restore_public_dns().await; }); } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 370bc2a0d..b80b1b8aa 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -166,7 +166,7 @@ async fn clean_async() -> bool { let dns_task = async { match timeout( Duration::from_millis(1000), - crate::utils::resolve::restore_public_dns(), + crate::utils::resolve::dns::restore_public_dns(), ) .await { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f867d7f11..c0c0ff275 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,6 @@ +#![allow(non_snake_case)] +#![recursion_limit = "512"] + mod cmd; pub mod config; mod core; @@ -11,9 +14,13 @@ mod utils; #[cfg(target_os = "macos")] use crate::utils::window_manager::WindowManager; use crate::{ + core::handle, core::hotkey, process::AsyncHandler, - utils::{resolve, resolve::resolve_scheme, server}, + utils::{ + resolve::{self, scheme::resolve_scheme}, + server, + }, }; use config::Config; use parking_lot::Mutex; @@ -28,15 +35,13 @@ use utils::logging::Type; /// Application initialization helper functions mod app_init { - use crate::core::handle; - use super::*; /// Initialize singleton monitoring for other instances pub fn init_singleton_check() { AsyncHandler::spawn(move || async move { logging!(info, Type::Setup, true, "开始检查单例实例..."); - match timeout(Duration::from_secs(3), server::check_singleton()).await { + match timeout(Duration::from_millis(500), server::check_singleton()).await { Ok(result) => { if result.is_err() { logging!(info, Type::Setup, true, "检测到已有应用实例运行"); @@ -133,47 +138,6 @@ mod app_init { Ok(()) } - /// Initialize core components asynchronously - pub fn init_core_async(app_handle: &AppHandle) { - let app_handle = 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, "应用设置成功完成"); - } - Err(_) => { - logging!( - error, - Type::Setup, - true, - "应用设置超时(30秒),继续执行后续流程" - ); - } - } - }); - } - - /// Initialize core components synchronously - pub async fn init_core_sync(app_handle: &AppHandle) -> Result<(), Box> { - logging!(info, Type::Setup, true, "初始化核心句柄..."); - core::handle::Handle::global().init(app_handle.clone()); - - logging!(info, Type::Setup, true, "初始化配置..."); - utils::init::init_config().await?; - - logging!(info, Type::Setup, true, "初始化资源..."); - utils::init::init_resources().await?; - - logging!(info, Type::Setup, true, "核心组件初始化完成"); - Ok(()) - } - /// Generate all command handlers for the application pub fn generate_handlers( ) -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { @@ -308,11 +272,8 @@ pub fn run() { // Setup singleton check app_init::init_singleton_check(); - // Initialize network manager - utils::network::NetworkManager::new().init(); - - // Initialize portable flag - let _ = utils::dirs::init_portable_flag(); + // We don't need init_portable_flag here anymore due to the init_config will do the things + // let _ = utils::dirs::init_portable_flag(); // Set Linux environment variable #[cfg(target_os = "linux")] @@ -371,23 +332,11 @@ pub fn run() { let app_handle = app.handle().clone(); - // Initialize core components asynchronously - app_init::init_core_async(&app_handle); - logging!(info, Type::Setup, true, "执行主要设置操作..."); - // Initialize core components synchronously - AsyncHandler::spawn(move || async move { - if let Err(e) = app_init::init_core_sync(&app_handle).await { - logging!( - error, - Type::Setup, - true, - "Failed to initialize core components: {}", - e - ); - } - }); + logging!(info, Type::Setup, true, "异步执行应用设置..."); + resolve::resolve_setup_sync(app_handle); + resolve::resolve_setup_async(); logging!(info, Type::Setup, true, "初始化完成,继续执行"); Ok(()) diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 48821e8ae..b2424ffeb 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -78,7 +78,7 @@ pub async fn run_once_auto_lightweight() { } } -pub async fn auto_lightweight_mode_init() { +pub async fn auto_lightweight_mode_init() -> Result<()> { if let Some(app_handle) = handle::Handle::global().app_handle() { // Check if state is available before accessing it if app_handle.try_state::>().is_none() { @@ -88,7 +88,7 @@ pub async fn auto_lightweight_mode_init() { true, "LightWeightState 尚未初始化,跳过自动轻量模式初始化" ); - return; + return Err(anyhow::anyhow!("LightWeightState has not been initialized")); } let is_silent_start = @@ -114,9 +114,12 @@ pub async fn auto_lightweight_mode_init() { // 确保托盘状态更新 if let Err(e) = Tray::global().update_part().await { log::warn!("Failed to update tray: {e}"); + return Err(e); } } } + + Ok(()) } // 检查是否处于轻量模式 @@ -214,7 +217,7 @@ pub async fn exit_lightweight_mode() { handle::Handle::global().set_activation_policy_regular(); // 重置UI就绪状态 - crate::utils::resolve::reset_ui_ready(); + crate::utils::resolve::ui::reset_ui_ready(); // 更新托盘显示 let _tray = crate::core::tray::Tray::global(); diff --git a/src-tauri/src/process/async_handler.rs b/src-tauri/src/process/async_handler.rs index a06e7067c..f5c624278 100644 --- a/src-tauri/src/process/async_handler.rs +++ b/src-tauri/src/process/async_handler.rs @@ -50,9 +50,9 @@ impl AsyncHandler { where F: ?Sized, { - const TRACE_MINI_SIZE: usize = 4; + const TRACE_SPECIAL_SIZE: [usize; 3] = [0, 4, 24]; let size = std::mem::size_of_val(f); - if size <= TRACE_MINI_SIZE { + if TRACE_SPECIAL_SIZE.contains(&size) { return; } diff --git a/src-tauri/src/state/proxy.rs b/src-tauri/src/state/proxy.rs index 9d8941978..b7c930589 100644 --- a/src-tauri/src/state/proxy.rs +++ b/src-tauri/src/state/proxy.rs @@ -27,46 +27,56 @@ impl ProxyRequestCache { pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc where - F: Fn() -> Fut, - Fut: std::future::Future, + F: Fn() -> Fut + Send + 'static, + Fut: std::future::Future + Send + 'static, { - let now = Instant::now(); - let key_cloned = key.clone(); - let cell = self - .map - .entry(key) - .or_insert_with(|| Arc::new(OnceCell::new())) - .clone(); + loop { + let now = Instant::now(); + let key_cloned = key.clone(); - if let Some(entry) = cell.get() { - if entry.expires_at > now { - return Arc::clone(&entry.value); - } - } + // Get or create the cell + let cell = self + .map + .entry(key_cloned.clone()) + .or_insert_with(|| Arc::new(OnceCell::new())) + .clone(); - if let Some(entry) = cell.get() { - if entry.expires_at <= now { + // Check if we have a valid cached entry + if let Some(entry) = cell.get() { + if entry.expires_at > now { + return Arc::clone(&entry.value); + } + // Entry is expired, remove it self.map .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); - let new_cell = Arc::new(OnceCell::new()); - self.map.insert(key_cloned.clone(), Arc::clone(&new_cell)); - return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await; + continue; // Retry with fresh cell + } + + // Try to set a new value + let value = fetch_fn().await; + let entry = CacheEntry { + value: Arc::new(value), + expires_at: Instant::now() + ttl, + }; + + match cell.set(entry) { + Ok(_) => { + // Successfully set the value, it must exist now + if let Some(set_entry) = cell.get() { + return Arc::clone(&set_entry.value); + } + } + Err(_) => { + if let Some(existing_entry) = cell.get() { + if existing_entry.expires_at > Instant::now() { + return Arc::clone(&existing_entry.value); + } + self.map + .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); + } + } } } - - let value = fetch_fn().await; - let entry = CacheEntry { - value: Arc::new(value), - expires_at: Instant::now() + ttl, - }; - let _ = cell.set(entry); - // Safe to unwrap here as we just set the value - Arc::clone( - &cell - .get() - .unwrap_or_else(|| unreachable!("Cell value should exist after set")) - .value, - ) } } diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs deleted file mode 100644 index e2c85a3ed..000000000 --- a/src-tauri/src/utils/resolve.rs +++ /dev/null @@ -1,737 +0,0 @@ -use crate::{ - config::{Config, PrfItem}, - core::*, - logging, logging_error, - module::lightweight::{self, auto_lightweight_mode_init}, - process::AsyncHandler, - utils::{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 scopeguard; -use std::time::{Duration, Instant}; -use tauri::{AppHandle, Manager}; - -use tauri::Url; -//#[cfg(not(target_os = "linux"))] -// use window_shadows::set_shadow; - -pub static VERSION: OnceCell = OnceCell::new(); - -// 定义默认窗口尺寸常量 -const DEFAULT_WIDTH: u32 = 940; -const DEFAULT_HEIGHT: u32 = 700; - -// 添加全局UI准备就绪标志 -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, -} - -impl Default for UiReadyState { - fn default() -> Self { - Self { - stage: RwLock::new(UiReadyStage::NotStarted), - } - } -} - -// 获取UI就绪状态细节 -static UI_READY_STATE: OnceCell = OnceCell::new(); - -fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> { - WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) -} - -fn get_ui_ready() -> &'static RwLock { - UI_READY.get_or_init(|| RwLock::new(false)) -} - -fn get_ui_ready_state() -> &'static UiReadyState { - UI_READY_STATE.get_or_init(UiReadyState::default) -} - -// 更新UI准备阶段 -pub fn update_ui_ready_stage(stage: UiReadyStage) { - let state = get_ui_ready_state(); - let mut stage_lock = state.stage.write(); - - *stage_lock = stage; - // 如果是最终阶段,标记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 state = get_ui_ready_state(); - let mut stage = state.stage.write(); - *stage = UiReadyStage::NotStarted; - } - logging!(info, Type::Window, true, "UI就绪状态已重置"); -} - -/// 异步方式处理启动后的额外任务 -pub async fn resolve_setup_async(app_handle: &AppHandle) { - let start_time = std::time::Instant::now(); - logging!( - info, - Type::Setup, - true, - "开始执行异步设置任务... 线程ID: {:?}", - std::thread::current().id() - ); - - 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() - }); - } - - logging_error!(Type::Setup, true, init::init_scheme()); - - logging_error!(Type::Setup, true, init::startup_script().await); - - logging!( - info, - Type::Config, - true, - "开始初始化配置... 线程ID: {:?}", - std::thread::current().id() - ); - logging_error!(Type::Config, true, Config::init_config().await); - logging!(info, Type::Config, true, "配置初始化完成"); - - logging!(trace, Type::Core, true, "启动核心管理器..."); - logging_error!(Type::Core, true, CoreManager::global().init().await); - - log::trace!(target: "app", "启动内嵌服务器..."); - server::embed_server(); - - logging!(trace, Type::Core, true, "启动 IPC 监控服务..."); - - logging_error!(Type::Tray, true, tray::Tray::global().init()); - - 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) - .await; - 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, - sysopt::Sysopt::global().update_sysproxy().await - ); - logging_error!( - Type::System, - true, - sysopt::Sysopt::global().init_guard_sysproxy() - ); - - // 创建窗口 - let is_silent_start = - { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); - #[cfg(target_os = "macos")] - { - if is_silent_start { - handle::Handle::global().set_activation_policy_accessory(); - } - } - create_window(!is_silent_start).await; - - // 初始化定时器 - logging_error!(Type::System, true, timer::Timer::global().init().await); - - // 自动进入轻量模式 - auto_lightweight_mode_init().await; - - logging_error!(Type::Tray, true, tray::Tray::global().update_part().await); - - logging!(trace, Type::System, true, "初始化热键..."); - logging_error!(Type::System, true, hotkey::Hotkey::global().init().await); - - let elapsed = start_time.elapsed(); - logging!( - info, - Type::Setup, - true, - "异步设置任务完成,耗时: {:?}", - elapsed - ); - - // 如果初始化时间过长,记录警告 - if elapsed.as_secs() > 10 { - logging!( - warn, - Type::Setup, - true, - "异步设置任务耗时较长({:?})", - elapsed - ); - } -} - -/// reset system proxy (异步) -pub async fn resolve_reset_async() { - #[cfg(target_os = "macos")] - logging!(info, Type::Tray, true, "Unsubscribing from traffic updates"); - #[cfg(target_os = "macos")] - tray::Tray::global().unsubscribe_traffic(); - - logging_error!( - Type::System, - true, - sysopt::Sysopt::global().reset_sysproxy().await - ); - logging_error!(Type::Core, true, CoreManager::global().stop_core().await); - #[cfg(target_os = "macos")] - { - logging!(info, Type::System, true, "Restoring system DNS settings"); - restore_public_dns().await; - } -} - -/// Create the main window -pub async fn create_window(is_show: bool) -> bool { - logging!( - info, - Type::Window, - true, - "开始创建/显示主窗口, is_show={}", - is_show - ); - - if !is_show { - logging!(info, Type::Window, true, "静默模式启动时不创建窗口"); - lightweight::set_lightweight_mode(true).await; - handle::Handle::notify_startup_completed(); - return false; - } - - if let Some(app_handle) = handle::Handle::global().app_handle() { - if let Some(window) = app_handle.get_webview_window("main") { - logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口"); - if is_show { - if window.is_minimized().unwrap_or(false) { - logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化"); - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - - #[cfg(target_os = "macos")] - handle::Handle::global().set_activation_policy_regular(); - } - return true; - } - } - - let creating_lock = get_window_creating_lock(); - let mut creating = creating_lock.lock(); - - let (is_creating, last_time) = *creating; - let elapsed = last_time.elapsed(); - - if is_creating && elapsed < Duration::from_secs(2) { - logging!( - info, - Type::Window, - true, - "窗口创建请求被忽略,因为最近创建过 ({:?}ms)", - elapsed.as_millis() - ); - return false; - } - - *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] 窗口创建状态已重置"); - }); - - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - logging!( - error, - Type::Window, - true, - "无法获取app_handle,窗口创建失败" - ); - return false; - } - }; - - match tauri::WebviewWindowBuilder::new( - &app_handle, - "main", /* the unique window label */ - tauri::WebviewUrl::App("index.html".into()), - ) - .title("Clash Verge") - .center() - .decorations(true) - .fullscreen(false) - .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) - .min_inner_size(520.0, 520.0) - .visible(true) // 立即显示窗口,避免用户等待 - .initialization_script( - r#" - console.log('[Tauri] 窗口初始化脚本开始执行'); - - function createLoadingOverlay() { - - if (document.getElementById('initial-loading-overlay')) { - console.log('[Tauri] 加载指示器已存在'); - return; - } - - console.log('[Tauri] 创建加载指示器'); - const loadingDiv = document.createElement('div'); - loadingDiv.id = 'initial-loading-overlay'; - loadingDiv.innerHTML = ` -
-
-
-
-
Loading Clash Verge...
-
- - `; - - if (document.body) { - document.body.appendChild(loadingDiv); - } else { - document.addEventListener('DOMContentLoaded', () => { - if (document.body && !document.getElementById('initial-loading-overlay')) { - document.body.appendChild(loadingDiv); - } - }); - } - } - - createLoadingOverlay(); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createLoadingOverlay); - } else { - createLoadingOverlay(); - } - - console.log('[Tauri] 窗口初始化脚本执行完成'); - "#, - ) - .build() - { - Ok(newly_created_window) => { - logging!(debug, Type::Window, true, "主窗口实例创建成功"); - - update_ui_ready_stage(UiReadyStage::NotStarted); - - AsyncHandler::spawn(move || async move { - handle::Handle::global().mark_startup_completed(); - logging!( - debug, - Type::Window, - true, - "异步窗口任务开始 (启动已标记完成)" - ); - - // 先运行轻量模式检测 - lightweight::run_once_auto_lightweight().await; - - // 发送启动完成事件,触发前端开始加载 - logging!( - debug, - Type::Window, - true, - "发送 verge://startup-completed 事件" - ); - handle::Handle::notify_startup_completed(); - - if is_show { - let window_clone = newly_created_window.clone(); - - // 立即显示窗口 - let _ = window_clone.show(); - let _ = window_clone.set_focus(); - logging!(info, Type::Window, true, "窗口已立即显示"); - #[cfg(target_os = "macos")] - handle::Handle::global().set_activation_policy_regular(); - - let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() { - 3 - } else { - 8 - }; - - logging!( - info, - Type::Window, - true, - "开始监控UI加载状态 (最多{}秒)...", - timeout_seconds - ); - - // 异步监控UI状态,使用try_read避免死锁 - AsyncHandler::spawn(move || async move { - logging!( - debug, - Type::Window, - true, - "启动UI状态监控线程,超时{}秒", - timeout_seconds - ); - - let ui_ready_checker = || async { - let (mut check_count, mut consecutive_failures) = (0, 0); - - loop { - let is_ready = get_ui_ready() - .try_read() - .map(|guard| *guard) - .unwrap_or_else(|| { - consecutive_failures += 1; - if consecutive_failures > 50 { - logging!( - warn, - Type::Window, - true, - "UI状态监控连续{}次无法获取读锁,可能存在死锁", - consecutive_failures - ); - consecutive_failures = 0; - } - false - }); - - if is_ready { - logging!( - debug, - Type::Window, - true, - "UI状态监控检测到就绪信号,退出监控" - ); - return; - } - - consecutive_failures = 0; - tokio::time::sleep(Duration::from_millis(100)).await; - check_count += 1; - - if check_count % 20 == 0 { - logging!( - debug, - Type::Window, - true, - "UI加载状态检查... ({}秒)", - check_count / 10 - ); - } - } - }; - - let wait_result = tokio::time::timeout( - Duration::from_secs(timeout_seconds), - ui_ready_checker(), - ) - .await; - - match wait_result { - Ok(_) => { - logging!(info, Type::Window, true, "UI已完全加载就绪"); - handle::Handle::global() - .get_window() - .map(|window| window.eval(r" - const overlay = document.getElementById('initial-loading-overlay'); - if (overlay) { - overlay.style.opacity = '0'; - setTimeout(() => overlay.remove(), 300); - } - ")); - } - Err(_) => { - logging!( - warn, - Type::Window, - true, - "UI加载监控超时({}秒),但窗口已正常显示", - timeout_seconds - ); - - get_ui_ready() - .try_write() - .map(|mut guard| { - *guard = true; - logging!( - info, - Type::Window, - true, - "超时后成功设置UI就绪状态" - ); - }) - .unwrap_or_else(|| { - logging!( - error, - Type::Window, - true, - "超时后无法获取UI状态写锁,可能存在严重死锁" - ); - }); - } - } - }); - - logging!(info, Type::Window, true, "窗口显示流程完成"); - } else { - logging!( - debug, - Type::Window, - true, - "is_show为false,窗口保持隐藏状态" - ); - } - }); - true - } - Err(e) => { - logging!(error, Type::Window, true, "主窗口构建失败: {}", e); - false - } - } -} - -pub async fn resolve_scheme(param: String) -> Result<()> { - log::info!(target:"app", "received deep link: {param}"); - - let param_str = if param.starts_with("[") && param.len() > 4 { - param - .get(2..param.len() - 2) - .ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))? - } else { - param.as_str() - }; - - // 解析 URL - let link_parsed = match Url::parse(param_str) { - Ok(url) => url, - Err(e) => { - bail!("failed to parse deep link: {:?}, param: {:?}", e, param); - } - }; - - if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { - let name = link_parsed - .query_pairs() - .find(|(key, _)| key == "name") - .map(|(_, value)| value.into_owned()); - - let url_param = if let Some(query) = link_parsed.query() { - let prefix = "url="; - if let Some(pos) = query.find(prefix) { - let raw_url = &query[pos + prefix.len()..]; - Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) - } else { - None - } - } else { - None - }; - - match url_param { - Some(url) => { - log::info!(target:"app", "decoded subscription url: {url}"); - - create_window(false).await; - match PrfItem::from_url(url.as_ref(), name, None, None).await { - Ok(item) => { - let uid = match item.uid.clone() { - Some(uid) => uid, - None => { - logging!(error, Type::Config, true, "Profile item missing UID"); - handle::Handle::notice_message( - "import_sub_url::error", - "Profile item missing UID".to_string(), - ); - return Ok(()); - } - }; - let result = crate::config::profiles::profiles_append_item_safe(item).await; - let _ = wrap_err!(result); - handle::Handle::notice_message("import_sub_url::ok", uid); - } - Err(e) => { - handle::Handle::notice_message("import_sub_url::error", e.to_string()); - } - } - } - None => bail!("failed to get profile url"), - } - } - - Ok(()) -} - -#[cfg(target_os = "macos")] -pub async fn set_public_dns(dns_server: String) { - use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::error!(target: "app", "app_handle not available for DNS configuration"); - return; - } - }; - - log::info!(target: "app", "try to set system dns"); - let resource_dir = match dirs::app_resources_dir() { - Ok(dir) => dir, - Err(e) => { - log::error!(target: "app", "Failed to get resource directory: {}", e); - return; - } - }; - let script = resource_dir.join("set_dns.sh"); - if !script.exists() { - log::error!(target: "app", "set_dns.sh not found"); - return; - } - let script = script.to_string_lossy().into_owned(); - match app_handle - .shell() - .command("bash") - .args([script, dns_server]) - .current_dir(resource_dir) - .status() - .await - { - Ok(status) => { - if status.success() { - log::info!(target: "app", "set system dns successfully"); - } else { - let code = status.code().unwrap_or(-1); - log::error!(target: "app", "set system dns failed: {code}"); - } - } - Err(err) => { - log::error!(target: "app", "set system dns failed: {err}"); - } - } -} - -#[cfg(target_os = "macos")] -pub async fn restore_public_dns() { - use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::error!(target: "app", "app_handle not available for DNS restoration"); - return; - } - }; - log::info!(target: "app", "try to unset system dns"); - let resource_dir = match dirs::app_resources_dir() { - Ok(dir) => dir, - Err(e) => { - log::error!(target: "app", "Failed to get resource directory: {}", e); - return; - } - }; - let script = resource_dir.join("unset_dns.sh"); - if !script.exists() { - log::error!(target: "app", "unset_dns.sh not found"); - return; - } - let script = script.to_string_lossy().into_owned(); - match app_handle - .shell() - .command("bash") - .args([script]) - .current_dir(resource_dir) - .status() - .await - { - Ok(status) => { - if status.success() { - log::info!(target: "app", "unset system dns successfully"); - } else { - let code = status.code().unwrap_or(-1); - log::error!(target: "app", "unset system dns failed: {code}"); - } - } - Err(err) => { - log::error!(target: "app", "unset system dns failed: {err}"); - } - } -} diff --git a/src-tauri/src/utils/resolve/dns.rs b/src-tauri/src/utils/resolve/dns.rs new file mode 100644 index 000000000..e3618178f --- /dev/null +++ b/src-tauri/src/utils/resolve/dns.rs @@ -0,0 +1,94 @@ +#[cfg(target_os = "macos")] +pub async fn set_public_dns(dns_server: String) { + use crate::{core::handle, utils::dirs}; + use tauri_plugin_shell::ShellExt; + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::error!(target: "app", "app_handle not available for DNS configuration"); + return; + } + }; + + log::info!(target: "app", "try to set system dns"); + let resource_dir = match dirs::app_resources_dir() { + Ok(dir) => dir, + Err(e) => { + log::error!(target: "app", "Failed to get resource directory: {}", e); + return; + } + }; + let script = resource_dir.join("set_dns.sh"); + if !script.exists() { + log::error!(target: "app", "set_dns.sh not found"); + return; + } + let script = script.to_string_lossy().into_owned(); + match app_handle + .shell() + .command("bash") + .args([script, dns_server]) + .current_dir(resource_dir) + .status() + .await + { + Ok(status) => { + if status.success() { + log::info!(target: "app", "set system dns successfully"); + } else { + let code = status.code().unwrap_or(-1); + log::error!(target: "app", "set system dns failed: {code}"); + } + } + Err(err) => { + log::error!(target: "app", "set system dns failed: {err}"); + } + } +} + +#[cfg(target_os = "macos")] +pub async fn restore_public_dns() { + use crate::{core::handle, utils::dirs}; + use tauri_plugin_shell::ShellExt; + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + log::error!(target: "app", "app_handle not available for DNS restoration"); + return; + } + }; + log::info!(target: "app", "try to unset system dns"); + let resource_dir = match dirs::app_resources_dir() { + Ok(dir) => dir, + Err(e) => { + log::error!(target: "app", "Failed to get resource directory: {}", e); + return; + } + }; + let script = resource_dir.join("unset_dns.sh"); + if !script.exists() { + log::error!(target: "app", "unset_dns.sh not found"); + return; + } + let script = script.to_string_lossy().into_owned(); + match app_handle + .shell() + .command("bash") + .args([script]) + .current_dir(resource_dir) + .status() + .await + { + Ok(status) => { + if status.success() { + log::info!(target: "app", "unset system dns successfully"); + } else { + let code = status.code().unwrap_or(-1); + log::error!(target: "app", "unset system dns failed: {code}"); + } + } + Err(err) => { + log::error!(target: "app", "unset system dns failed: {err}"); + } + } +} diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs new file mode 100644 index 000000000..0eb64a102 --- /dev/null +++ b/src-tauri/src/utils/resolve/mod.rs @@ -0,0 +1,213 @@ +use tauri::AppHandle; + +use crate::{ + config::Config, + core::{handle, hotkey::Hotkey, sysopt, tray::Tray, CoreManager, Timer}, + logging, logging_error, + module::lightweight::auto_lightweight_mode_init, + process::AsyncHandler, + utils::{init, logging::Type, network::NetworkManager, resolve::window::create_window, server}, +}; + +pub mod dns; +pub mod scheme; +pub mod ui; +pub mod window; +pub mod window_script; + +pub fn resolve_setup_sync(app_handle: AppHandle) { + init_handle(app_handle); + init_scheme(); + init_embed_server(); + NetworkManager::new().init(); +} + +pub fn resolve_setup_async() { + let start_time = std::time::Instant::now(); + logging!( + info, + Type::Setup, + true, + "开始执行异步设置任务... 线程ID: {:?}", + std::thread::current().id() + ); + + AsyncHandler::spawn(|| async { + init_resources().await; + init_work_config().await; + init_startup_script().await; + + init_timer().await; + init_hotkey().await; + init_auto_lightweight_mode().await; + + init_verge_config().await; + init_core_manager().await; + init_system_proxy().await; + + // 在单独的 spawn 中运行 sync 函数,避免 Send 问题 + AsyncHandler::spawn_blocking(|| { + init_system_proxy_guard(); + }); + + init_window().await; + init_tray().await; + refresh_tray_menu().await + }); + + let elapsed = start_time.elapsed(); + logging!( + info, + Type::Setup, + true, + "异步设置任务完成,耗时: {:?}", + elapsed + ); + + if elapsed.as_secs() > 10 { + logging!( + warn, + Type::Setup, + true, + "异步设置任务耗时较长({:?})", + elapsed + ); + } +} + +// 其它辅助函数不变 +pub async fn resolve_reset_async() { + logging!(info, Type::Tray, true, "Resetting system proxy"); + logging_error!( + Type::System, + true, + sysopt::Sysopt::global().reset_sysproxy().await + ); + + logging!(info, Type::Core, true, "Stopping core service"); + logging_error!(Type::Core, true, CoreManager::global().stop_core().await); + + #[cfg(target_os = "macos")] + { + use dns::restore_public_dns; + + logging!(info, Type::System, true, "Restoring system DNS settings"); + restore_public_dns().await; + } +} + +pub fn init_handle(app_handle: AppHandle) { + logging!(info, Type::Setup, true, "Initializing app handle..."); + handle::Handle::global().init(app_handle); +} + +pub(super) fn init_scheme() { + logging!(info, Type::Setup, true, "Initializing custom URL scheme"); + logging_error!(Type::Setup, true, init::init_scheme()); +} + +pub(super) fn init_embed_server() { + logging!(info, Type::Setup, true, "Initializing embedded server..."); + server::embed_server(); +} +pub(super) async fn init_resources() { + logging!(info, Type::Setup, true, "Initializing resources..."); + logging_error!(Type::Setup, true, init::init_resources().await); +} + +pub(super) async fn init_startup_script() { + logging!(info, Type::Setup, true, "Initializing startup script"); + logging_error!(Type::Setup, true, init::startup_script().await); +} + +pub(super) async fn init_timer() { + logging!(info, Type::Setup, true, "Initializing timer..."); + logging_error!(Type::Setup, true, Timer::global().init().await); +} + +pub(super) async fn init_hotkey() { + logging!(info, Type::Setup, true, "Initializing hotkey..."); + logging_error!(Type::Setup, true, Hotkey::global().init().await); +} + +pub(super) async fn init_auto_lightweight_mode() { + logging!( + info, + Type::Setup, + true, + "Initializing auto lightweight mode..." + ); + logging_error!(Type::Setup, true, auto_lightweight_mode_init().await); +} + +pub async fn init_work_config() { + logging!( + info, + Type::Setup, + true, + "Initializing work configuration..." + ); + logging_error!(Type::Setup, true, init::init_config().await); +} + +pub(super) async fn init_tray() { + logging!(info, Type::Setup, true, "Initializing system tray..."); + logging_error!(Type::Setup, true, Tray::global().init().await); +} + +pub(super) async fn init_verge_config() { + logging!( + info, + Type::Setup, + true, + "Initializing verge configuration..." + ); + logging_error!(Type::Setup, true, Config::init_config().await); +} + +pub(super) async fn init_core_manager() { + logging!(info, Type::Setup, true, "Initializing core manager..."); + logging_error!(Type::Setup, true, CoreManager::global().init().await); +} + +pub(super) async fn init_system_proxy() { + logging!(info, Type::Setup, true, "Initializing system proxy..."); + logging_error!( + Type::Setup, + true, + sysopt::Sysopt::global().update_sysproxy().await + ); +} + +pub(super) fn init_system_proxy_guard() { + logging!( + info, + Type::Setup, + true, + "Initializing system proxy guard..." + ); + logging_error!( + Type::Setup, + true, + sysopt::Sysopt::global().init_guard_sysproxy() + ); +} + +pub(super) async fn refresh_tray_menu() { + logging!(info, Type::Setup, true, "Refreshing tray menu..."); + logging_error!(Type::Setup, true, Tray::global().update_part().await); +} + +pub(super) async fn init_window() { + let is_silent_start = + { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); + #[cfg(target_os = "macos")] + { + if is_silent_start { + use crate::core::handle::Handle; + + Handle::global().set_activation_policy_accessory(); + } + } + create_window(!is_silent_start).await; +} diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs new file mode 100644 index 000000000..513c927ba --- /dev/null +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -0,0 +1,75 @@ +use anyhow::{bail, Result}; +use percent_encoding::percent_decode_str; +use tauri::Url; + +use crate::{config::PrfItem, core::handle, logging, utils::logging::Type, wrap_err}; + +pub async fn resolve_scheme(param: String) -> Result<()> { + log::info!(target:"app", "received deep link: {param}"); + + let param_str = if param.starts_with("[") && param.len() > 4 { + param + .get(2..param.len() - 2) + .ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))? + } else { + param.as_str() + }; + + // 解析 URL + let link_parsed = match Url::parse(param_str) { + Ok(url) => url, + Err(e) => { + bail!("failed to parse deep link: {:?}, param: {:?}", e, param); + } + }; + + if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { + let name = link_parsed + .query_pairs() + .find(|(key, _)| key == "name") + .map(|(_, value)| value.into_owned()); + + let url_param = if let Some(query) = link_parsed.query() { + let prefix = "url="; + if let Some(pos) = query.find(prefix) { + let raw_url = &query[pos + prefix.len()..]; + Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) + } else { + None + } + } else { + None + }; + + match url_param { + Some(url) => { + log::info!(target:"app", "decoded subscription url: {url}"); + // create_window(false).await; + match PrfItem::from_url(url.as_ref(), name, None, None).await { + Ok(item) => { + let uid = match item.uid.clone() { + Some(uid) => uid, + None => { + logging!(error, Type::Config, true, "Profile item missing UID"); + handle::Handle::notice_message( + "import_sub_url::error", + "Profile item missing UID".to_string(), + ); + return Ok(()); + } + }; + let result = crate::config::profiles::profiles_append_item_safe(item).await; + let _ = wrap_err!(result); + handle::Handle::notice_message("import_sub_url::ok", uid); + } + Err(e) => { + handle::Handle::notice_message("import_sub_url::error", e.to_string()); + } + } + } + None => bail!("failed to get profile url"), + } + } + + Ok(()) +} diff --git a/src-tauri/src/utils/resolve/ui.rs b/src-tauri/src/utils/resolve/ui.rs new file mode 100644 index 000000000..c7589fb32 --- /dev/null +++ b/src-tauri/src/utils/resolve/ui.rs @@ -0,0 +1,106 @@ +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use tokio::sync::Notify; + +use crate::{logging, utils::logging::Type}; + +// 使用 AtomicBool 替代 RwLock,性能更好且无锁 +static UI_READY: OnceCell = OnceCell::new(); +// 获取UI就绪状态细节 +static UI_READY_STATE: OnceCell = OnceCell::new(); +// 添加通知机制,用于事件驱动的 UI 就绪检测 +static UI_READY_NOTIFY: OnceCell> = OnceCell::new(); + +// UI就绪阶段状态枚举 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UiReadyStage { + NotStarted, + Loading, + DomReady, + ResourcesLoaded, + Ready, +} + +// UI就绪详细状态 +#[derive(Debug)] +struct UiReadyState { + stage: RwLock, +} + +impl Default for UiReadyState { + fn default() -> Self { + Self { + stage: RwLock::new(UiReadyStage::NotStarted), + } + } +} + +pub(super) fn get_ui_ready() -> &'static AtomicBool { + UI_READY.get_or_init(|| AtomicBool::new(false)) +} + +fn get_ui_ready_state() -> &'static UiReadyState { + UI_READY_STATE.get_or_init(UiReadyState::default) +} + +fn get_ui_ready_notify() -> &'static Arc { + UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new())) +} + +/// 等待 UI 就绪的异步函数,使用事件驱动而非轮询 +pub async fn wait_for_ui_ready(timeout_seconds: u64) -> bool { + // 首先检查是否已经就绪 + if get_ui_ready().load(Ordering::Acquire) { + return true; + } + + // 使用 tokio::select! 同时等待通知和超时 + tokio::select! { + _ = get_ui_ready_notify().notified() => { + // 收到通知后再次确认状态(防止虚假通知) + get_ui_ready().load(Ordering::Acquire) + } + _ = tokio::time::sleep(std::time::Duration::from_secs(timeout_seconds)) => { + // 超时,返回当前状态 + get_ui_ready().load(Ordering::Acquire) + } + } +} + +// 更新UI准备阶段 +pub fn update_ui_ready_stage(stage: UiReadyStage) { + let state = get_ui_ready_state(); + let mut stage_lock = state.stage.write(); + + *stage_lock = stage; + // 如果是最终阶段,标记UI完全就绪 + if stage == UiReadyStage::Ready { + mark_ui_ready(); + } +} + +// 标记UI已准备就绪 +pub fn mark_ui_ready() { + get_ui_ready().store(true, Ordering::Release); + logging!(info, Type::Window, true, "UI已标记为完全就绪"); + + // 通知所有等待的任务 + get_ui_ready_notify().notify_waiters(); +} + +// 重置UI就绪状态 +pub fn reset_ui_ready() { + get_ui_ready().store(false, Ordering::Release); + { + let state = get_ui_ready_state(); + let mut stage = state.stage.write(); + *stage = UiReadyStage::NotStarted; + } + logging!(info, Type::Window, true, "UI就绪状态已重置"); + + // 注意:这里不需要通知,因为重置后状态变为 false +} diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs new file mode 100644 index 000000000..eebaac5e3 --- /dev/null +++ b/src-tauri/src/utils/resolve/window.rs @@ -0,0 +1,298 @@ +use std::time::{Duration, Instant}; + +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use tauri::{Manager, WebviewWindow}; + +use crate::{ + core::handle, + logging, + module::lightweight, + process::AsyncHandler, + utils::{ + logging::Type, + resolve::{ + ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage}, + window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT}, + }, + }, +}; + +// 定义默认窗口尺寸常量 +const DEFAULT_WIDTH: f64 = 940.0; +const DEFAULT_HEIGHT: f64 = 700.0; + +const MINIMAL_WIDTH: f64 = 520.0; +const MINIMAL_HEIGHT: f64 = 520.0; + +// 窗口创建锁,防止并发创建窗口 +static WINDOW_CREATING: OnceCell> = OnceCell::new(); + +fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> { + WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) +} + +/// 检查是否已存在窗口,如果存在则显示并返回 true +fn check_existing_window(is_show: bool) -> Option { + if let Some(app_handle) = handle::Handle::global().app_handle() { + if let Some(window) = app_handle.get_webview_window("main") { + logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口"); + if is_show { + if window.is_minimized().unwrap_or(false) { + logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化"); + let _ = window.unminimize(); + } + let _ = window.show(); + let _ = window.set_focus(); + + #[cfg(target_os = "macos")] + handle::Handle::global().set_activation_policy_regular(); + } + return Some(true); + } + } + None +} + +/// 获取窗口创建锁,防止并发创建 +fn acquire_window_creation_lock() -> Result<(), bool> { + let creating_lock = get_window_creating_lock(); + let mut creating = creating_lock.lock(); + + let (is_creating, last_time) = *creating; + let elapsed = last_time.elapsed(); + + if is_creating && elapsed < Duration::from_secs(2) { + logging!( + info, + Type::Window, + true, + "窗口创建请求被忽略,因为最近创建过 ({:?}ms)", + elapsed.as_millis() + ); + return Err(false); + } + + *creating = (true, Instant::now()); + Ok(()) +} + +/// 重置窗口创建锁 +fn reset_window_creation_lock() { + let creating_lock = get_window_creating_lock(); + let mut creating = creating_lock.lock(); + *creating = (false, Instant::now()); + logging!(debug, Type::Window, true, "窗口创建状态已重置"); +} + +/// 构建新的 WebView 窗口 +fn build_new_window() -> Result { + let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { + logging!( + error, + Type::Window, + true, + "无法获取app_handle,窗口创建失败" + ); + "无法获取app_handle".to_string() + })?; + + tauri::WebviewWindowBuilder::new( + &app_handle, + "main", /* the unique window label */ + tauri::WebviewUrl::App("index.html".into()), + ) + .title("Clash Verge") + .center() + .decorations(true) + .fullscreen(false) + .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) + .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) + .visible(true) // 立即显示窗口,避免用户等待 + .initialization_script(WINDOW_INITIAL_SCRIPT) + .build() + .map_err(|e| { + logging!(error, Type::Window, true, "主窗口构建失败: {}", e); + e.to_string() + }) +} + +/// 窗口创建后的初始设置 +async fn setup_window_post_creation() { + update_ui_ready_stage(UiReadyStage::NotStarted); + handle::Handle::global().mark_startup_completed(); + + logging!( + debug, + Type::Window, + true, + "异步窗口任务开始 (启动已标记完成)" + ); + + // 先运行轻量模式检测 + lightweight::run_once_auto_lightweight().await; + + // 发送启动完成事件,触发前端开始加载 + logging!( + debug, + Type::Window, + true, + "发送 verge://startup-completed 事件" + ); + handle::Handle::notify_startup_completed(); +} + +/// 通过窗口标签处理窗口显示逻辑(减少任务大小的优化版本) +fn handle_window_display_by_label(window_label: String, is_show: bool) { + if !is_show { + logging!( + debug, + Type::Window, + true, + "is_show为false,窗口保持隐藏状态" + ); + return; + } + + // 通过标签重新获取窗口实例 + let app_handle = match handle::Handle::global().app_handle() { + Some(handle) => handle, + None => { + logging!(error, Type::Window, true, "无法获取app handle"); + return; + } + }; + + let window = match app_handle.get_webview_window(&window_label) { + Some(window) => window, + None => { + logging!( + error, + Type::Window, + true, + "无法通过标签获取窗口: {}", + window_label + ); + return; + } + }; + + // 立即显示窗口 + let _ = window.show(); + let _ = window.set_focus(); + logging!(info, Type::Window, true, "窗口已立即显示"); + + #[cfg(target_os = "macos")] + handle::Handle::global().set_activation_policy_regular(); + + let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() { + 3 + } else { + 8 + }; + + logging!( + info, + Type::Window, + true, + "开始监控UI加载状态 (最多{}秒)...", + timeout_seconds + ); + + // 异步监控UI状态 + AsyncHandler::spawn(move || async move { + monitor_ui_loading(timeout_seconds).await; + }); + + logging!(info, Type::Window, true, "窗口显示流程完成"); +} + +/// 监控 UI 加载状态 - 优雅的事件驱动版本 +async fn monitor_ui_loading(timeout_seconds: u64) { + logging!( + debug, + Type::Window, + true, + "启动UI状态监控,使用事件驱动方式,超时{}秒", + timeout_seconds + ); + + // 使用事件驱动的方式等待 UI 就绪,完全消除轮询 + let ui_ready = wait_for_ui_ready(timeout_seconds).await; + + if ui_ready { + logging!(info, Type::Window, true, "UI已完全加载就绪(事件驱动)"); + handle::Handle::global() + .get_window() + .map(|window| window.eval(INITIAL_LOADING_OVERLAY)); + } else { + logging!( + warn, + Type::Window, + true, + "UI加载监控超时({}秒),但窗口已正常显示", + timeout_seconds + ); + + // 超时后手动设置 UI 就绪状态(保持向后兼容性) + get_ui_ready().store(true, std::sync::atomic::Ordering::Release); + logging!(info, Type::Window, true, "超时后成功设置UI就绪状态"); + } +} + +pub async fn create_window(is_show: bool) -> bool { + logging!( + info, + Type::Window, + true, + "开始创建/显示主窗口, is_show={}", + is_show + ); + + if !is_show { + lightweight::set_lightweight_mode(true).await; + handle::Handle::notify_startup_completed(); + return false; + } + + // 检查是否已存在窗口 + if let Some(result) = check_existing_window(is_show) { + return result; + } + + // 检查 app_handle 是否存在 + if handle::Handle::global().app_handle().is_none() { + logging!(error, Type::Window, true, "No window found in app_handle"); + return false; + } + + // 获取窗口创建锁 + if let Err(should_return) = acquire_window_creation_lock() { + return should_return; + } + + // 构建新窗口 + let newly_created_window = match build_new_window() { + Ok(window) => { + // 窗口创建成功,重置锁状态 + reset_window_creation_lock(); + window + } + Err(_) => { + // 窗口创建失败,重置锁状态 + reset_window_creation_lock(); + return false; + } + }; + + logging!(debug, Type::Window, true, "主窗口实例创建成功"); + + // 获取窗口标签,减少闭包捕获的大小 + let window_label = newly_created_window.label().to_string(); + + // 异步处理窗口后续设置,只捕获必要的小数据 + setup_window_post_creation().await; + handle_window_display_by_label(window_label, is_show); + + true +} diff --git a/src-tauri/src/utils/resolve/window_script.rs b/src-tauri/src/utils/resolve/window_script.rs new file mode 100644 index 000000000..632b0bf86 --- /dev/null +++ b/src-tauri/src/utils/resolve/window_script.rs @@ -0,0 +1,71 @@ +pub const WINDOW_INITIAL_SCRIPT: &str = r#" + console.log('[Tauri] 窗口初始化脚本开始执行'); + + function createLoadingOverlay() { + + if (document.getElementById('initial-loading-overlay')) { + console.log('[Tauri] 加载指示器已存在'); + return; + } + + console.log('[Tauri] 创建加载指示器'); + const loadingDiv = document.createElement('div'); + loadingDiv.id = 'initial-loading-overlay'; + loadingDiv.innerHTML = ` +
+
+
+
+
Loading Clash Verge...
+
+ + `; + + if (document.body) { + document.body.appendChild(loadingDiv); + } else { + document.addEventListener('DOMContentLoaded', () => { + if (document.body && !document.getElementById('initial-loading-overlay')) { + document.body.appendChild(loadingDiv); + } + }); + } + } + + createLoadingOverlay(); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', createLoadingOverlay); + } else { + createLoadingOverlay(); + } + + console.log('[Tauri] 窗口初始化脚本执行完成'); +"#; + +pub const INITIAL_LOADING_OVERLAY: &str = r" + const overlay = document.getElementById('initial-loading-overlay'); + if (overlay) { + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 300); + } +"; diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 2ec197f63..507399588 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -44,9 +44,8 @@ pub async fn check_singleton() -> Result<()> { pub fn embed_server() { let port = IVerge::get_singleton_port(); - AsyncHandler::spawn(move || async move { + AsyncHandler::spawn_blocking(move || async move { let visible = warp::path!("commands" / "visible").and_then(|| async { - resolve::create_window(false).await; Ok::<_, warp::Rejection>(warp::reply::with_status( "ok".to_string(), warp::http::StatusCode::OK, @@ -85,7 +84,11 @@ pub fn embed_server() { // Spawn async work in a fire-and-forget manner let param = query.param.clone(); tokio::task::spawn_local(async move { - logging_error!(Type::Setup, true, resolve::resolve_scheme(param).await); + logging_error!( + Type::Setup, + true, + resolve::scheme::resolve_scheme(param).await + ); }); warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK) }); diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index c1b287f9e..97b5984ae 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -365,7 +365,7 @@ impl WindowManager { use crate::utils::resolve; // 使用 tokio runtime 阻塞调用 async 函数 - AsyncHandler::block_on(resolve::create_window(true)) + AsyncHandler::block_on(resolve::window::create_window(true)) } /// 获取详细的窗口状态信息