From 272c09568cc0ab206f926a012cba581cf915db9f Mon Sep 17 00:00:00 2001 From: wonfen Date: Mon, 24 Aug 2026 22:30:42 +0800 Subject: [PATCH] fix(sysproxy): retry a transient system proxy write on Windows --- src-tauri/src/core/proxy_control.rs | 56 +++++++++++++++++++++++++-- src-tauri/src/feat/listener.rs | 2 +- src/locales/ar/settings.json | 3 +- src/locales/de/settings.json | 3 +- src/locales/en/settings.json | 3 +- src/locales/es/settings.json | 3 +- src/locales/fa/settings.json | 3 +- src/locales/id/settings.json | 3 +- src/locales/jp/settings.json | 3 +- src/locales/ko/settings.json | 3 +- src/locales/ru/settings.json | 3 +- src/locales/tr/settings.json | 3 +- src/locales/tt/settings.json | 3 +- src/locales/zh/settings.json | 3 +- src/locales/zhtw/settings.json | 3 +- src/services/notice-service.ts | 2 + src/types/generated/i18n-keys.ts | 1 + src/types/generated/i18n-resources.ts | 1 + 18 files changed, 83 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/core/proxy_control.rs b/src-tauri/src/core/proxy_control.rs index f6f11e30d..3be5d3873 100644 --- a/src-tauri/src/core/proxy_control.rs +++ b/src-tauri/src/core/proxy_control.rs @@ -25,6 +25,9 @@ use std::{ }; use tokio::{sync::Mutex, time::timeout}; +const CLEAR_ATTEMPTS: u32 = 3; +const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100); + /// Actionable system-proxy failure attached to an `anyhow` chain. /// /// Classification remains downcastable while the original error stays available for diagnostics. @@ -40,6 +43,8 @@ pub enum SysproxyFailure { GuardStopped { detail: String }, /// The Core was not ready, so enabling the system proxy was refused. CoreNotReady, + /// A Windows system call refused the write, usually a transient RPC hiccup. + SystemCallFailed, } impl SysproxyFailure { @@ -51,6 +56,7 @@ impl SysproxyFailure { Self::SidecarWhileServiceReady => "SYSPROXY_SIDECAR_WHILE_SERVICE_READY", Self::GuardStopped { .. } => "SYSPROXY_GUARD_STOPPED", Self::CoreNotReady => "SYSPROXY_CORE_NOT_READY", + Self::SystemCallFailed => "SYSPROXY_SYSTEM_CALL_FAILED", } } @@ -74,6 +80,7 @@ impl std::fmt::Display for SysproxyFailure { write!(f, "system proxy guard stopped after repeated failures: {detail}") } Self::CoreNotReady => f.write_str("the core is not ready, so the system proxy was not enabled"), + Self::SystemCallFailed => f.write_str("a Windows system call failed while writing the system proxy"), } } } @@ -121,10 +128,28 @@ pub fn rollback_failure(caused_by: anyhow::Error, rollback: anyhow::Error) -> an } fn classify_local_failure(error: anyhow::Error) -> anyhow::Error { - if !was_refused_locally(&error) { - return error; + if was_refused_locally(&error) { + return error.context(SysproxyFailure::PrivilegeRequired); } - error.context(SysproxyFailure::PrivilegeRequired) + if was_a_failed_system_call(&error) { + return error.context(SysproxyFailure::SystemCallFailed); + } + error +} + +#[cfg(target_os = "windows")] +fn was_a_failed_system_call(error: &anyhow::Error) -> bool { + error.chain().any(|cause| { + matches!( + cause.downcast_ref::(), + Some(sysproxy::Error::SystemCall(_)) + ) + }) +} + +#[cfg(not(target_os = "windows"))] +const fn was_a_failed_system_call(_error: &anyhow::Error) -> bool { + false } fn was_refused_locally(error: &anyhow::Error) -> bool { @@ -447,7 +472,7 @@ pub async fn apply() -> Result<()> { } pub async fn clear() -> Result<()> { - let result = clear_inner().await; + let result = clear_with_retry().await; // Clear failures must be filed even when a restart aborts before apply. settle_table( clear_table_effect(&result), @@ -459,6 +484,29 @@ pub async fn clear() -> Result<()> { result } +/// A failed system call is usually a transient RPC hiccup, so give it a few tries. +async fn clear_with_retry() -> Result<()> { + for _ in 1..CLEAR_ATTEMPTS { + match clear_inner().await { + Err(error) + if matches!( + SysproxyFailure::from_chain(&error), + Some(SysproxyFailure::SystemCallFailed) + ) => + { + logging!( + warn, + Type::Core, + "clearing the system proxy failed; retrying: {error:#}" + ); + tokio::time::sleep(CLEAR_RETRY_DELAY).await; + } + other => return other, + } + } + clear_inner().await +} + async fn clear_inner() -> Result<()> { let running_mode = CoreManager::global().get_running_mode(); match proxy_backend_route(cfg!(target_os = "macos"), &running_mode) { diff --git a/src-tauri/src/feat/listener.rs b/src-tauri/src/feat/listener.rs index cfb905501..9abb6699b 100644 --- a/src-tauri/src/feat/listener.rs +++ b/src-tauri/src/feat/listener.rs @@ -108,7 +108,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result> = { 'settings.feedback.errors.sysproxy.sidecarWhileServiceReady', SYSPROXY_PRIVILEGE_REQUIRED: 'settings.feedback.errors.sysproxy.privilegeRequired', + SYSPROXY_SYSTEM_CALL_FAILED: + 'settings.feedback.errors.sysproxy.systemCallFailed', } let nextId = 0 diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index d34826676..090f3428b 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -780,6 +780,7 @@ export const translationKeys = [ 'settings.feedback.errors.sysproxy.guardStopped', 'settings.feedback.errors.sysproxy.sidecarWhileServiceReady', 'settings.feedback.errors.sysproxy.coreNotReady', + 'settings.feedback.errors.sysproxy.systemCallFailed', 'settings.feedback.notifications.clash.restartSuccess', 'settings.feedback.notifications.clash.versionUpdated', 'settings.feedback.notifications.clash.alreadyLatestVersion', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 2063308b8..7026825f4 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -817,6 +817,7 @@ export interface TranslationResources { guardStopped: string privilegeRequired: string sidecarWhileServiceReady: string + systemCallFailed: string } } notifications: {