mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:43:44 +08:00
refactor: separate notification thread in frontend to prevent UI freeze
This commit is contained in:
parent
3983762202
commit
ba0f4cdde0
@ -42,6 +42,7 @@
|
|||||||
- 编辑非激活订阅的时候不在触发当前订阅配置重载
|
- 编辑非激活订阅的时候不在触发当前订阅配置重载
|
||||||
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程。减少应用程序可能出现的主进程卡死情况
|
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程。减少应用程序可能出现的主进程卡死情况
|
||||||
- 优化系统代理设置更新逻辑
|
- 优化系统代理设置更新逻辑
|
||||||
|
- 重构前端通知系统分离通知线程防止前端卡死
|
||||||
|
|
||||||
## v2.2.3
|
## v2.2.3
|
||||||
|
|
||||||
|
@ -1,9 +1,32 @@
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
mpsc, Arc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||||
|
|
||||||
use crate::{logging, logging_error, process::AsyncHandler, utils::logging::Type};
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
/// 不同类型的前端通知
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum FrontendEvent {
|
||||||
|
RefreshClash,
|
||||||
|
RefreshVerge,
|
||||||
|
NoticeMessage { status: String, message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 事件发送统计和监控
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct EventStats {
|
||||||
|
total_sent: AtomicU64,
|
||||||
|
total_errors: AtomicU64,
|
||||||
|
last_error_time: RwLock<Option<Instant>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 存储启动期间的错误消息
|
/// 存储启动期间的错误消息
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -12,30 +35,270 @@ struct ErrorMessage {
|
|||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
/// 全局前端通知系统
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NotificationSystem {
|
||||||
|
sender: Option<mpsc::Sender<FrontendEvent>>,
|
||||||
|
worker_handle: Option<thread::JoinHandle<()>>,
|
||||||
|
is_running: bool,
|
||||||
|
stats: EventStats,
|
||||||
|
last_emit_time: RwLock<Instant>,
|
||||||
|
/// 当通知系统失败超过阈值时,进入紧急模式
|
||||||
|
emergency_mode: RwLock<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotificationSystem {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotificationSystem {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sender: None,
|
||||||
|
worker_handle: None,
|
||||||
|
is_running: false,
|
||||||
|
stats: EventStats::default(),
|
||||||
|
last_emit_time: RwLock::new(Instant::now()),
|
||||||
|
emergency_mode: RwLock::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动通知处理线程
|
||||||
|
fn start(&mut self) {
|
||||||
|
if self.is_running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
self.sender = Some(tx);
|
||||||
|
self.is_running = true;
|
||||||
|
|
||||||
|
*self.last_emit_time.write() = Instant::now();
|
||||||
|
|
||||||
|
self.worker_handle = Some(
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("frontend-notifier".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let handle = Handle::global();
|
||||||
|
|
||||||
|
while !handle.is_exiting() {
|
||||||
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
|
Ok(event) => {
|
||||||
|
let system_guard = handle.notification_system.read();
|
||||||
|
let is_emergency = system_guard
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|sys| Some(*sys.emergency_mode.read()))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if is_emergency {
|
||||||
|
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||||
|
if status == "info" {
|
||||||
|
log::warn!(
|
||||||
|
"Emergency mode active, skipping info message"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = handle.get_window() {
|
||||||
|
match event {
|
||||||
|
FrontendEvent::RefreshClash => {
|
||||||
|
Self::emit_with_timeout(
|
||||||
|
&window,
|
||||||
|
"verge://refresh-clash-config",
|
||||||
|
"yes",
|
||||||
|
&handle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FrontendEvent::RefreshVerge => {
|
||||||
|
Self::emit_with_timeout(
|
||||||
|
&window,
|
||||||
|
"verge://refresh-verge-config",
|
||||||
|
"yes",
|
||||||
|
&handle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FrontendEvent::NoticeMessage {
|
||||||
|
ref status,
|
||||||
|
ref message,
|
||||||
|
} => {
|
||||||
|
if let Err(e) = window.emit(
|
||||||
|
"verge://notice-message",
|
||||||
|
(status.clone(), message.clone()),
|
||||||
|
) {
|
||||||
|
log::warn!("Failed to send notice: {}", e);
|
||||||
|
if let Some(sys) = system_guard.as_ref() {
|
||||||
|
sys.stats
|
||||||
|
.total_errors
|
||||||
|
.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*sys.stats.last_error_time.write() =
|
||||||
|
Some(Instant::now());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(sys) = system_guard.as_ref() {
|
||||||
|
sys.stats
|
||||||
|
.total_sent
|
||||||
|
.fetch_add(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
log::info!(
|
||||||
|
"Notification channel disconnected, exiting worker thread"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Notification worker thread exiting");
|
||||||
|
})
|
||||||
|
.expect("Failed to start notification worker thread"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用超时控制发送事件,防止无限阻塞
|
||||||
|
fn emit_with_timeout<P: serde::Serialize + Clone + Send + 'static>(
|
||||||
|
window: &WebviewWindow,
|
||||||
|
event: &str,
|
||||||
|
payload: P,
|
||||||
|
handle: &Handle,
|
||||||
|
) {
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
let system_guard = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_guard.as_ref() {
|
||||||
|
*system.last_emit_time.write() = start;
|
||||||
|
|
||||||
|
let window_label = window.label().to_string();
|
||||||
|
let event_clone = event.to_string();
|
||||||
|
let app_handle_clone = match handle.app_handle() {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let _ = thread::Builder::new()
|
||||||
|
.name("emit-timeout".into())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Some(win) = app_handle_clone.get_webview_window(&window_label) {
|
||||||
|
let result = win.emit(&event_clone, payload);
|
||||||
|
let _ = tx.send(result);
|
||||||
|
} else {
|
||||||
|
let _ = tx.send(Err(tauri::Error::WebviewNotFound));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match rx.recv_timeout(Duration::from_millis(500)) {
|
||||||
|
Ok(result) => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::warn!("Failed to emit event {}: {}", event, e);
|
||||||
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
} else {
|
||||||
|
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("Emit timed out for event: {}", event);
|
||||||
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
|
||||||
|
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
||||||
|
if errors > 5 {
|
||||||
|
log::warn!("Too many emit errors, entering emergency mode");
|
||||||
|
*system.emergency_mode.write() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送事件到队列
|
||||||
|
fn send_event(&self, event: FrontendEvent) -> bool {
|
||||||
|
if *self.emergency_mode.read() {
|
||||||
|
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||||
|
if status == "info" {
|
||||||
|
log::info!("Skipping info message in emergency mode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
match sender.send(event) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to send event to notification queue: {:?}", e);
|
||||||
|
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Notification system not started, can't send event");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown(&mut self) {
|
||||||
|
self.is_running = false;
|
||||||
|
self.sender = None;
|
||||||
|
|
||||||
|
if let Some(handle) = self.worker_handle.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||||
pub is_exiting: Arc<RwLock<bool>>,
|
pub is_exiting: Arc<RwLock<bool>>,
|
||||||
/// 存储启动过程中产生的错误消息队列
|
|
||||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||||
startup_completed: Arc<RwLock<bool>>,
|
startup_completed: Arc<RwLock<bool>>,
|
||||||
|
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Handle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
app_handle: Arc::new(RwLock::new(None)),
|
||||||
|
is_exiting: Arc::new(RwLock::new(false)),
|
||||||
|
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
startup_completed: Arc::new(RwLock::new(false)),
|
||||||
|
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
pub fn global() -> &'static Handle {
|
pub fn global() -> &'static Handle {
|
||||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||||
|
HANDLE.get_or_init(|| Handle::default())
|
||||||
HANDLE.get_or_init(|| Handle {
|
|
||||||
app_handle: Arc::new(RwLock::new(None)),
|
|
||||||
is_exiting: Arc::new(RwLock::new(false)),
|
|
||||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
|
||||||
startup_completed: Arc::new(RwLock::new(false)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, app_handle: &AppHandle) {
|
pub fn init(&self, app_handle: &AppHandle) {
|
||||||
let mut handle = self.app_handle.write();
|
{
|
||||||
*handle = Some(app_handle.clone());
|
let mut handle = self.app_handle.write();
|
||||||
|
*handle = Some(app_handle.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut system_opt = self.notification_system.write();
|
||||||
|
if let Some(system) = system_opt.as_mut() {
|
||||||
|
system.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||||
@ -43,7 +306,7 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||||
let app_handle = self.app_handle().unwrap();
|
let app_handle = self.app_handle()?;
|
||||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||||
if window.is_none() {
|
if window.is_none() {
|
||||||
log::debug!(target:"app", "main window not found");
|
log::debug!(target:"app", "main window not found");
|
||||||
@ -52,43 +315,35 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_clash() {
|
pub fn refresh_clash() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
let handle = Self::global();
|
||||||
logging_error!(
|
if handle.is_exiting() {
|
||||||
Type::Frontend,
|
return;
|
||||||
true,
|
}
|
||||||
window.emit("verge://refresh-clash-config", "yes")
|
|
||||||
);
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::RefreshClash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_verge() {
|
pub fn refresh_verge() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
let handle = Self::global();
|
||||||
logging_error!(
|
if handle.is_exiting() {
|
||||||
Type::Frontend,
|
return;
|
||||||
true,
|
}
|
||||||
window.emit("verge://refresh-verge-config", "yes")
|
|
||||||
);
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::RefreshVerge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
/// 通知前端显示消息队列
|
||||||
pub fn refresh_profiles() {
|
|
||||||
if let Some(window) = Self::global().get_window() {
|
|
||||||
logging_error!(
|
|
||||||
Type::Frontend,
|
|
||||||
true,
|
|
||||||
window.emit("verge://refresh-profiles-config", "yes")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通知前端显示消息,如果在启动过程中,则将消息存入启动错误队列
|
|
||||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||||
let handle = Self::global();
|
let handle = Self::global();
|
||||||
let status_str = status.into();
|
let status_str = status.into();
|
||||||
let msg_str = msg.into();
|
let msg_str = msg.into();
|
||||||
|
|
||||||
// 检查是否正在启动过程中
|
|
||||||
if !*handle.startup_completed.read() {
|
if !*handle.startup_completed.read() {
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
@ -99,7 +354,6 @@ impl Handle {
|
|||||||
msg_str
|
msg_str
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将消息添加到启动错误队列
|
|
||||||
let mut errors = handle.startup_errors.write();
|
let mut errors = handle.startup_errors.write();
|
||||||
errors.push(ErrorMessage {
|
errors.push(ErrorMessage {
|
||||||
status: status_str,
|
status: status_str,
|
||||||
@ -108,32 +362,19 @@ impl Handle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用AsyncHandler发送消息,防止阻塞
|
if handle.is_exiting() {
|
||||||
let status_clone = status_str.clone();
|
return;
|
||||||
let msg_clone = msg_str.clone();
|
}
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(move || async move {
|
let system_opt = handle.notification_system.read();
|
||||||
let handle_clone = Self::global();
|
if let Some(system) = system_opt.as_ref() {
|
||||||
if let Some(window) = handle_clone.get_window() {
|
system.send_event(FrontendEvent::NoticeMessage {
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
|
status: status_str,
|
||||||
window.emit("verge://notice-message", (status_clone, msg_clone))
|
message: msg_str,
|
||||||
})
|
});
|
||||||
.await
|
}
|
||||||
{
|
|
||||||
Ok(result) => {
|
|
||||||
if let Err(e) = result {
|
|
||||||
logging!(warn, Type::Frontend, true, "发送通知消息失败: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
logging!(warn, Type::Frontend, true, "发送通知消息超时");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 标记启动已完成,并发送所有启动阶段累积的错误消息
|
|
||||||
pub fn mark_startup_completed(&self) {
|
pub fn mark_startup_completed(&self) {
|
||||||
{
|
{
|
||||||
let mut completed = self.startup_completed.write();
|
let mut completed = self.startup_completed.write();
|
||||||
@ -162,26 +403,47 @@ impl Handle {
|
|||||||
errors.len()
|
errors.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 等待2秒以确保前端已完全加载,延迟发送错误通知
|
// 启动单独线程处理启动错误,避免阻塞主线程
|
||||||
if let Some(window) = self.get_window() {
|
let thread_result = thread::Builder::new()
|
||||||
let window_clone = window.clone();
|
.name("startup-errors-sender".into())
|
||||||
let errors_clone = errors.clone();
|
.spawn(move || {
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
let handle = Handle::global();
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for error in errors_clone {
|
let system_opt = handle.notification_system.read();
|
||||||
let _ =
|
if let Some(system) = system_opt.as_ref() {
|
||||||
window_clone.emit("verge://notice-message", (error.status, error.message));
|
for error in errors {
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
if handle.is_exiting() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
system.send_event(FrontendEvent::NoticeMessage {
|
||||||
|
status: error.status,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(300));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Err(e) = thread_result {
|
||||||
|
log::error!("Failed to spawn startup errors thread: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_is_exiting(&self) {
|
pub fn set_is_exiting(&self) {
|
||||||
let mut is_exiting = self.is_exiting.write();
|
let mut is_exiting = self.is_exiting.write();
|
||||||
*is_exiting = true;
|
*is_exiting = true;
|
||||||
|
|
||||||
|
let mut system_opt = self.notification_system.write();
|
||||||
|
if let Some(system) = system_opt.as_mut() {
|
||||||
|
system.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_exiting(&self) -> bool {
|
pub fn is_exiting(&self) -> bool {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user