feat: improve core functionality to prevent main process blocking, enhance MihomoManager, and optimize window creation process

This commit is contained in:
wonfen 2025-04-25 18:24:16 +08:00
parent d6a79316a6
commit 55cde38562
4 changed files with 256 additions and 155 deletions

View File

@ -37,6 +37,7 @@
- 切换到规则页面时自动刷新规则数据 - 切换到规则页面时自动刷新规则数据
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
- 编辑非激活订阅的时候不在触发当前订阅配置重载 - 编辑非激活订阅的时候不在触发当前订阅配置重载
- 改进核心功能防止主进程阻塞、改进MihomoManager实现以及优化窗口创建流程。减少应用程序可能出现的主进程卡死情况
## v2.2.3 ## v2.2.3

View File

@ -19,6 +19,7 @@ use tauri::AppHandle;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
use utils::logging::Type; use utils::logging::Type;
/// A global singleton handle to the application. /// A global singleton handle to the application.
@ -85,14 +86,23 @@ impl AppHandleManager {
#[allow(clippy::panic)] #[allow(clippy::panic)]
pub fn run() { pub fn run() {
// 单例检测 // 单例检测 - 使用超时机制防止阻塞
let app_exists: bool = AsyncHandler::block_on(move || async move { let app_exists: bool = AsyncHandler::block_on(move || async move {
if server::check_singleton().await.is_err() { match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
println!("app exists"); println!("app exists");
true true
} else { } else {
false false
} }
}
Err(_) => {
// 超时处理
println!("singleton check timeout, assuming app doesn't exist");
false
}
}
}); });
if app_exists { if app_exists {
return; return;
@ -139,8 +149,21 @@ pub fn run() {
}); });
}); });
AsyncHandler::block_on(move || async move { // 使用 block_on 但增加超时保护
resolve::resolve_setup(app).await; AsyncHandler::block_on(|| async {
match timeout(Duration::from_secs(30), resolve::resolve_setup(app)).await {
Ok(_) => {
logging!(info, Type::Setup, true, "App setup completed successfully");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"App setup timed out, proceeding anyway"
);
}
}
}); });
Ok(()) Ok(())

View File

