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.
This commit is contained in:
Tunglies
2026-10-13 12:33:22 +00:00
committed by GitHub
parent 3ec858eb9f
commit 5b10331022
8 changed files with 36 additions and 174 deletions
+23 -31
View File
@@ -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::{ use std::sync::{
OnceLock, OnceLock,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@@ -5,20 +17,13 @@ use std::sync::{
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
#[cfg(unix)]
mod unix; mod unix;
#[cfg(windows)]
mod windows;
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new(); pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new(); pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)] /// Serializes signal-triggered cleanups: a signal arriving while a quit
pub enum ShutdownOutcome { /// attempt is still in flight is dropped instead of overlapping it.
Committed,
Canceled,
}
pub(crate) struct ShutdownLatch { pub(crate) struct ShutdownLatch {
is_cleaning_up: AtomicBool, is_cleaning_up: AtomicBool,
} }
@@ -36,17 +41,18 @@ impl ShutdownLatch {
.is_ok() .is_ok()
} }
fn finish(&self, outcome: ShutdownOutcome) { fn finish(&self) {
if matches!(outcome, ShutdownOutcome::Canceled) { self.is_cleaning_up.store(false, Ordering::Release);
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, Fut>(f: F) pub fn register<F, Fut>(f: F)
where where
F: Fn() -> Fut + Send + Sync + 'static, F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static, Fut: Future<Output = ()> + Send + 'static,
{ {
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() { RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
Ok(rt) => Some(rt), Ok(rt) => Some(rt),
@@ -61,36 +67,22 @@ where
} }
}); });
#[cfg(unix)]
unix::register(f); unix::register(f);
#[cfg(windows)]
windows::register(f);
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ShutdownLatch, ShutdownOutcome}; use super::ShutdownLatch;
#[test] #[test]
fn canceled_shutdown_releases_latch_and_permits_next_attempt() { fn finished_attempt_releases_latch_for_the_next_signal() {
let latch = ShutdownLatch::new(); let latch = ShutdownLatch::new();
assert!(latch.try_begin()); assert!(latch.try_begin());
assert!(!latch.try_begin()); assert!(!latch.try_begin());
latch.finish(ShutdownOutcome::Canceled); latch.finish();
assert!(latch.try_begin()); 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());
}
} }
+4 -3
View File
@@ -1,12 +1,12 @@
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
use tokio::signal::unix::{SignalKind, signal}; use tokio::signal::unix::{SignalKind, signal};
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome}; use crate::{RUNTIME, SHUTDOWN_LATCH};
pub fn register<F, Fut>(f: F) pub fn register<F, Fut>(f: F)
where where
F: Fn() -> Fut + Send + Sync + 'static, F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static, Fut: Future<Output = ()> + Send + 'static,
{ {
if let Some(Some(rt)) = RUNTIME.get() { if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move { rt.spawn(async move {
@@ -60,7 +60,8 @@ where
} }
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name); logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
SHUTDOWN_LATCH.finish(f().await); f().await;
SHUTDOWN_LATCH.finish();
} }
}); });
} else { } else {
-83
View File
@@ -1,83 +0,0 @@
use clash_verge_logging::{Type, logging};
use tokio::signal::windows;
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ShutdownOutcome> + 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"
);
}
}
+4 -1
View File
@@ -35,7 +35,6 @@ tauri-build = { version = "2.7.1", features = [] }
clash-verge-draft = { workspace = true } clash-verge-draft = { workspace = true }
clash-verge-media-unlock = { workspace = true } clash-verge-media-unlock = { workspace = true }
clash-verge-logging = { workspace = true } clash-verge-logging = { workspace = true }
clash-verge-signal = { workspace = true }
clash-verge-i18n = { workspace = true } clash-verge-i18n = { workspace = true }
clash-verge-limiter = { workspace = true } clash-verge-limiter = { workspace = true }
tauri-plugin-clash-verge-sysinfo = { workspace = true } tauri-plugin-clash-verge-sysinfo = { workspace = true }
@@ -116,6 +115,10 @@ specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.190" 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] [target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true } tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
objc2 = "0.6.5" objc2 = "0.6.5"
+1 -3
View File
@@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() {
logging!(info, Type::Window, "Window toggle result: {result:?}"); logging!(info, Type::Window, "Window toggle result: {result:?}");
} }
pub async fn quit() -> clash_verge_signal::ShutdownOutcome { pub async fn quit() {
logging!(debug, Type::System, "启动退出流程"); logging!(debug, Type::System, "启动退出流程");
// 设置退出标志 // 设置退出标志
handle::Handle::global().set_is_exiting(); handle::Handle::global().set_is_exiting();
@@ -165,13 +165,11 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
NoticeStatus::AppQuitCoreStopFailed, NoticeStatus::AppQuitCoreStopFailed,
cleanup_result.stop_error.unwrap_or_default(), cleanup_result.stop_error.unwrap_or_default(),
); );
return clash_verge_signal::ShutdownOutcome::Canceled;
} }
utils::server::shutdown_embedded_server(); utils::server::shutdown_embedded_server();
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
clash_verge_signal::ShutdownOutcome::Committed
} }
pub async fn clean_async() -> CleanupResult { pub async fn clean_async() -> CleanupResult {
+1
View File
@@ -230,6 +230,7 @@ pub fn run() -> std::process::ExitCode {
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
resolve::resolve_setup_async(); resolve::resolve_setup_async();
resolve::resolve_setup_sync(); resolve::resolve_setup_sync();
#[cfg(target_os = "linux")]
resolve::init_signal(); resolve::init_signal();
})) { })) {
log_setup_panic("window-core", panic); log_setup_panic("window-core", panic);
+2 -1
View File
@@ -20,7 +20,6 @@ use crate::{
utils::{init, server, window_manager::WindowManager}, utils::{init, server, window_manager::WindowManager},
}; };
use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_signal;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod dns; pub mod dns;
@@ -189,6 +188,8 @@ async fn init_silent_updater() {
logging!(info, Type::Setup, "Silent updater initialized"); 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() { pub(crate) fn init_signal() {
logging!(debug, Type::Setup, "Initializing signal handlers..."); logging!(debug, Type::Setup, "Initializing signal handlers...");
clash_verge_signal::register(feat::quit); clash_verge_signal::register(feat::quit);
+1 -52
View File
@@ -39,8 +39,6 @@ static INSTANCE_LOCK: OnceLock<std::fs::File> = OnceLock::new();
const PAC_INITIAL_AVAILABLE: bool = false; const PAC_INITIAL_AVAILABLE: bool = false;
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE); static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
static COMMANDS_READY: AtomicBool = AtomicBool::new(false); 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SingletonDisposition { pub enum SingletonDisposition {
@@ -164,55 +162,12 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
.untuple_one() .untuple_one()
} }
#[cfg(feature = "verge-dev")]
fn dev_quit_route<F>(
token: String,
commands_ready: &'static AtomicBool,
quit_requested: &'static AtomicBool,
trigger: F,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + 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) { fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (shutdown_tx, shutdown_rx) = oneshot::channel();
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx))); let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
#[cfg(feature = "verge-dev")] #[cfg(feature = "verge-dev")]
let auth = instance_auth(token.clone()); let auth = instance_auth(token);
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
let auth = instance_auth(token); 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); 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 { AsyncHandler::spawn(move || async move {
warp::serve(commands) warp::serve(commands)
.incoming(listener) .incoming(listener)