fix(proxy): resolve system proxy toggle stuck and state desync (#6614) (#6657)

* fix(proxy): resolve system proxy toggle stuck and state desync (#6614)

Backend: replace hand-rolled AtomicBool lock in update_sysproxy() with
tokio::sync::Mutex so concurrent calls wait instead of being silently
dropped, ensuring the latest config is always applied.

Move blocking OS calls (networksetup on macOS) to spawn_blocking so
they no longer stall the tokio worker thread pool.

Frontend: release SwitchRow pendingRef in .finally() so the UI always
re-syncs with the actual OS proxy state, and rollback checked on error.

Closes #6614

* fix(changelog): add note for macOS proxy toggle freeze issue
This commit is contained in:
Tunglies 2026-03-27 23:40:03 +08:00 committed by GitHub
parent 607ef5a8a9
commit ca8e350694
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 83 deletions

View File

@ -6,6 +6,7 @@
### 🐞 修复问题 ### 🐞 修复问题
- 修复系统代理关闭后在 PAC 模式下未完全关闭 - 修复系统代理关闭后在 PAC 模式下未完全关闭
- 修复 macOS 开关代理时可能的卡死
### ✨ 新增功能 ### ✨ 新增功能

View File

@ -15,9 +15,10 @@ use std::{
time::Duration, time::Duration,
}; };
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy}; use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
use tokio::sync::Mutex as TokioMutex;
pub struct Sysopt { pub struct Sysopt {
update_sysproxy: AtomicBool, update_lock: TokioMutex<()>,
reset_sysproxy: AtomicBool, reset_sysproxy: AtomicBool,
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>, inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
guard: Arc<RwLock<GuardMonitor>>, guard: Arc<RwLock<GuardMonitor>>,
@ -26,7 +27,7 @@ pub struct Sysopt {
impl Default for Sysopt { impl Default for Sysopt {
fn default() -> Self { fn default() -> Self {
Self { Self {
update_sysproxy: AtomicBool::new(false), update_lock: TokioMutex::new(()),
reset_sysproxy: AtomicBool::new(false), reset_sysproxy: AtomicBool::new(false),
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
@ -107,95 +108,70 @@ impl Sysopt {
/// init the sysproxy /// init the sysproxy
pub async fn update_sysproxy(&self) -> Result<()> { pub async fn update_sysproxy(&self) -> Result<()> {
if self.update_sysproxy.load(Ordering::Acquire) { let _lock = self.update_lock.lock().await;
logging!(info, Type::Core, "Sysproxy update is already in progress.");
return Ok(());
}
if self
.update_sysproxy
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
logging!(info, Type::Core, "Sysproxy update is already in progress.");
return Ok(());
}
defer! {
logging!(info, Type::Core, "Sysproxy update completed.");
self.update_sysproxy.store(false, Ordering::Release);
}
let verge = Config::verge().await.latest_arc(); let verge = Config::verge().await.latest_arc();
let port = { let port = match verge.verge_mixed_port {
let verge_port = verge.verge_mixed_port;
match verge_port {
Some(port) => port, Some(port) => port,
None => Config::clash().await.latest_arc().get_mixed_port(), None => Config::clash().await.latest_arc().get_mixed_port(),
}
}; };
let pac_port = IVerge::get_singleton_port(); let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
let (sys_enable, pac_enable, proxy_host, proxy_guard) = {
(
verge.enable_system_proxy.unwrap_or_default(), verge.enable_system_proxy.unwrap_or_default(),
verge.proxy_auto_config.unwrap_or_default(), verge.proxy_auto_config.unwrap_or_default(),
verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")), verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")),
verge.enable_proxy_guard.unwrap_or_default(), verge.enable_proxy_guard.unwrap_or_default(),
) );
};
// 先 await, 避免持有锁导致的 Send 问题 // 先 await, 避免持有锁导致的 Send 问题
let bypass = get_bypass().await; let bypass = get_bypass().await;
let (sys, auto, guard_type) = {
let (sys, auto) = &mut *self.inner_proxy.write(); let (sys, auto) = &mut *self.inner_proxy.write();
sys.enable = false;
sys.host = proxy_host.clone().into(); sys.host = proxy_host.clone().into();
sys.port = port; sys.port = port;
sys.bypass = bypass.into(); sys.bypass = bypass.into();
auto.enable = false;
auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac");
self.access_guard().write().set_guard_type(GuardType::None);
// `enable_system_proxy` is the master switch. // `enable_system_proxy` is the master switch.
// When disabled, force clear both global proxy and PAC at OS level. // When disabled, force clear both global proxy and PAC at OS level.
if !sys_enable { let guard_type = if !sys_enable {
sys.set_system_proxy()?; sys.enable = false;
auto.set_auto_proxy()?; auto.enable = false;
return Ok(()); GuardType::None
} } else if pac_enable {
if pac_enable {
sys.enable = false; sys.enable = false;
auto.enable = true; auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
if proxy_guard { if proxy_guard {
self.access_guard() GuardType::Autoproxy(auto.clone())
.write() } else {
.set_guard_type(GuardType::Autoproxy(auto.clone())); GuardType::None
} }
return Ok(()); } else {
}
if sys_enable {
auto.enable = false;
sys.enable = true; sys.enable = true;
auto.set_auto_proxy()?; auto.enable = false;
sys.set_system_proxy()?;
if proxy_guard { if proxy_guard {
self.access_guard() GuardType::Sysproxy(sys.clone())
.write() } else {
.set_guard_type(GuardType::Sysproxy(sys.clone())); GuardType::None
}
return Ok(());
} }
};
(sys.clone(), auto.clone(), guard_type)
};
self.access_guard().write().set_guard_type(guard_type);
tokio::task::spawn_blocking(move || -> Result<()> {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
Ok(())
})
.await??;
Ok(()) Ok(())
} }
/// reset the sysproxy /// reset the sysproxy
#[allow(clippy::unused_async)]
pub async fn reset_sysproxy(&self) -> Result<()> { pub async fn reset_sysproxy(&self) -> Result<()> {
if self if self
.reset_sysproxy .reset_sysproxy
@ -212,11 +188,19 @@ impl Sysopt {
self.access_guard().write().set_guard_type(GuardType::None); self.access_guard().write().set_guard_type(GuardType::None);
// 直接关闭所有代理 // 直接关闭所有代理
let (sys, auto) = {
let (sys, auto) = &mut *self.inner_proxy.write(); let (sys, auto) = &mut *self.inner_proxy.write();
sys.enable = false; sys.enable = false;
sys.set_system_proxy()?;
auto.enable = false; auto.enable = false;
(sys.clone(), auto.clone())
};
tokio::task::spawn_blocking(move || -> Result<()> {
sys.set_system_proxy()?;
auto.set_auto_proxy()?; auto.set_auto_proxy()?;
Ok(())
})
.await??;
Ok(()) Ok(())
} }

View File

@ -67,10 +67,14 @@ const SwitchRow = ({
const handleChange = (_: React.ChangeEvent, value: boolean) => { const handleChange = (_: React.ChangeEvent, value: boolean) => {
pendingRef.current = true pendingRef.current = true
setChecked(value) setChecked(value)
onToggle(value).catch((err: any) => { onToggle(value)
pendingRef.current = false .catch((err: any) => {
setChecked(active)
onError?.(err) onError?.(err)
}) })
.finally(() => {
pendingRef.current = false
})
} }
return ( return (