mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
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:
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user