From 5b10331022940dfbd049adfa6f714d3da2d5a6ef Mon Sep 17 00:00:00 2001 From: Tunglies Date: Tue, 13 Oct 2026 12:33:22 +0000 Subject: [PATCH] feat!: remove signal crate for macOS and windows (#7827) * feat!: remove signal crate * fix: clippy * feat(signal): limit the shutdown signal crate to Linux Upstream now covers termination and session-end events on Windows and macOS, but Linux has no equivalent, so pre-shutdown cleanup there still depends on listening for SIGTERM/SIGINT/SIGHUP (#7827). Restore clash-verge-signal behind a target_os = "linux" gate instead of dropping it: the dependency lives in a linux-only target section, the crate root carries an English Linux-only rationale and compiles to nothing elsewhere, and the Windows backend is gone. register() now takes the quit flow's unit future: the latch serializes in-flight cleanups and releases after each attempt, leaving the exit-or-cancel decision to the quit flow itself. Verified with cargo check and clippy for aarch64-unknown-linux-gnu, and cargo check on the host where the crate compiles empty. --- crates/clash-verge-signal/src/lib.rs | 54 +++++++-------- crates/clash-verge-signal/src/unix.rs | 7 +- crates/clash-verge-signal/src/windows.rs | 83 ------------------------ src-tauri/Cargo.toml | 5 +- src-tauri/src/feat/window.rs | 4 +- src-tauri/src/lib.rs | 1 + src-tauri/src/utils/resolve/mod.rs | 3 +- src-tauri/src/utils/server.rs | 53 +-------------- 8 files changed, 36 insertions(+), 174 deletions(-) delete mode 100644 crates/clash-verge-signal/src/windows.rs diff --git a/crates/clash-verge-signal/src/lib.rs b/crates/clash-verge-signal/src/lib.rs index 60511d960..7ddc283aa 100644 --- a/crates/clash-verge-signal/src/lib.rs +++ b/crates/clash-verge-signal/src/lib.rs @@ -1,3 +1,15 @@ +//! Linux-only termination signal bridge. +//! +//! Upstream Tauri delivers session-end and termination events to the app on +//! Windows and macOS, but has no equivalent on Linux: a desktop session may +//! end without any window or run event reaching the process. Linux builds +//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them +//! to the app quit flow, so system proxy and TUN cleanup still runs before +//! the process dies. Non-Linux builds compile this crate to nothing and must +//! not depend on it. + +#![cfg(target_os = "linux")] + use std::sync::{ OnceLock, atomic::{AtomicBool, Ordering}, @@ -5,20 +17,13 @@ use std::sync::{ use clash_verge_logging::{Type, logging}; -#[cfg(unix)] mod unix; -#[cfg(windows)] -mod windows; pub(crate) static RUNTIME: OnceLock> = OnceLock::new(); pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new(); -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ShutdownOutcome { - Committed, - Canceled, -} - +/// Serializes signal-triggered cleanups: a signal arriving while a quit +/// attempt is still in flight is dropped instead of overlapping it. pub(crate) struct ShutdownLatch { is_cleaning_up: AtomicBool, } @@ -36,17 +41,18 @@ impl ShutdownLatch { .is_ok() } - fn finish(&self, outcome: ShutdownOutcome) { - if matches!(outcome, ShutdownOutcome::Canceled) { - self.is_cleaning_up.store(false, Ordering::Release); - } + fn finish(&self) { + self.is_cleaning_up.store(false, Ordering::Release); } } +/// Spawns the Linux signal loop on a dedicated runtime so it survives the +/// teardown of any app-owned runtime. The handler runs once per accepted +/// signal; the quit flow it invokes owns the exit-or-cancel decision. pub fn register(f: F) where F: Fn() -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, + Fut: Future + Send + 'static, { RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() { Ok(rt) => Some(rt), @@ -61,36 +67,22 @@ where } }); - #[cfg(unix)] unix::register(f); - - #[cfg(windows)] - windows::register(f); } #[cfg(test)] mod tests { - use super::{ShutdownLatch, ShutdownOutcome}; + use super::ShutdownLatch; #[test] - fn canceled_shutdown_releases_latch_and_permits_next_attempt() { + fn finished_attempt_releases_latch_for_the_next_signal() { let latch = ShutdownLatch::new(); assert!(latch.try_begin()); assert!(!latch.try_begin()); - latch.finish(ShutdownOutcome::Canceled); + latch.finish(); assert!(latch.try_begin()); } - - #[test] - fn committed_shutdown_retains_latch_and_suppresses_repeats() { - let latch = ShutdownLatch::new(); - - assert!(latch.try_begin()); - latch.finish(ShutdownOutcome::Committed); - - assert!(!latch.try_begin()); - } } diff --git a/crates/clash-verge-signal/src/unix.rs b/crates/clash-verge-signal/src/unix.rs index 1bba1e833..5bbcb779a 100644 --- a/crates/clash-verge-signal/src/unix.rs +++ b/crates/clash-verge-signal/src/unix.rs @@ -1,12 +1,12 @@ use clash_verge_logging::{Type, logging}; use tokio::signal::unix::{SignalKind, signal}; -use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome}; +use crate::{RUNTIME, SHUTDOWN_LATCH}; pub fn register(f: F) where F: Fn() -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, + Fut: Future + Send + 'static, { if let Some(Some(rt)) = RUNTIME.get() { rt.spawn(async move { @@ -60,7 +60,8 @@ where } logging!(info, Type::SystemSignal, "Caught signal {}", signal_name); - SHUTDOWN_LATCH.finish(f().await); + f().await; + SHUTDOWN_LATCH.finish(); } }); } else { diff --git a/crates/clash-verge-signal/src/windows.rs b/crates/clash-verge-signal/src/windows.rs deleted file mode 100644 index 836a59c35..000000000 --- a/crates/clash-verge-signal/src/windows.rs +++ /dev/null @@ -1,83 +0,0 @@ -use clash_verge_logging::{Type, logging}; -use tokio::signal::windows; - -use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome}; - -pub fn register(f: F) -where - F: Fn() -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, -{ - if let Some(Some(rt)) = RUNTIME.get() { - rt.spawn(async move { - let mut ctrl_c = match windows::ctrl_c() { - Ok(s) => s, - Err(e) => { - logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e); - return; - } - }; - - let mut ctrl_close = match windows::ctrl_close() { - Ok(s) => s, - Err(e) => { - logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e); - return; - } - }; - - let mut ctrl_shutdown = match windows::ctrl_shutdown() { - Ok(s) => s, - Err(e) => { - logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e); - return; - } - }; - - let mut ctrl_logoff = match windows::ctrl_logoff() { - Ok(s) => s, - Err(e) => { - logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e); - return; - } - }; - - loop { - let signal_name; - tokio::select! { - _ = ctrl_c.recv() => { - signal_name = "Ctrl+C"; - } - _ = ctrl_close.recv() => { - signal_name = "Ctrl+Close"; - } - _ = ctrl_shutdown.recv() => { - signal_name = "Ctrl+Shutdown"; - } - _ = ctrl_logoff.recv() => { - signal_name = "Ctrl+Logoff"; - } - } - - if !SHUTDOWN_LATCH.try_begin() { - logging!( - info, - Type::SystemSignal, - "Already shutting down, ignoring repeated signal: {}", - signal_name - ); - continue; - } - logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name); - - SHUTDOWN_LATCH.finish(f().await); - } - }); - } else { - logging!( - error, - Type::SystemSignal, - "register shutdown signal failed, RUNTIME is not available" - ); - } -} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6a1bc7942..a6b6a53fe 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,7 +35,6 @@ tauri-build = { version = "2.7.1", features = [] } clash-verge-draft = { workspace = true } clash-verge-media-unlock = { workspace = true } clash-verge-logging = { workspace = true } -clash-verge-signal = { workspace = true } clash-verge-i18n = { workspace = true } clash-verge-limiter = { workspace = true } tauri-plugin-clash-verge-sysinfo = { workspace = true } @@ -116,6 +115,10 @@ specta-typescript = "=0.0.12" [target.'cfg(unix)'.dependencies] libc = "0.2.190" +[target.'cfg(target_os = "linux")'.dependencies] +# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal. +clash-verge-signal = { workspace = true } + [target.'cfg(target_os = "macos")'.dependencies] tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true } objc2 = "0.6.5" diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 35f16987c..e618701ed 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() { logging!(info, Type::Window, "Window toggle result: {result:?}"); } -pub async fn quit() -> clash_verge_signal::ShutdownOutcome { +pub async fn quit() { logging!(debug, Type::System, "启动退出流程"); // 设置退出标志 handle::Handle::global().set_is_exiting(); @@ -165,13 +165,11 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome { NoticeStatus::AppQuitCoreStopFailed, cleanup_result.stop_error.unwrap_or_default(), ); - return clash_verge_signal::ShutdownOutcome::Canceled; } utils::server::shutdown_embedded_server(); let app_handle = handle::Handle::app_handle(); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); - clash_verge_signal::ShutdownOutcome::Committed } pub async fn clean_async() -> CleanupResult { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f2d61f427..7e11e5c02 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -230,6 +230,7 @@ pub fn run() -> std::process::ExitCode { if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { resolve::resolve_setup_async(); resolve::resolve_setup_sync(); + #[cfg(target_os = "linux")] resolve::init_signal(); })) { log_setup_panic("window-core", panic); diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index f96939eb8..7ba959ae3 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -20,7 +20,6 @@ use crate::{ utils::{init, server, window_manager::WindowManager}, }; use clash_verge_logging::{Type, logging, logging_error}; -use clash_verge_signal; #[cfg(target_os = "macos")] pub mod dns; @@ -189,6 +188,8 @@ async fn init_silent_updater() { logging!(info, Type::Setup, "Silent updater initialized"); } +// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal. +#[cfg(target_os = "linux")] pub(crate) fn init_signal() { logging!(debug, Type::Setup, "Initializing signal handlers..."); clash_verge_signal::register(feat::quit); diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 9d05c92d4..42d097a86 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -39,8 +39,6 @@ static INSTANCE_LOCK: OnceLock = OnceLock::new(); const PAC_INITIAL_AVAILABLE: bool = false; static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE); static COMMANDS_READY: AtomicBool = AtomicBool::new(false); -#[cfg(feature = "verge-dev")] -static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SingletonDisposition { @@ -164,55 +162,12 @@ fn instance_auth(token: String) -> impl warp::Filter( - token: String, - commands_ready: &'static AtomicBool, - quit_requested: &'static AtomicBool, - trigger: F, -) -> impl warp::Filter + Clone -where - F: Fn() + Clone + Send + Sync + 'static, -{ - instance_auth(token) - .and(warp::path!("commands" / "dev" / "quit")) - .and(warp::post()) - .and_then(move || { - let trigger = trigger.clone(); - async move { - if !commands_ready.load(Ordering::Acquire) { - return Ok::<_, warp::Rejection>(warp::reply::with_status( - String::from("starting"), - warp::http::StatusCode::SERVICE_UNAVAILABLE, - )); - } - if quit_requested - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - trigger(); - } - Ok(warp::reply::with_status( - String::from("accepted"), - warp::http::StatusCode::ACCEPTED, - )) - } - }) -} - -#[cfg(feature = "verge-dev")] -fn release_dev_quit_latch(quit_requested: &AtomicBool, outcome: clash_verge_signal::ShutdownOutcome) { - if matches!(outcome, clash_verge_signal::ShutdownOutcome::Canceled) { - quit_requested.store(false, Ordering::Release); - } -} - fn start_embedded_server(listener: tokio::net::TcpListener, token: String) { let (shutdown_tx, shutdown_rx) = oneshot::channel(); let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx))); #[cfg(feature = "verge-dev")] - let auth = instance_auth(token.clone()); + let auth = instance_auth(token); #[cfg(not(feature = "verge-dev"))] let auth = instance_auth(token); @@ -278,12 +233,6 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) { }); let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac); - #[cfg(feature = "verge-dev")] - let commands = commands.or(dev_quit_route(token, &COMMANDS_READY, &DEV_QUIT_REQUESTED, || { - AsyncHandler::spawn(|| async { - release_dev_quit_latch(&DEV_QUIT_REQUESTED, crate::feat::quit().await); - }); - })); AsyncHandler::spawn(move || async move { warp::serve(commands) .incoming(listener)