mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-29 18:12:10 +08:00
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.
260 lines
8.6 KiB
Rust
260 lines
8.6 KiB
Rust
use super::{CoreManager, RunningMode};
|
|
use crate::cmd::StringifyErr as _;
|
|
use crate::config::{Config, IVerge};
|
|
use crate::core::handle::Handle;
|
|
use crate::core::manager::CLASH_LOGGER;
|
|
use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
|
|
use anyhow::Result;
|
|
use clash_verge_logging::{Type, logging};
|
|
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;
|
|
defer! {
|
|
self.after_core_process();
|
|
}
|
|
|
|
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<()> {
|
|
CLASH_LOGGER.clear_logs().await;
|
|
defer! {
|
|
self.after_core_process();
|
|
}
|
|
|
|
match *self.get_running_mode() {
|
|
RunningMode::Service => self.stop_core_by_service().await,
|
|
RunningMode::Sidecar => {
|
|
self.stop_core_by_sidecar();
|
|
Ok(())
|
|
}
|
|
RunningMode::NotRunning => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub async fn restart_core(&self) -> Result<()> {
|
|
logging!(info, Type::Core, "Restarting core");
|
|
self.stop_core().await?;
|
|
self.start_core().await
|
|
}
|
|
|
|
pub async fn change_core(&self, clash_core: &String) -> Result<(), String> {
|
|
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
|
|
return Err(format!("Invalid clash core: {}", clash_core).into());
|
|
}
|
|
|
|
Config::verge().await.edit_draft(|d| {
|
|
d.clash_core = Some(clash_core.to_owned());
|
|
});
|
|
Config::verge().await.apply();
|
|
|
|
let verge_data = Config::verge().await.latest_arc();
|
|
verge_data.save_file().await.map_err(|e| e.to_string())?;
|
|
|
|
self.update_config_checked().await.stringify_err()?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn prepare_startup(&self) {
|
|
#[cfg(target_os = "windows")]
|
|
self.wait_for_service_if_needed().await;
|
|
self.set_running_mode(match SERVICE_MANAGER.current().await {
|
|
ServiceStatus::Ready => RunningMode::Service,
|
|
_ => RunningMode::Sidecar,
|
|
});
|
|
}
|
|
|
|
fn after_core_process(&self) {
|
|
let app_handle = Handle::app_handle();
|
|
tauri_plugin_clash_verge_sysinfo::set_app_core_mode(app_handle, self.get_running_mode().to_string());
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
async fn wait_for_service_if_needed(&self) {
|
|
use crate::{config::Config, constants::timing, core::service};
|
|
use backon::{ConstantBuilder, Retryable as _};
|
|
|
|
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
|
|
|
|
if !needs_service {
|
|
return;
|
|
}
|
|
|
|
let max_times = timing::SERVICE_WAIT_MAX.as_millis() / timing::SERVICE_WAIT_INTERVAL.as_millis();
|
|
let backoff = ConstantBuilder::default()
|
|
.with_delay(timing::SERVICE_WAIT_INTERVAL)
|
|
.with_max_times(max_times as usize);
|
|
|
|
let _ = (|| async {
|
|
if matches!(SERVICE_MANAGER.current().await, ServiceStatus::Ready) {
|
|
return Ok(());
|
|
}
|
|
|
|
// If the service IPC path is not ready yet, treat it as transient and retry.
|
|
// Running init/refresh too early can mark service state unavailable and break later config reloads.
|
|
if !service::is_service_ipc_path_exists() {
|
|
return Err(anyhow::anyhow!("Service IPC not ready"));
|
|
}
|
|
|
|
SERVICE_MANAGER.init().await?;
|
|
let _ = SERVICE_MANAGER.refresh().await;
|
|
|
|
if matches!(SERVICE_MANAGER.current().await, ServiceStatus::Ready) {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Service not ready"))
|
|
}
|
|
})
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|