Compare commits

...
2 Commits
Author SHA1 Message Date
Tunglies b3502ea46a fix: clippy 2026-08-30 02:14:26 +08:00
Tunglies 86cada4cff feat!: remove signal crate 2026-08-30 02:14:26 +08:00
11 changed files with 18 additions and 448 deletions
Generated
-10
View File
@@ -1119,7 +1119,6 @@ dependencies = [
"clash-verge-limiter",
"clash-verge-logging",
"clash-verge-media-unlock",
"clash-verge-signal",
"clash_verge_logger",
"clash_verge_service_ipc",
"compact_str",
@@ -1234,15 +1233,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "clash-verge-signal"
version = "0.1.0"
dependencies = [
"clash-verge-logging",
"log",
"tokio",
]
[[package]]
name = "clash_verge_logger"
version = "0.2.2"
+8 -4
View File
@@ -4,7 +4,6 @@ members = [
"crates/clash-verge-draft",
"crates/clash-verge-media-unlock",
"crates/clash-verge-logging",
"crates/clash-verge-signal",
"crates/tauri-plugin-clash-verge-sysinfo",
"crates/clash-verge-i18n",
"crates/clash-verge-limiter",
@@ -50,7 +49,6 @@ split-debuginfo = "unpacked"
clash-verge-draft = { path = "crates/clash-verge-draft" }
clash-verge-media-unlock = { path = "crates/clash-verge-media-unlock" }
clash-verge-logging = { path = "crates/clash-verge-logging" }
clash-verge-signal = { path = "crates/clash-verge-signal" }
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
clash-verge-limiter = { path = "crates/clash-verge-limiter" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
@@ -70,7 +68,13 @@ tokio = { version = "1.53.1", features = [
flexi_logger = "0.31.10"
log = "0.4.33"
regex = "1.13.1"
reqwest = { version = "0.13.4", features = ["json", "cookies", "gzip", "rustls", "form"] }
reqwest = { version = "0.13.4", features = [
"json",
"cookies",
"gzip",
"rustls",
"form",
] }
rust_iso3166 = "0.2.0"
smartstring = { version = "1.0.1" }
@@ -92,7 +96,7 @@ windows-sys = { version = "0.61.2", features = [
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_Threading",
"Win32_System_JobObjects"
"Win32_System_JobObjects",
] }
# *********************************
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "clash-verge-signal"
version = "0.1.0"
edition = "2024"
rust-version = "1.91"
[dependencies]
clash-verge-logging = { workspace = true }
log = { workspace = true }
tokio = { workspace = true, features = ["signal"] }
[lints]
workspace = true
-96
View File
@@ -1,96 +0,0 @@
use std::sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
};
use clash_verge_logging::{Type, logging};
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShutdownOutcome {
Committed,
Canceled,
}
pub(crate) struct ShutdownLatch {
is_cleaning_up: AtomicBool,
}
impl ShutdownLatch {
const fn new() -> Self {
Self {
is_cleaning_up: AtomicBool::new(false),
}
}
fn try_begin(&self) -> bool {
self.is_cleaning_up
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
fn finish(&self, outcome: ShutdownOutcome) {
if matches!(outcome, ShutdownOutcome::Canceled) {
self.is_cleaning_up.store(false, Ordering::Release);
}
}
}
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
{
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
Ok(rt) => Some(rt),
Err(e) => {
logging!(
info,
Type::SystemSignal,
"register shutdown signal failed, create tokio runtime error: {}",
e
);
None
}
});
#[cfg(unix)]
unix::register(f);
#[cfg(windows)]
windows::register(f);
}
#[cfg(test)]
mod tests {
use super::{ShutdownLatch, ShutdownOutcome};
#[test]
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
let latch = ShutdownLatch::new();
assert!(latch.try_begin());
assert!(!latch.try_begin());
latch.finish(ShutdownOutcome::Canceled);
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());
}
}
-73
View File
@@ -1,73 +0,0 @@
use clash_verge_logging::{Type, logging};
use tokio::signal::unix::{SignalKind, signal};
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 sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register SIGTERM: {}", e);
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register SIGINT: {}", e);
return;
}
};
let mut sighup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register SIGHUP: {}", e);
return;
}
};
loop {
let signal_name;
tokio::select! {
_ = sigterm.recv() => {
signal_name = "SIGTERM";
}
_ = sigint.recv() => {
signal_name = "SIGINT";
}
_ = sighup.recv() => {
signal_name = "SIGHUP";
}
else => {
break;
}
}
if !SHUTDOWN_LATCH.try_begin() {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
SHUTDOWN_LATCH.finish(f().await);
}
});
} else {
logging!(
error,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
}
}
-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"
);
}
}
+7 -3
View File
@@ -17,7 +17,10 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["clash_verge_logger/color", "clash_verge_service_ipc/development-channel"]
verge-dev = [
"clash_verge_logger/color",
"clash_verge_service_ipc/development-channel",
]
dev-sidecar = ["verge-dev"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
@@ -34,7 +37,6 @@ tauri-build = { version = "2.6.3", 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 }
@@ -60,7 +62,9 @@ bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.1"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
rfd = { version = "0.16.0", default-features = false, features = [
"common-controls-v6",
] }
dunce = "1.0.5"
nanoid = "0.5"
chrono = { workspace = true }
+1 -3
View File
@@ -110,7 +110,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();
@@ -133,13 +133,11 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
"app_quit::core_stop_failed",
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 {
-1
View File
@@ -302,7 +302,6 @@ 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();
resolve::init_signal();
logging!(info, Type::Setup, "初始化已启动");
})) {
log_setup_panic("window-core", panic);
-6
View File
@@ -18,7 +18,6 @@ use crate::{
utils::{init, server, window_manager::WindowManager},
};
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_signal;
pub mod dns;
mod scheme;
@@ -179,11 +178,6 @@ async fn init_silent_updater() {
logging!(info, Type::Setup, "Silent updater initialized");
}
pub(crate) fn init_signal() {
logging!(info, Type::Setup, "Initializing signal handlers...");
clash_verge_signal::register(feat::quit);
}
async fn init_work_config() {
logging_error!(Type::Setup, init::init_config().await);
}
+2 -156
View File
@@ -39,8 +39,6 @@ static INSTANCE_LOCK: OnceCell<std::fs::File> = OnceCell::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 {
@@ -144,55 +142,12 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
.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) {
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);
@@ -256,12 +211,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)
@@ -432,113 +381,10 @@ fn write_instance_record(path: &Path, record: &InstanceRecord) -> Result<()> {
#[cfg(test)]
mod tests {
use super::{
InstanceRecord, PAC_INITIAL_AVAILABLE, bind_primary_listener, read_instance_record, write_instance_record,
};
use super::{InstanceRecord, bind_primary_listener, read_instance_record, write_instance_record};
#[cfg(unix)]
use super::{open_instance_lock, try_lock_instance};
#[cfg(feature = "verge-dev")]
use super::{INSTANCE_TOKEN_HEADER, dev_quit_route, release_dev_quit_latch};
#[cfg(feature = "verge-dev")]
use std::sync::atomic::Ordering;
#[cfg(feature = "verge-dev")]
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize},
};
#[test]
fn pac_is_fail_closed_before_core_readiness() {
// Asserts the default rather than the live flag: PAC availability is now derived from
// the Running Mode by `core::runstate`, so any test that drives a Core transition
// legitimately moves the live flag. That the derivation itself can never disagree with
// the Running Mode is covered in `core::runstate` against a fake environment.
const { assert!(!PAC_INITIAL_AVAILABLE) }
}
#[cfg(feature = "verge-dev")]
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn dev_quit_route_requires_private_auth_ready_state_and_dispatches_once() -> anyhow::Result<()> {
static READY: AtomicBool = AtomicBool::new(false);
static REQUESTED: AtomicBool = AtomicBool::new(false);
READY.store(false, Ordering::Release);
REQUESTED.store(false, Ordering::Release);
let dispatches = Arc::new(AtomicUsize::new(0));
let dispatches_for_route = Arc::clone(&dispatches);
let token = "ab".repeat(32);
let route = dev_quit_route(token.clone(), &READY, &REQUESTED, move || {
dispatches_for_route.fetch_add(1, Ordering::AcqRel);
});
let missing = warp::test::request()
.method("POST")
.path("/commands/dev/quit")
.reply(&route)
.await;
assert_eq!(missing.status(), warp::http::StatusCode::NOT_FOUND);
let wrong_token = warp::test::request()
.method("POST")
.path("/commands/dev/quit")
.header(INSTANCE_TOKEN_HEADER, "cd".repeat(32))
.reply(&route)
.await;
assert_eq!(wrong_token.status(), warp::http::StatusCode::NOT_FOUND);
let not_ready = warp::test::request()
.method("POST")
.path("/commands/dev/quit")
.header(INSTANCE_TOKEN_HEADER, &token)
.reply(&route)
.await;
assert_eq!(not_ready.status(), warp::http::StatusCode::SERVICE_UNAVAILABLE);
assert!(!REQUESTED.load(Ordering::Acquire));
let wrong_method = warp::test::request()
.method("GET")
.path("/commands/dev/quit")
.header(INSTANCE_TOKEN_HEADER, &token)
.reply(&route)
.await;
assert_eq!(wrong_method.status(), warp::http::StatusCode::METHOD_NOT_ALLOWED);
assert!(!REQUESTED.load(Ordering::Acquire));
READY.store(true, Ordering::Release);
let barrier = Arc::new(tokio::sync::Barrier::new(8));
let accepted = futures::future::join_all((0..8).map(|_| {
let barrier = Arc::clone(&barrier);
let route = route.clone();
let token = token.clone();
tokio::spawn(async move {
barrier.wait().await;
warp::test::request()
.method("POST")
.path("/commands/dev/quit")
.header(INSTANCE_TOKEN_HEADER, token)
.reply(&route)
.await
})
}))
.await;
for accepted in accepted {
let accepted = accepted?;
assert_eq!(accepted.status(), warp::http::StatusCode::ACCEPTED);
}
assert_eq!(dispatches.load(Ordering::Acquire), 1);
release_dev_quit_latch(&REQUESTED, clash_verge_signal::ShutdownOutcome::Canceled);
assert!(!REQUESTED.load(Ordering::Acquire));
let retry = warp::test::request()
.method("POST")
.path("/commands/dev/quit")
.header(INSTANCE_TOKEN_HEADER, &token)
.reply(&route)
.await;
assert_eq!(retry.status(), warp::http::StatusCode::ACCEPTED);
assert_eq!(dispatches.load(Ordering::Acquire), 2);
Ok(())
}
#[tokio::test]
async fn retained_record_supplies_the_next_primarys_preferred_port() -> anyhow::Result<()> {
let root = std::env::temp_dir().join(format!("singleton-sticky-port-{}", std::process::id()));