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 代理再次尝试更新
- 编辑非激活订阅的时候不在触发当前订阅配置重载
- 改进核心功能防止主进程阻塞、改进MihomoManager实现以及优化窗口创建流程。减少应用程序可能出现的主进程卡死情况
## v2.2.3

View File

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

View File

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

View File

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