@ -1,43 +1,92 @@
use crate::config::Config; use crate::config::Config;
use mihomo_api; use mihomo_api;
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::Lazy;
use std::sync::Mutex; use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant};
use tauri::http::{HeaderMap, HeaderValue}; use tauri::http::{HeaderMap, HeaderValue};
#[cfg(target_os = "macos")]
use tokio_tungstenite::tungstenite::http; // 缓存的最大有效期5秒
const CACHE_TTL: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate { pub struct Rate {
pub up: u64, pub up: u64,
pub down: u64, pub down: u64,
} }
// 缓存MihomoManager实例
struct MihomoCache {
manager: mihomo_api::MihomoManager,
created_at: Instant,
server: String,
}
// 使用RwLock替代Mutex允许多个读取操作并发进行
pub struct MihomoManager { pub struct MihomoManager {
mihomo: Mutex<OnceCell<mihomo_api::MihomoManager>>, mihomo_cache: RwLock<Option<MihomoCache>>,
create_lock: Mutex<()>,
} }
impl MihomoManager { impl MihomoManager {
fn __global() -> &'static MihomoManager { fn __global() -> &'static MihomoManager {
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager { static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
mihomo: Mutex::new(OnceCell::new()), mihomo_cache: RwLock::new(None),
create_lock: Mutex::new(()),
}); });
&INSTANCE &INSTANCE
} }
pub fn global() -> mihomo_api::MihomoManager { pub fn global() -> mihomo_api::MihomoManager {
let instance = MihomoManager::__global(); let instance = MihomoManager::__global();
let (current_server, headers) = MihomoManager::get_clash_client_info().unwrap();
let lock = instance.mihomo.lock().unwrap(); // 尝试从缓存读取(只需读锁)
if let Some(mihomo) = lock.get() { {
if mihomo.get_mihomo_server() == current_server { let cache = instance.mihomo_cache.read();
return mihomo.clone(); if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
// 检查缓存是否有效
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
} }
} }
lock.set(mihomo_api::MihomoManager::new(current_server, headers)) // 缓存无效,获取创建锁
.ok(); let _create_guard = instance.create_lock.lock();
lock.get().unwrap().clone()
// 再次检查缓存(双重检查锁定模式)
{
let cache = instance.mihomo_cache.read();
if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
}
}
// 创建新实例
let (current_server, headers) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
// 更新缓存
{
let mut cache = instance.mihomo_cache.write();
*cache = Some(MihomoCache {
manager: manager.clone(),
created_at: Instant::now(),
server: current_server,
});
}
manager
} }
} }
@ -54,17 +103,31 @@ impl MihomoManager {
Some((server, headers)) Some((server, headers))
} }
// 提供默认值的版本避免在connection_info为None时panic
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
Self::get_clash_client_info().unwrap_or_else(|| {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
("http://127.0.0.1:9090".to_string(), headers)
})
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn get_traffic_ws_url() -> (String, HeaderValue) { pub fn get_traffic_ws_url() -> (String, HeaderValue) {
let (url, headers) = MihomoManager::get_clash_client_info().unwrap(); let (url, headers) = MihomoManager::get_clash_client_info_or_default();
let ws_url = url.replace("http://", "ws://") + "/traffic"; let ws_url = url.replace("http://", "ws://") + "/traffic";
let auth = headers let auth = headers
.get("Authorization") .get("Authorization")
.unwrap() .map(|val| val.to_str().unwrap_or("").to_string())
.to_str() .unwrap_or_default();
.unwrap()
.to_string(); // 创建默认的空HeaderValue而不是使用unwrap_or_default
let token = http::header::HeaderValue::from_str(&auth).unwrap(); let token = match HeaderValue::from_str(&auth) {
Ok(v) => v,
Err(_) => HeaderValue::from_static(""),
};
(ws_url, token) (ws_url, token)
} }
} }

View File

