mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
fix(windows): fall back to sidecar after confirmed service failure
This commit is contained in:
@@ -105,6 +105,7 @@ windows-sys = { version = "0.61.2", features = [
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_JobObjects",
|
||||
] }
|
||||
# *********************************
|
||||
|
||||
@@ -186,39 +186,42 @@ where
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Fallback, FallbackFuture>(
|
||||
is_admin: bool,
|
||||
async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, CheckFuture, Fallback, FallbackFuture>(
|
||||
start_service: Start,
|
||||
confirm_idle: Check,
|
||||
start_sidecar: Fallback,
|
||||
) -> Result<()>
|
||||
where
|
||||
Start: FnOnce() -> StartFuture,
|
||||
StartFuture: std::future::Future<Output = Result<()>>,
|
||||
Check: FnOnce(bool) -> CheckFuture,
|
||||
CheckFuture: std::future::Future<Output = Result<()>>,
|
||||
Fallback: FnOnce(anyhow::Error) -> FallbackFuture,
|
||||
FallbackFuture: std::future::Future<Output = Result<()>>,
|
||||
{
|
||||
let Err(error) = start_service().await else {
|
||||
return Ok(());
|
||||
};
|
||||
// A transport failure cannot establish whether the service already started the core.
|
||||
let location_refused = error
|
||||
.downcast_ref::<crate::core::service::ServiceStartRefusal>()
|
||||
.is_some_and(|refusal| {
|
||||
refusal.code == clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16
|
||||
});
|
||||
if !is_admin || !location_refused {
|
||||
let refusal = error.downcast_ref::<crate::core::service::ServiceStartRefusal>();
|
||||
let location_refused = refusal.is_some_and(|refusal| {
|
||||
refusal.code == clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16
|
||||
});
|
||||
if refusal.is_some() && !location_refused {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// A lost response is not a refusal: require a stopped service and no leftover core.
|
||||
if let Err(probe_error) = confirm_idle(location_refused).await {
|
||||
return Err(error.context(format!("Sidecar fallback was not safe: {probe_error:#}")));
|
||||
}
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"service refused the core location while app is elevated; falling back to sidecar: {error:#}"
|
||||
"Service could not start the core; falling back to Sidecar: {error:#}"
|
||||
);
|
||||
let reason = format!("{error:#}");
|
||||
start_sidecar(error)
|
||||
.await
|
||||
.map_err(|error| error.context(format!("sidecar fallback failed after service refusal: {reason}")))
|
||||
.map_err(|error| error.context(format!("Sidecar fallback failed after Service failure: {reason}")))
|
||||
}
|
||||
|
||||
async fn run_sidecar_termination_transition<Clear, ClearFuture, Terminate>(
|
||||
@@ -578,6 +581,8 @@ impl CoreManager {
|
||||
tracing::Span::current().record("decision", tracing::field::debug(&startup));
|
||||
if matches!(startup, StartupDecision::Wait) {
|
||||
self.rollback_failed_start().await;
|
||||
#[cfg(target_os = "windows")]
|
||||
self.try_sidecar_after_service_unavailable().await?;
|
||||
return Ok(());
|
||||
}
|
||||
if Handle::global().is_exiting() {
|
||||
@@ -590,20 +595,9 @@ impl CoreManager {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
run_service_start_with_sidecar_fallback(
|
||||
crate::core::runstate::RUN_STATE.state().is_admin,
|
||||
|| self.start_core_by_service(),
|
||||
|error| async move {
|
||||
use crate::core::runstate::RUN_STATE;
|
||||
|
||||
RUN_STATE.allow_sidecar_after_service_refusal(format!("{error:#}"))?;
|
||||
let result = self.start_core_by_sidecar().await;
|
||||
if result.is_err() {
|
||||
SERVICE_MANAGER.withdraw_sidecar_allowance();
|
||||
} else {
|
||||
crate::core::service::notify_service_fallback();
|
||||
}
|
||||
result
|
||||
},
|
||||
crate::core::service::windows_fallback::confirm_idle,
|
||||
|error| self.start_sidecar_after_service_failure(error),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -628,6 +622,50 @@ impl CoreManager {
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn try_sidecar_after_service_unavailable(&self) -> Result<()> {
|
||||
use crate::core::runstate::{RUN_STATE, ServiceHealth};
|
||||
|
||||
let state = RUN_STATE.state();
|
||||
if state.pending.is_some() || state.op_in_flight || Handle::global().is_exiting() {
|
||||
return Ok(());
|
||||
}
|
||||
let reason = match state.health {
|
||||
ServiceHealth::Unavailable(reason) => reason,
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if let Err(error) = crate::core::service::windows_fallback::confirm_idle(false).await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Service unavailable ({reason}); Sidecar fallback withheld: {error:#}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
self.start_sidecar_after_service_failure(anyhow::anyhow!(reason)).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn start_sidecar_after_service_failure(&self, error: anyhow::Error) -> Result<()> {
|
||||
use crate::core::runstate::RUN_STATE;
|
||||
|
||||
if Handle::global().is_exiting() {
|
||||
return Err(error.context("application exited before Sidecar fallback"));
|
||||
}
|
||||
let reason = format!("{error:#}");
|
||||
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
|
||||
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
|
||||
let result = self.start_core_by_sidecar().await;
|
||||
if result.is_err() {
|
||||
SERVICE_MANAGER.withdraw_sidecar_allowance();
|
||||
self.rollback_failed_start().await;
|
||||
} else {
|
||||
crate::core::service::notify_service_fallback();
|
||||
}
|
||||
result.map_err(|error| error.context(format!("Service failure before Sidecar startup: {reason}")))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(mode = ?*self.get_running_mode()))]
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
|
||||
@@ -16,6 +16,23 @@ use std::path::Path;
|
||||
use tauri_plugin_mihomo::MihomoExt as _;
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
|
||||
let tun = config
|
||||
.entry(Value::from("tun"))
|
||||
.or_insert_with(|| Value::Mapping(Mapping::new()));
|
||||
if !tun.is_mapping() {
|
||||
*tun = Value::Mapping(Mapping::new());
|
||||
}
|
||||
tun.as_mapping_mut()
|
||||
.context("invalid Sidecar TUN configuration")?
|
||||
.insert(Value::from("enable"), Value::Bool(false));
|
||||
Ok(serde_yaml_ng::to_string(&config)?)
|
||||
}
|
||||
|
||||
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -142,6 +159,15 @@ impl CoreManager {
|
||||
handle::Handle::app_handle()
|
||||
.mihomo()
|
||||
.update_socket_path(dirs::path_to_str(&sidecar_ipc)?.to_owned())?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let config_file = if crate::core::runstate::RUN_STATE.state().is_admin {
|
||||
Config::generate_file().await?
|
||||
} else {
|
||||
// Reconciliation persists the preference later; the first spawn must already have TUN off.
|
||||
let yaml = sidecar_config_without_tun(&Config::runtime_config_yaml().await?)?;
|
||||
Config::write_runtime_file(&yaml).await?
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let config_file = Config::generate_file().await?;
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
|
||||
|
||||
@@ -37,6 +37,9 @@ use std::{
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod windows_fallback;
|
||||
|
||||
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
static ACTIVE_SERVICE_SESSION: Lazy<Mutex<Option<ActiveServiceSession>>> = Lazy::new(|| Mutex::new(None));
|
||||
static PENDING_SERVICE_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle};
|
||||
use windows_service::{
|
||||
Error as WindowsServiceError,
|
||||
service::{ServiceAccess, ServiceState},
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{ERROR_NO_MORE_FILES, ERROR_SERVICE_DOES_NOT_EXIST, INVALID_HANDLE_VALUE},
|
||||
System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) async fn confirm_idle(location_refused: bool) -> Result<()> {
|
||||
if location_refused {
|
||||
let credentials = crate::core::owner_identity::current_owner_credentials()?;
|
||||
let response = clash_verge_service_ipc::get_status(&credentials).await?;
|
||||
if response.code != 0 {
|
||||
bail!(
|
||||
"cannot confirm idle service: code {} ({})",
|
||||
response.code,
|
||||
response.message
|
||||
);
|
||||
}
|
||||
let status = response.data.context("service did not return its status")?;
|
||||
if status.is_active
|
||||
|| status.core_pid.is_some()
|
||||
|| status.service_state != clash_verge_service_ipc::ServiceLifecycleState::Running
|
||||
{
|
||||
bail!("service still has an active or recovering core session");
|
||||
}
|
||||
}
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !location_refused {
|
||||
require_stopped_service()?;
|
||||
}
|
||||
require_no_core_process(!location_refused)?;
|
||||
if !location_refused {
|
||||
require_stopped_service()?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("service fallback probe did not finish")?
|
||||
}
|
||||
|
||||
fn require_stopped_service() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
|
||||
match manager.open_service(
|
||||
clash_verge_service_ipc::WINDOWS_SERVICE_NAME,
|
||||
ServiceAccess::QUERY_STATUS,
|
||||
) {
|
||||
Ok(service) => {
|
||||
let status = service.query_status()?;
|
||||
if status.current_state != ServiceState::Stopped || status.process_id.is_some_and(|pid| pid != 0) {
|
||||
bail!(
|
||||
"service is {:?}; Sidecar fallback requires a stopped service",
|
||||
status.current_state
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(WindowsServiceError::Winapi(error))
|
||||
if error.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => Err(error).context("cannot confirm that the service has stopped"),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_no_core_process(include_service: bool) -> Result<()> {
|
||||
let raw = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
||||
if raw == INVALID_HANDLE_VALUE {
|
||||
return Err(std::io::Error::last_os_error()).context("cannot inspect existing core processes");
|
||||
}
|
||||
let snapshot = unsafe { OwnedHandle::from_raw_handle(raw) };
|
||||
let mut entry = PROCESSENTRY32W {
|
||||
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
||||
..unsafe { std::mem::zeroed() }
|
||||
};
|
||||
let mut found = unsafe { Process32FirstW(snapshot.as_raw_handle(), &mut entry) };
|
||||
while found != 0 {
|
||||
let length = entry
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|value| *value == 0)
|
||||
.unwrap_or(entry.szExeFile.len());
|
||||
let name = String::from_utf16_lossy(&entry.szExeFile[..length]);
|
||||
// SCM can be stopped while a core from an earlier service process remains alive.
|
||||
if crate::config::IVerge::VALID_CLASH_CORES
|
||||
.iter()
|
||||
.any(|core| name.eq_ignore_ascii_case(&format!("{core}.exe")))
|
||||
|| (include_service && name.eq_ignore_ascii_case("clash-verge-service.exe"))
|
||||
{
|
||||
bail!(
|
||||
"process {name} (PID {}) is still running; refusing a second core",
|
||||
entry.th32ProcessID
|
||||
);
|
||||
}
|
||||
found = unsafe { Process32NextW(snapshot.as_raw_handle(), &mut entry) };
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.raw_os_error() != Some(ERROR_NO_MORE_FILES as i32) {
|
||||
return Err(error).context("core process enumeration did not complete");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "تم تثبيت الخدمة بنجاح",
|
||||
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة بسبب أذونات المسار. تم التبديل إلى وضع المسؤول لمواصلة التشغيل.",
|
||||
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة. تم التبديل إلى وضع Sidecar؛ ولا تزال الخدمة غير متاحة.",
|
||||
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Service erfolgreich installiert",
|
||||
"sidecarFallback": "Der Dienst konnte den Kern aufgrund der Pfadberechtigungen nicht starten. Die Anwendung läuft im Administratormodus weiter.",
|
||||
"sidecarFallback": "Der Dienst konnte den Kern nicht starten. Die Anwendung läuft im Sidecar-Modus weiter; der Dienst bleibt nicht verfügbar.",
|
||||
"uninstallSuccess": "Service erfolgreich deinstalliert"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Service Installed Successfully",
|
||||
"sidecarFallback": "The service could not start the core due to path permissions. Switched to administrator mode to continue running.",
|
||||
"sidecarFallback": "The service could not start the core. Switched to Sidecar mode; the service remains unavailable.",
|
||||
"uninstallSuccess": "Service Uninstalled Successfully"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Servicio instalado con éxito",
|
||||
"sidecarFallback": "El servicio no pudo iniciar el núcleo debido a los permisos de la ruta. Se ha cambiado al modo administrador para continuar.",
|
||||
"sidecarFallback": "El servicio no pudo iniciar el núcleo. Se ha cambiado al modo Sidecar; el servicio sigue sin estar disponible.",
|
||||
"uninstallSuccess": "Servicio desinstalado con éxito"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "سرویس با موفقیت نصب شد",
|
||||
"sidecarFallback": "سرویس بهدلیل مجوزهای مسیر نتوانست هسته را اجرا کند. برای ادامهٔ اجرا به حالت مدیر تغییر یافت.",
|
||||
"sidecarFallback": "سرویس نتوانست هسته را اجرا کند. اجرا در حالت Sidecar ادامه مییابد؛ سرویس همچنان در دسترس نیست.",
|
||||
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Layanan Berhasil Diinstal",
|
||||
"sidecarFallback": "Layanan tidak dapat memulai inti karena izin jalur. Beralih ke mode administrator untuk melanjutkan.",
|
||||
"sidecarFallback": "Layanan tidak dapat memulai inti. Beralih ke mode Sidecar; layanan masih tidak tersedia.",
|
||||
"uninstallSuccess": "Layanan Berhasil Dicopot"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "サービスのインストールに成功しました。",
|
||||
"sidecarFallback": "パスの権限によりサービスがコアを起動できなかったため、管理者モードに切り替えて動作を継続しています。",
|
||||
"sidecarFallback": "サービスでコアを起動できなかったため、Sidecar モードで動作を継続しています。サービスは引き続き利用できません。",
|
||||
"uninstallSuccess": "サービスのアンインストールに成功しました。"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "서비스가 설치되었습니다",
|
||||
"sidecarFallback": "경로 권한 문제로 서비스가 코어를 시작하지 못해 관리자 모드로 전환하여 계속 실행합니다.",
|
||||
"sidecarFallback": "서비스가 코어를 시작하지 못해 Sidecar 모드로 계속 실행합니다. 서비스는 여전히 사용할 수 없습니다.",
|
||||
"uninstallSuccess": "서비스가 제거되었습니다"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Служба успешно установлена",
|
||||
"sidecarFallback": "Служба не смогла запустить ядро из-за прав доступа к пути. Работа продолжена в режиме администратора.",
|
||||
"sidecarFallback": "Служба не смогла запустить ядро. Работа продолжена в режиме Sidecar; служба по-прежнему недоступна.",
|
||||
"uninstallSuccess": "Служба успешно удалена"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Hizmet Başarıyla Kuruldu",
|
||||
"sidecarFallback": "Hizmet, yol izinleri nedeniyle çekirdeği başlatamadı. Çalışmaya devam etmek için yönetici moduna geçildi.",
|
||||
"sidecarFallback": "Hizmet çekirdeği başlatamadı. Sidecar moduna geçildi; hizmet hâlâ kullanılamıyor.",
|
||||
"uninstallSuccess": "Hizmet Başarıyla Kaldırıldı"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "Сервис уңышлы урнаштырылды",
|
||||
"sidecarFallback": "Хезмәт юлга керү хокуклары аркасында төшне эшләтеп җибәрә алмады. Эшне дәвам итү өчен администратор режимына күчелде.",
|
||||
"sidecarFallback": "Хезмәт төшне эшләтеп җибәрә алмады. Эш Sidecar режимында дәвам итә; хезмәт һаман да эшләми.",
|
||||
"uninstallSuccess": "Сервис уңышлы салдырылды"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "已成功安装服务",
|
||||
"sidecarFallback": "服务因路径权限问题无法启动内核,已切回管理员模式继续运行。",
|
||||
"sidecarFallback": "服务无法启动内核,已降级为 Sidecar 模式运行,服务仍不可用。",
|
||||
"uninstallSuccess": "已成功卸载服务"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"clashService": {
|
||||
"installSuccess": "已成功安裝服務",
|
||||
"sidecarFallback": "服務因路徑權限問題無法啟動核心,已切回管理員模式繼續執行。",
|
||||
"sidecarFallback": "服務無法啟動核心,已降級為 Sidecar 模式執行,服務仍無法使用。",
|
||||
"uninstallSuccess": "已成功解除安裝服務"
|
||||
},
|
||||
"updater": {
|
||||
|
||||
Reference in New Issue
Block a user