mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
fix(exit): bound session-ending cleanup with a hard deadline
Refs #8180
This commit is contained in:
Generated
+1
-1
@@ -7424,7 +7424,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-mihomo"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#ba8434c08c869916b8041d707ad66599bb5230b2"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clashmap",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题
|
||||
- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题
|
||||
- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题
|
||||
- 修复从 Dock 退出、注销或关机时,应用可能长时间卡住无法退出的问题
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -297,7 +297,7 @@ importers:
|
||||
version: 2.5.1(react@19.3.0)
|
||||
tauri-plugin-mihomo-api:
|
||||
specifier: github:clash-verge-rev/tauri-plugin-mihomo#main
|
||||
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/ba8434c08c869916b8041d707ad66599bb5230b2
|
||||
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615
|
||||
types-pac:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
@@ -4865,8 +4865,8 @@ packages:
|
||||
resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/ba8434c08c869916b8041d707ad66599bb5230b2:
|
||||
resolution: {gitHosted: true, integrity: sha512-dxCM8Uw9QJGOicxNZwBQYnDScIIO8E6wlVLEcZ6RQGDfK8UMuFFL1tiQYWxBjUd9z5JBqjHKOCoYTGezxd/3Yg==, tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/ba8434c08c869916b8041d707ad66599bb5230b2}
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615:
|
||||
resolution: {gitHosted: true, integrity: sha512-1d1ELlQxqD6vb/77T7A9AKdLjPP1s9c+21i6D6Kw96DoCqxC5/aJFcG+ecYpSuSCggoYyVwWUuol1StgasB8Gw==, tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615}
|
||||
version: 0.6.0
|
||||
|
||||
teex@1.0.1:
|
||||
@@ -10190,7 +10190,7 @@ snapshots:
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/ba8434c08c869916b8041d707ad66599bb5230b2:
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.12.1
|
||||
|
||||
|
||||
@@ -3,12 +3,32 @@ use crate::core::{CoreManager, handle};
|
||||
use crate::module::lightweight;
|
||||
use crate::utils;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::mpsc::RecvTimeoutError;
|
||||
use tokio::time::Duration;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const SESSION_ENDING_STOP_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
// TODO(macOS): a DNS script already running can outlast this and write after exit.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const SESSION_ENDING_STOP_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
// Outlasts the script's own kill deadline.
|
||||
#[cfg(target_os = "macos")]
|
||||
const DNS_RESTORE_TIMEOUT: Duration =
|
||||
crate::utils::resolve::dns::DNS_SCRIPT_TIMEOUT.saturating_add(Duration::from_secs(1));
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const DNS_RESTORE_TIMEOUT: Duration = Duration::ZERO;
|
||||
|
||||
// Backstop for when the cleanup's own deadlines cannot fire.
|
||||
const SESSION_ENDING_HARD_DEADLINE: Duration = SESSION_ENDING_STOP_TIMEOUT
|
||||
.saturating_add(DNS_RESTORE_TIMEOUT)
|
||||
.saturating_add(Duration::from_secs(1));
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CleanupResult {
|
||||
pub all_success: bool,
|
||||
@@ -81,13 +101,7 @@ where
|
||||
|
||||
async fn restore_dns_after_core_stop() -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
// Outlasts the script's own deadline, so a hung restore is killed with its process group before exit.
|
||||
match timeout(
|
||||
crate::utils::resolve::dns::DNS_SCRIPT_TIMEOUT + Duration::from_secs(1),
|
||||
crate::utils::resolve::dns::restore_public_dns(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match timeout(DNS_RESTORE_TIMEOUT, crate::utils::resolve::dns::restore_public_dns()).await {
|
||||
Ok(Ok(())) => {
|
||||
logging!(debug, Type::Window, "DNS设置已恢复");
|
||||
true
|
||||
@@ -193,15 +207,50 @@ pub async fn clean_async() -> CleanupResult {
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn clean_session_ending_best_effort() -> CleanupResult {
|
||||
#[cfg(target_os = "windows")]
|
||||
let stop_timeout = Duration::from_secs(2);
|
||||
// TODO(macOS): a DNS script already blocked when the session ends holds the lifecycle lock for up to
|
||||
// DNS_SCRIPT_TIMEOUT, past this deadline; the stop is cancelled, the DNS restore skipped, and the
|
||||
// script can still write after the app exits.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let stop_timeout = Duration::from_secs(3);
|
||||
pub fn clean_session_ending_with_hard_deadline() -> Option<CleanupResult> {
|
||||
match run_with_hard_deadline(clean_session_ending_best_effort, SESSION_ENDING_HARD_DEADLINE) {
|
||||
Ok(result) => Some(result),
|
||||
Err(error) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::System,
|
||||
"Session-ending cleanup abandoned; exiting without it: {error:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Avoids the app runtime, whose workers may wait on the blocked main thread.
|
||||
fn run_with_hard_deadline<F, Fut, T>(cleanup: F, deadline: Duration) -> anyhow::Result<T>
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = T>,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
std::thread::Builder::new()
|
||||
.name("clash-verge-session-ending".into())
|
||||
.spawn(move || {
|
||||
let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let _ = sender.send(Err(error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Send first: dropping the runtime waits for blocking tasks.
|
||||
let _ = sender.send(Ok(runtime.block_on(cleanup())));
|
||||
})
|
||||
.context("failed to spawn the cleanup thread")?;
|
||||
match receiver.recv_timeout(deadline) {
|
||||
Ok(result) => result.context("failed to build the cleanup runtime"),
|
||||
Err(RecvTimeoutError::Timeout) => Err(anyhow!("still running after {} ms", deadline.as_millis())),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(anyhow!("cleanup thread ended without a result")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn clean_session_ending_best_effort() -> CleanupResult {
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
@@ -226,12 +275,12 @@ pub async fn clean_session_ending_best_effort() -> CleanupResult {
|
||||
}
|
||||
},
|
||||
async move {
|
||||
tokio::time::sleep(stop_timeout).await;
|
||||
tokio::time::sleep(SESSION_ENDING_STOP_TIMEOUT).await;
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
"Session-ending best-effort core stop timed out after {} seconds; OS or session exit is already in progress",
|
||||
stop_timeout.as_secs()
|
||||
SESSION_ENDING_STOP_TIMEOUT.as_secs()
|
||||
);
|
||||
},
|
||||
restore_dns_after_core_stop,
|
||||
@@ -274,7 +323,8 @@ pub async fn hide() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
run_interactive_cleanup_transition, run_session_ending_cleanup_transition, should_abort_exit_after_cleanup,
|
||||
run_interactive_cleanup_transition, run_session_ending_cleanup_transition, run_with_hard_deadline,
|
||||
should_abort_exit_after_cleanup,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -282,8 +332,10 @@ mod tests {
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
@@ -306,6 +358,34 @@ mod tests {
|
||||
assert!(!should_abort_exit_after_cleanup(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_deadline_returns_while_cleanup_blocks_its_thread() {
|
||||
let (release, blocked) = mpsc::channel::<()>();
|
||||
|
||||
let result = run_with_hard_deadline(
|
||||
move || async move {
|
||||
let _ = blocked.recv();
|
||||
},
|
||||
Duration::from_millis(50),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(error) if error.to_string().starts_with("still running")));
|
||||
drop(release);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_deadline_cleanup_runs_its_own_timers() {
|
||||
let result = run_with_hard_deadline(
|
||||
|| async {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
true
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert!(result.is_ok_and(|done| done));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interactive_cleanup_awaits_barrier_controlled_stop_without_cancellation() {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
+11
-10
@@ -472,23 +472,24 @@ pub fn run() -> std::process::ExitCode {
|
||||
event_handlers::handle_reopen(has_visible_windows).await;
|
||||
});
|
||||
}
|
||||
tauri::RunEvent::Exit => AsyncHandler::block_on(async {
|
||||
tauri::RunEvent::Exit => {
|
||||
// Windows session ending currently reaches Tao as WM_ENDSESSION and
|
||||
// destroys the loop without a preventable ExitRequested event.
|
||||
if !handle::Handle::global().is_exiting() {
|
||||
handle::Handle::global().set_is_exiting();
|
||||
let cleanup_result = feat::clean_session_ending_best_effort().await;
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"Unpreventable session-ending best-effort cleanup returned - core stopped: {}, all cleanup successful: {}",
|
||||
cleanup_result.core_stopped,
|
||||
cleanup_result.all_success
|
||||
);
|
||||
if let Some(cleanup_result) = feat::clean_session_ending_with_hard_deadline() {
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"Unpreventable session-ending best-effort cleanup returned - core stopped: {}, all cleanup successful: {}",
|
||||
cleanup_result.core_stopped,
|
||||
cleanup_result.all_success
|
||||
);
|
||||
}
|
||||
}
|
||||
logging!(info, Type::System, "Application exited");
|
||||
crate::core::logger::Logger::global().flush_logs();
|
||||
}),
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if module::lightweight::is_in_lightweight_mode() && !handle::Handle::global().is_exiting() {
|
||||
|
||||
Reference in New Issue
Block a user