@ -6,13 +6,14 @@ use crate::{
logging, logging_error, logging, logging_error,
module::lightweight, module::lightweight,
process::AsyncHandler, process::AsyncHandler,
utils::{error, init, logging::Type, server}, utils::{dirs, error, init, logging::Type, server},
wrap_err, wrap_err,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use serde::{Deserialize, Serialize};
use serde_json; use serde_json;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{ use std::{
@ -32,12 +33,23 @@ pub static VERSION: OnceCell<String> = OnceCell::new();
static STATE_WIDTH: OnceCell<u32> = OnceCell::new(); static STATE_WIDTH: OnceCell<u32> = OnceCell::new();
static STATE_HEIGHT: OnceCell<u32> = OnceCell::new(); static STATE_HEIGHT: OnceCell<u32> = OnceCell::new();
// 定义默认窗口尺寸常量
const DEFAULT_WIDTH: u32 = 900;
const DEFAULT_HEIGHT: u32 = 700;
// 添加全局UI准备就绪标志 // 添加全局UI准备就绪标志
static UI_READY: OnceCell<Arc<RwLock<bool>>> = OnceCell::new(); static UI_READY: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
// 窗口创建锁,防止并发创建窗口 // 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new(); static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
// 定义窗口状态结构体
#[derive(Debug, Serialize, Deserialize)]
struct WindowState {
width: Option<u32>,
height: Option<u32>,
}
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> { fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
} }
@ -195,49 +207,47 @@ pub fn create_window(is_showup: bool) {
let _guard = WindowCreateGuard; let _guard = WindowCreateGuard;
// 打印 .window-state.json 文件路径 // 打印 .window-state.json 文件路径
if let Ok(app_dir) = crate::utils::dirs::app_home_dir() { let window_state_file = dirs::app_home_dir()
let window_state_path = app_dir.join(".window-state.json"); .ok()
.map(|dir| dir.join(".window-state.json"));
logging!( logging!(
info, info,
Type::Window, Type::Window,
true, true,
"窗口状态文件路径: {:?}", "窗口状态文件路径: {:?}",
window_state_path window_state_file
); );
// 尝试读取窗口状态文件内容 // 从文件加载窗口状态
if window_state_path.exists() { if let Some(window_state_file_path) = window_state_file {
match std::fs::read_to_string(&window_state_path) { if window_state_file_path.exists() {
match std::fs::read_to_string(&window_state_file_path) {
Ok(content) => { Ok(content) => {
logging!(info, Type::Window, true, "窗口状态文件内容: {}", content); logging!(
debug,
// 解析窗口状态文件 Type::Window,
match serde_json::from_str::<serde_json::Value>(&content) { true,
Ok(state_json) => { "读取窗口状态文件内容成功: {} 字节",
if let Some(main_window) = state_json.get("main") { content.len()
let width = main_window );
.get("width")
.and_then(|v| v.as_u64())
.unwrap_or(0)
as u32;
let height = main_window
.get("height")
.and_then(|v| v.as_u64())
.unwrap_or(0)
as u32;
match serde_json::from_str::<WindowState>(&content) {
Ok(window_state) => {
logging!( logging!(
info, info,
Type::Window, Type::Window,
true, true,
"窗口状态文件中的尺寸: {}x{}", "成功解析窗口状态: width={:?}, height={:?}",
width, window_state.width,
height window_state.height
); );
// 保存读取到的尺寸,用于后续检查 // 存储窗口状态以供后续使用
STATE_WIDTH.get_or_init(|| width); if let Some(width) = window_state.width {
STATE_HEIGHT.get_or_init(|| height); STATE_WIDTH.set(width).ok();
}
if let Some(height) = window_state.height {
STATE_HEIGHT.set(height).ok();
} }
} }
Err(e) => { Err(e) => {
@ -299,56 +309,59 @@ pub fn create_window(is_showup: bool) {
} }
} }
// 定义默认窗口大小 let width = STATE_WIDTH.get().copied().unwrap_or(DEFAULT_WIDTH);
const DEFAULT_WIDTH: u32 = 900; let height = STATE_HEIGHT.get().copied().unwrap_or(DEFAULT_HEIGHT);
const DEFAULT_HEIGHT: u32 = 700;
const MIN_WIDTH: u32 = 650;
const MIN_HEIGHT: u32 = 580;
#[cfg(target_os = "windows")] logging!(
let window = tauri::WebviewWindowBuilder::new( info,
&app_handle, Type::Window,
"main".to_string(), true,
tauri::WebviewUrl::App("index.html".into()), "Initializing new window with size: {}x{}",
) width,
.title("Clash Verge") height
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) );
.min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64)
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.shadow(true)
.visible(false) // 初始不可见等待UI加载完成后再显示
.build();
// 根据不同平台创建不同配置的窗口
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let window = tauri::WebviewWindowBuilder::new( let win_builder = {
// 基本配置
let builder = tauri::WebviewWindowBuilder::new(
&app_handle, &app_handle,
"main".to_string(), "main",
tauri::WebviewUrl::App("index.html".into()),
)
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64)
.visible(false) // 初始不可见等待UI加载完成后再显示
.build();
#[cfg(target_os = "linux")]
let window = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
) )
.title("Clash Verge") .title("Clash Verge")
.decorations(false) .center()
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .decorations(true)
.min_inner_size(MIN_WIDTH as f64, MIN_HEIGHT as f64) .hidden_title(true) // 隐藏标题文本
.transparent(true) .fullscreen(false)
.visible(false) // 初始不可见等待UI加载完成后再显示 .inner_size(width as f64, height as f64)
.build(); .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",
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.fullscreen(false)
.inner_size(width as f64, height as f64)
.min_inner_size(520.0, 520.0)
.visible(false)
.decorations(false);
let window = win_builder.build();
match window { match window {
Ok(window) => { Ok(window) => {
@ -379,33 +392,43 @@ pub fn create_window(is_showup: bool) {
DEFAULT_HEIGHT DEFAULT_HEIGHT
); );
if state_width < DEFAULT_WIDTH || state_height < DEFAULT_HEIGHT { // 优化窗口大小设置
if size.width < state_width || size.height < state_height {
logging!( logging!(
info, info,
Type::Window, Type::Window,
true, true,
"状态文件窗口尺寸小于默认值,将使用默认尺寸: {}x{}", "强制设置窗口尺寸: {}x{}",
DEFAULT_WIDTH, state_width,
DEFAULT_HEIGHT state_height
); );
let _ = window.set_size(tauri::LogicalSize::new( // 尝试不同的方式设置窗口大小
DEFAULT_WIDTH as f64, let _ = window.set_size(tauri::PhysicalSize {
DEFAULT_HEIGHT as f64, width: state_width,
)); height: state_height,
} else if size.width != state_width || size.height != state_height { });
// 如果API报告的尺寸与状态文件不一致记录日志
// 关键:等待短暂时间让窗口尺寸生效
std::thread::sleep(std::time::Duration::from_millis(50));
// 再次检查窗口尺寸
if let Ok(new_size) = window.inner_size() {
logging!( logging!(
warn, info,
Type::Window, Type::Window,
true, true,
"API报告的窗口尺寸与状态文件不一致" "设置后API报告的窗口尺寸: {}x{}",
new_size.width,
new_size.height
); );
} }
} }
}
// 创建异步任务处理UI就绪和显示窗口 // 标记此窗口是否从轻量模式恢复
let was_from_lightweight = from_lightweight; let was_from_lightweight = from_lightweight;
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
// 处理启动完成 // 处理启动完成
handle::Handle::global().mark_startup_completed(); handle::Handle::global().mark_startup_completed();
@ -425,53 +448,44 @@ pub fn create_window(is_showup: bool) {
5 5
}; };
// 等待UI就绪 // 使用普通的等待方式替代事件监听,简化实现
async fn wait_for_ui_ready() { let wait_result =
tokio::time::timeout(Duration::from_secs(timeout_seconds), async {
while !*get_ui_ready().read() { while !*get_ui_ready().read() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
}
} }
})
.await;
// 使用超时机制等待UI就绪 // 根据结果处理
match tokio::time::timeout( match wait_result {
std::time::Duration::from_secs(timeout_seconds),
wait_for_ui_ready(),
)
.await
{
Ok(_) => { Ok(_) => {
logging!(info, Type::Window, true, "UI准备就绪,显示窗口"); logging!(info, Type::Window, true, "UI就绪显示窗口");
} }
Err(_) => { Err(_) => {
logging!( logging!(
warn, warn,
Type::Window, Type::Window,
true, true,
"等待UI准备就绪超时({}秒),强制显示窗口", "等待UI就绪超时({}秒),强制显示窗口",
timeout_seconds timeout_seconds
); );
// 强制设置UI就绪状态
*get_ui_ready().write() = true;
} }
} }
// 无论是否超时,都显示窗口 // 显示窗口
let _ = window_clone.show(); let _ = window_clone.show();
let _ = window_clone.set_focus(); let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "窗口创建和显示流程已完成");
} }
} else {
logging!(error, Type::Window, true, "无法获取主窗口");
} }
}); });
logging!(info, Type::Window, true, "异步任务已创建");
} }
Err(e) => { Err(e) => {
logging!( logging!(error, Type::Window, true, "Failed to create window: {}", e);
error,
Type::Window,
true,
"Failed to create window: {:?}",
e
);
} }
} }
} }