mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-06 17:12:07 +08:00
fix(core): improve service startup and cleanup reliability
Wait longer for slow Windows service startup, then hand off from sidecar to service when it becomes ready. Retry service core startup during the handoff window, and clean up root-owned macOS service core/IPC leftovers before uninstalling the service.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
- 更健壮的 service 生命周期管理
|
||||
|
||||
- 关闭 autofill 弹出窗口
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ pub struct CoreManager {
|
||||
state: ArcSwap<State>,
|
||||
last_update: ArcSwapOption<Instant>,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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<String> = 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user