diff --git a/Changelog.md b/Changelog.md index 6aeff12fc..3c1d7f858 100644 --- a/Changelog.md +++ b/Changelog.md @@ -21,6 +21,7 @@
🚀 优化改进 +- 更健壮的 service 生命周期管理 - 关闭 autofill 弹出窗口 diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index f5f40d6f6..1ff4e1496 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -25,10 +25,23 @@ pub mod timing { pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300); pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2); + // Windows 服务冷启动较慢,避免过早回退 sidecar。 #[cfg(target_os = "windows")] - pub const SERVICE_WAIT_MAX: Duration = Duration::from_millis(3000); + pub const SERVICE_WAIT_MAX: Duration = Duration::from_millis(30000); #[cfg(target_os = "windows")] pub const SERVICE_WAIT_INTERVAL: Duration = Duration::from_millis(200); + + // 回退 sidecar 后继续等待服务就绪并尝试交接。 + #[cfg(target_os = "windows")] + pub const SERVICE_HANDOFF_WINDOW: Duration = Duration::from_secs(120); + #[cfg(target_os = "windows")] + pub const SERVICE_HANDOFF_INTERVAL: Duration = Duration::from_secs(2); + + // 交接时等待 sidecar 释放 ext-controller 通道。 + #[cfg(target_os = "windows")] + pub const SERVICE_START_RETRIES: usize = 5; + #[cfg(target_os = "windows")] + pub const SERVICE_START_RETRY_DELAY: Duration = Duration::from_millis(300); } pub mod files { diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 49e439260..788c6c043 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -10,6 +10,17 @@ use scopeguard::defer; use smartstring::alias::String; use tauri_plugin_clash_verge_sysinfo; +/// sidecar→service 交接结果 +#[cfg(target_os = "windows")] +enum HandoffOutcome { + /// 服务尚未就绪 + NotReady, + /// 已完成或无需交接 + Done, + /// 交接失败并已回退 + Failed, +} + impl CoreManager { pub async fn start_core(&self) -> Result<()> { self.prepare_startup().await; @@ -17,10 +28,18 @@ impl CoreManager { self.after_core_process(); } - match *self.get_running_mode() { + let result = match *self.get_running_mode() { RunningMode::Service => self.start_core_by_service().await, RunningMode::NotRunning | RunningMode::Sidecar => self.start_core_by_sidecar().await, + }; + + // 回退 sidecar 后,后台等待服务就绪再交接 + #[cfg(target_os = "windows")] + if result.is_ok() && matches!(*self.get_running_mode(), RunningMode::Sidecar) { + self.spawn_service_handoff_watcher().await; } + + result } pub async fn stop_core(&self) -> Result<()> { @@ -115,4 +134,126 @@ impl CoreManager { .retry(backoff) .await; } + + /// 在窗口内等待服务就绪,再从 sidecar 交接到 service + #[cfg(target_os = "windows")] + async fn spawn_service_handoff_watcher(&self) { + use crate::constants::timing; + use crate::process::AsyncHandler; + use std::sync::atomic::Ordering; + use std::time::Instant; + + // 仅 TUN 模式需要服务交接 + let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false); + if !needs_service { + return; + } + + // 单实例,避免并发交接 + if self.handoff_watcher_running.swap(true, Ordering::AcqRel) { + return; + } + + logging!( + info, + Type::Core, + "service not ready at startup; sidecar active, watching for handoff" + ); + + AsyncHandler::spawn(|| async move { + let manager = Self::global(); + let started = Instant::now(); + loop { + if started.elapsed() >= timing::SERVICE_HANDOFF_WINDOW { + logging!( + info, + Type::Core, + "service handoff window elapsed; staying in sidecar mode" + ); + break; + } + tokio::time::sleep(timing::SERVICE_HANDOFF_INTERVAL).await; + + // 模式已变更时退出 + if !matches!(*manager.get_running_mode(), RunningMode::Sidecar) { + break; + } + match manager.try_handoff_sidecar_to_service().await { + // 已交接或无需交接 + HandoffOutcome::Done => break, + // 已回退 sidecar,停止重试 + HandoffOutcome::Failed => { + logging!(warn, Type::Core, "handoff attempt failed; staying in sidecar mode"); + break; + } + HandoffOutcome::NotReady => {} + } + } + manager.handoff_watcher_running.store(false, Ordering::Release); + }); + } + + /// 服务就绪后停止 sidecar,再以 service 重启内核 + #[cfg(target_os = "windows")] + async fn try_handoff_sidecar_to_service(&self) -> HandoffOutcome { + use crate::core::service; + + // 主动刷新服务状态,避免缓存状态阻止交接 + if !service::is_service_ipc_path_exists() { + return HandoffOutcome::NotReady; + } + if SERVICE_MANAGER.init().await.is_err() { + return HandoffOutcome::NotReady; + } + let _ = SERVICE_MANAGER.refresh().await; + if !matches!(SERVICE_MANAGER.current().await, ServiceStatus::Ready) { + return HandoffOutcome::NotReady; + } + + // 与配置更新/重启互斥 + if !self.try_start_config_update() { + return HandoffOutcome::NotReady; + } + defer! { + self.finish_config_update(); + } + + // 加锁后复检运行模式和 TUN 状态 + if !matches!(*self.get_running_mode(), RunningMode::Sidecar) + || !Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false) + { + return HandoffOutcome::Done; + } + + logging!( + info, + Type::Core, + "service became ready; handing off from sidecar to service" + ); + self.stop_core_by_sidecar(); + + match self.start_core_by_service().await { + Ok(()) => { + logging!(info, Type::Core, "handoff to service mode succeeded"); + HandoffOutcome::Done + } + Err(e) => { + logging!( + error, + Type::Core, + "handoff to service failed: {}; restarting sidecar", + e + ); + if let Err(e2) = self.start_core_by_sidecar().await { + logging!( + error, + Type::Core, + "failed to restart sidecar after handoff failure: {}", + e2 + ); + } + HandoffOutcome::Failed + } + } + } } diff --git a/src-tauri/src/core/manager/mod.rs b/src-tauri/src/core/manager/mod.rs index 18d63725c..940b4c5d4 100644 --- a/src-tauri/src/core/manager/mod.rs +++ b/src-tauri/src/core/manager/mod.rs @@ -42,6 +42,9 @@ pub struct CoreManager { state: ArcSwap, last_update: ArcSwapOption, config_update_in_progress: AtomicBool, + // sidecar→service 交接 watcher 单实例标志。 + #[cfg(target_os = "windows")] + handoff_watcher_running: AtomicBool, } #[derive(Debug)] @@ -65,6 +68,8 @@ impl Default for CoreManager { state: ArcSwap::new(Arc::new(State::default())), last_update: ArcSwapOption::new(None), config_update_in_progress: AtomicBool::new(false), + #[cfg(target_os = "windows")] + handoff_watcher_running: AtomicBool::new(false), } } } diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index eb4ad960d..3b105e280 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -109,9 +109,41 @@ impl CoreManager { pub(super) async fn start_core_by_service(&self) -> Result<()> { logging!(info, Type::Core, "Starting core in service mode"); let config_file = Config::generate_file(crate::config::ConfigType::Run).await?; - service::run_core_by_service(&config_file).await?; - self.set_running_mode(RunningMode::Service); - Ok(()) + + // 交接时等待 sidecar 释放 ext-controller 通道。 + #[cfg(target_os = "windows")] + { + use crate::constants::timing; + let mut last_err = None; + for attempt in 0..timing::SERVICE_START_RETRIES { + match service::run_core_by_service(&config_file).await { + Ok(()) => { + self.set_running_mode(RunningMode::Service); + return Ok(()); + } + Err(e) => { + logging!( + warn, + Type::Core, + "service start attempt {}/{} failed: {}", + attempt + 1, + timing::SERVICE_START_RETRIES, + e + ); + last_err = Some(e); + tokio::time::sleep(timing::SERVICE_START_RETRY_DELAY).await; + } + } + } + Err(last_err.unwrap_or_else(|| anyhow::anyhow!("service start failed"))) + } + + #[cfg(not(target_os = "windows"))] + { + service::run_core_by_service(&config_file).await?; + self.set_running_mode(RunningMode::Service); + Ok(()) + } } pub(super) async fn stop_core_by_service(&self) -> Result<()> { diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index e1e8ded0f..99d0b271e 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -142,6 +142,28 @@ const fn macos_cleanup_translocated_desired_state_shell() -> &'static str { "for f in '/var/root/.local/state/clash-verge-service/desired-state.json' '/var/lib/clash-verge-service/desired-state.json'; do if [ -f \"$f\" ] && /usr/bin/grep -q AppTranslocation \"$f\"; then backup=\"$f.apptranslocation.bak\"; if [ -e \"$backup\" ]; then backup=\"$f.apptranslocation.$(/bin/date +%s).bak\"; fi; /bin/mv \"$f\" \"$backup\"; fi; done" } +/// 卸载服务前以 root 清理残留 core 和 IPC 套接字。 +#[cfg(target_os = "macos")] +fn macos_force_stop_core_shell() -> String { + use crate::config::IVerge; + + // 只清理 root 拥有的服务内核。 + let mut parts: Vec = IVerge::VALID_CLASH_CORES + .iter() + .map(|core| format!("/usr/bin/pkill -U root -x {core} 2>/dev/null || true")) + .collect(); + + if let Ok(ipc) = dirs::ipc_path() + && let Ok(ipc_str) = dirs::path_to_str(&ipc) + { + // 转义单引号,避免破坏 shell 参数。 + let escaped = ipc_str.replace('\'', r"'\''"); + parts.push(format!("/bin/rm -f '{escaped}' 2>/dev/null || true")); + } + + parts.join("; ") +} + #[cfg(target_os = "macos")] fn escape_osascript_double_quoted_string(value: &str) -> String { value.replace('\\', "\\\\").replace('"', "\\\"") @@ -360,8 +382,11 @@ fn uninstall_service() -> Result<()> { // clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref()); let prompt = clash_verge_i18n::t!("service.adminUninstallPrompt"); - let command = - format!(r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#); + // 先清理服务残留,再执行卸载器。 + let uninstall_quoted = uninstall_shell.replace('\'', r"'\''"); + let shell = format!("{}; sudo '{uninstall_quoted}'", macos_force_stop_core_shell()); + let shell = escape_osascript_double_quoted_string(&shell); + let command = format!(r#"do shell script "{shell}" with administrator privileges with prompt "{prompt}""#); // logging!(debug, Type::Service, "uninstall command: {}", command);