refactor(sysproxy): watch the network from the main run loop

This commit is contained in:
wonfen
2026-09-03 22:42:50 +08:00
parent e57d115110
commit c0c1fbcfc2
2 changed files with 25 additions and 41 deletions
+24 -40
View File
@@ -10,10 +10,7 @@ use core_foundation::{
runloop::{CFRunLoop, kCFRunLoopCommonModes},
string::CFString,
};
use std::{
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use system_configuration::dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext};
/// Set once the subscription is live. Startup defers the proxy write to the watcher only then.
@@ -23,42 +20,29 @@ pub fn is_armed() -> bool {
ARMED.load(Ordering::Acquire)
}
/// Watch the primary IPv4 service from a dedicated run loop thread. Returns once the
/// subscription is live, so a service appearing from here on is never missed.
pub async fn start() {
const ARM_TIMEOUT: Duration = Duration::from_secs(5);
let (ready, armed) = tokio::sync::oneshot::channel();
let spawned = std::thread::Builder::new().name("network-watch".into()).spawn(move || {
let store = SCDynamicStoreBuilder::new("clash-verge-rev")
.callback_context(SCDynamicStoreCallBackContext {
callout: on_change,
info: (),
})
.build();
let keys = CFArray::from_CFTypes(&[CFString::from_static_string("State:/Network/Global/IPv4")]);
let source = store
.filter(|store| store.set_notification_keys(&keys, &CFArray::<CFString>::from_CFTypes(&[])))
.and_then(|store| store.create_run_loop_source());
let Some(source) = source else {
logging!(
warn,
Type::Core,
"could not watch the network; the proxy is not re-applied on changes"
);
return;
};
CFRunLoop::get_current().add_source(&source, unsafe { kCFRunLoopCommonModes });
ARMED.store(true, Ordering::Release);
let _ = ready.send(());
CFRunLoop::run_current();
});
match spawned {
// A thread that gave up drops `ready`; a stuck one must not hold up startup either.
Ok(_) => {
let _ = tokio::time::timeout(ARM_TIMEOUT, armed).await;
}
Err(error) => logging!(warn, Type::Core, "could not start the network watcher: {error}"),
}
/// Subscribe to the primary IPv4 service on the main run loop. Armed on return, so a service
/// appearing from here on is never missed.
pub fn start() {
let store = SCDynamicStoreBuilder::new("clash-verge-rev")
.callback_context(SCDynamicStoreCallBackContext {
callout: on_change,
info: (),
})
.build();
let keys = CFArray::from_CFTypes(&[CFString::from_static_string("State:/Network/Global/IPv4")]);
let Some(source) = store
.filter(|store| store.set_notification_keys(&keys, &CFArray::<CFString>::from_CFTypes(&[])))
.and_then(|store| store.create_run_loop_source())
else {
logging!(
warn,
Type::Core,
"could not watch the network; the proxy is not re-applied on changes"
);
return;
};
CFRunLoop::get_main().add_source(&source, unsafe { kCFRunLoopCommonModes });
ARMED.store(true, Ordering::Release);
}
fn on_change(_: SCDynamicStore, _: CFArray<CFString>, (): &mut ()) {
+1 -1
View File
@@ -70,7 +70,7 @@ pub(crate) fn resolve_setup_async() {
// Live before the core starts, so a service appearing mid-start is not missed.
#[cfg(target_os = "macos")]
crate::core::network_watch::start().await;
crate::core::network_watch::start();
let core_init = AsyncHandler::spawn(|| async {
init_core_manager().await;