From ca8e350694892e73e7b92ff1f9e63512bb366f8a Mon Sep 17 00:00:00 2001 From: Tunglies Date: Fri, 27 Mar 2026 23:40:03 +0800 Subject: [PATCH] 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 --- Changelog.md | 1 + src-tauri/src/core/sysopt.rs | 142 ++++++++---------- .../shared/proxy-control-switches.tsx | 12 +- 3 files changed, 72 insertions(+), 83 deletions(-) diff --git a/Changelog.md b/Changelog.md index 90490c0d7..6d1ebf066 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ ### 🐞 修复问题 - 修复系统代理关闭后在 PAC 模式下未完全关闭 +- 修复 macOS 开关代理时可能的卡死 ### ✨ 新增功能 diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 32e42fa40..b3d0a7a98 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -15,9 +15,10 @@ use std::{ time::Duration, }; use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy}; +use tokio::sync::Mutex as TokioMutex; pub struct Sysopt { - update_sysproxy: AtomicBool, + update_lock: TokioMutex<()>, reset_sysproxy: AtomicBool, inner_proxy: Arc>, guard: Arc>, @@ -26,7 +27,7 @@ pub struct Sysopt { impl Default for Sysopt { fn default() -> Self { Self { - update_sysproxy: AtomicBool::new(false), + update_lock: TokioMutex::new(()), reset_sysproxy: AtomicBool::new(false), inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), @@ -107,95 +108,70 @@ impl Sysopt { /// init the sysproxy pub async fn update_sysproxy(&self) -> Result<()> { - if self.update_sysproxy.load(Ordering::Acquire) { - 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 _lock = self.update_lock.lock().await; let verge = Config::verge().await.latest_arc(); - let port = { - let verge_port = verge.verge_mixed_port; - match verge_port { - Some(port) => port, - None => Config::clash().await.latest_arc().get_mixed_port(), - } + let port = match verge.verge_mixed_port { + Some(port) => port, + None => Config::clash().await.latest_arc().get_mixed_port(), }; let pac_port = IVerge::get_singleton_port(); - - let (sys_enable, pac_enable, proxy_host, proxy_guard) = { - ( - verge.enable_system_proxy.unwrap_or_default(), - verge.proxy_auto_config.unwrap_or_default(), - verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")), - verge.enable_proxy_guard.unwrap_or_default(), - ) - }; - + let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( + verge.enable_system_proxy.unwrap_or_default(), + verge.proxy_auto_config.unwrap_or_default(), + verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")), + verge.enable_proxy_guard.unwrap_or_default(), + ); // 先 await, 避免持有锁导致的 Send 问题 let bypass = get_bypass().await; - let (sys, auto) = &mut *self.inner_proxy.write(); - sys.enable = false; - sys.host = proxy_host.clone().into(); - sys.port = port; - sys.bypass = bypass.into(); + let (sys, auto, guard_type) = { + let (sys, auto) = &mut *self.inner_proxy.write(); + sys.host = proxy_host.clone().into(); + sys.port = port; + sys.bypass = bypass.into(); + auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); - auto.enable = false; - auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); + // `enable_system_proxy` is the master switch. + // When disabled, force clear both global proxy and PAC at OS level. + let guard_type = if !sys_enable { + sys.enable = false; + auto.enable = false; + GuardType::None + } else if pac_enable { + sys.enable = false; + auto.enable = true; + if proxy_guard { + GuardType::Autoproxy(auto.clone()) + } else { + GuardType::None + } + } else { + sys.enable = true; + auto.enable = false; + if proxy_guard { + GuardType::Sysproxy(sys.clone()) + } else { + GuardType::None + } + }; - self.access_guard().write().set_guard_type(GuardType::None); + (sys.clone(), auto.clone(), guard_type) + }; - // `enable_system_proxy` is the master switch. - // When disabled, force clear both global proxy and PAC at OS level. - if !sys_enable { + self.access_guard().write().set_guard_type(guard_type); + + tokio::task::spawn_blocking(move || -> Result<()> { sys.set_system_proxy()?; auto.set_auto_proxy()?; - return Ok(()); - } - - if pac_enable { - sys.enable = false; - auto.enable = true; - sys.set_system_proxy()?; - auto.set_auto_proxy()?; - if proxy_guard { - self.access_guard() - .write() - .set_guard_type(GuardType::Autoproxy(auto.clone())); - } - return Ok(()); - } - - if sys_enable { - auto.enable = false; - sys.enable = true; - auto.set_auto_proxy()?; - sys.set_system_proxy()?; - if proxy_guard { - self.access_guard() - .write() - .set_guard_type(GuardType::Sysproxy(sys.clone())); - } - return Ok(()); - } + Ok(()) + }) + .await??; Ok(()) } /// reset the sysproxy - #[allow(clippy::unused_async)] pub async fn reset_sysproxy(&self) -> Result<()> { if self .reset_sysproxy @@ -212,11 +188,19 @@ impl Sysopt { self.access_guard().write().set_guard_type(GuardType::None); // 直接关闭所有代理 - let (sys, auto) = &mut *self.inner_proxy.write(); - sys.enable = false; - sys.set_system_proxy()?; - auto.enable = false; - auto.set_auto_proxy()?; + let (sys, auto) = { + let (sys, auto) = &mut *self.inner_proxy.write(); + sys.enable = false; + auto.enable = false; + (sys.clone(), auto.clone()) + }; + + tokio::task::spawn_blocking(move || -> Result<()> { + sys.set_system_proxy()?; + auto.set_auto_proxy()?; + Ok(()) + }) + .await??; Ok(()) } diff --git a/src/components/shared/proxy-control-switches.tsx b/src/components/shared/proxy-control-switches.tsx index 9210c8703..50c750299 100644 --- a/src/components/shared/proxy-control-switches.tsx +++ b/src/components/shared/proxy-control-switches.tsx @@ -67,10 +67,14 @@ const SwitchRow = ({ const handleChange = (_: React.ChangeEvent, value: boolean) => { pendingRef.current = true setChecked(value) - onToggle(value).catch((err: any) => { - pendingRef.current = false - onError?.(err) - }) + onToggle(value) + .catch((err: any) => { + setChecked(active) + onError?.(err) + }) + .finally(() => { + pendingRef.current = false + }) } return (