fix(service): stop waiting on a service that no longer starts with Windows

When the service's start type was changed from automatic, startup waited
about two minutes for it before falling back to Sidecar. Detect this
immediately, skip the TUN readiness wait, and offer a reinstall that
restores automatic start.

Refs #8129
This commit is contained in:
wonfen
2026-10-03 20:40:51 +08:00
parent 4a0882fc18
commit 57c5484080
23 changed files with 64 additions and 12 deletions
+1
View File
@@ -8,6 +8,7 @@
**🖥️ Windows**
- 修复 Windows 升级后因文件权限异常无法启动、需要手动清理配置的问题
- 修复 Windows 服务启动类型被改为手动后,软件启动卡住约两分钟的问题,并提供一键修复
</details>
+6 -1
View File
@@ -823,7 +823,12 @@ impl CoreManager {
return;
}
if crate::core::runstate::RUN_STATE.state().service_usable() {
let state = crate::core::runstate::RUN_STATE.state();
// Waiting cannot bring up a service nothing will start.
if state.service_usable()
|| matches!(&state.health, crate::core::runstate::ServiceHealth::Unavailable(reason)
if crate::core::service::is_not_auto_started(reason))
{
return;
}
+2 -2
View File
@@ -29,11 +29,11 @@ impl RunStateEnv for RealEnv {
async fn probe_service_version(&self) -> Result<ServiceVersionReply> {
// The IPC client would otherwise back off for about two minutes before failing.
#[cfg(windows)]
if tokio::task::spawn_blocking(crate::core::service::service_stopped)
if let Some(reason) = tokio::task::spawn_blocking(crate::core::service::service_stop_reason)
.await
.context("service status probe did not finish")??
{
anyhow::bail!("the Windows service is not running");
anyhow::bail!(reason);
}
let response = clash_verge_service_ipc::get_version().await?;
let core = if response.code == 0
+32 -8
View File
@@ -49,6 +49,15 @@ static PENDING_SERVICE_OWNER_NOTICE: Mutex<Option<String>> = Mutex::new(None);
pub enum ServiceFallbackNotice {
Unavailable,
CoreRejected(String),
NotAutoStarted,
}
/// The release installer registers AutoStart.
const SERVICE_NOT_AUTO_STARTED: &str = "the Windows service is stopped and no longer starts with Windows";
pub(crate) fn is_not_auto_started(reason: &str) -> bool {
// Detection prefixes its own context to the reason.
reason.contains(SERVICE_NOT_AUTO_STARTED)
}
#[cfg(target_os = "windows")]
@@ -61,6 +70,8 @@ pub(crate) fn take_service_fallback_notice() -> Option<ServiceFallbackNotice> {
let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?;
Some(if reason.starts_with(CORE_REJECTED_PREFIX) {
ServiceFallbackNotice::CoreRejected(reason)
} else if is_not_auto_started(&reason) {
ServiceFallbackNotice::NotAutoStarted
} else {
ServiceFallbackNotice::Unavailable
})
@@ -266,7 +277,7 @@ fn open_registered_service() -> Result<Option<windows_service::service::Service>
let manager = WindowsServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
match manager.open_service(
clash_verge_service_ipc::WINDOWS_SERVICE_NAME,
ServiceAccess::QUERY_STATUS,
ServiceAccess::QUERY_STATUS | ServiceAccess::QUERY_CONFIG,
) {
Ok(service) => Ok(Some(service)),
Err(WindowsServiceError::Winapi(error)) if error.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST) => {
@@ -281,21 +292,34 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
Ok(open_registered_service()?.is_some())
}
/// Whether IPC cannot succeed until the service is started again. A service that is starting, or
/// that the SCM has not started yet this boot, is left to the IPC retries, which wait for it.
/// Why IPC cannot succeed until the service is started again, if it cannot. A starting service,
/// or an AutoStart one the SCM has yet to start this boot, is left to the IPC retries.
#[cfg(windows)]
pub(crate) fn service_stopped() -> Result<bool> {
use windows_service::service::{ServiceExitCode, ServiceState};
pub(crate) fn service_stop_reason() -> Result<Option<&'static str>> {
use windows_service::service::{ServiceExitCode, ServiceStartType, ServiceState};
const ERROR_SERVICE_NEVER_STARTED: u32 = 1077;
const NOT_RUNNING: &str = "the Windows service is not running";
let Some(service) = open_registered_service()? else {
return Ok(true);
return Ok(Some(NOT_RUNNING));
};
let status = service
.query_status()
.context("failed to query Windows service status")?;
Ok(status.current_state == ServiceState::Stopped
&& status.exit_code != ServiceExitCode::Win32(ERROR_SERVICE_NEVER_STARTED))
if status.current_state != ServiceState::Stopped {
return Ok(None);
}
// The development channel registers an on-demand start.
if !cfg!(feature = "verge-dev")
&& service
.query_config()
.context("failed to query Windows service configuration")?
.start_type
!= ServiceStartType::AutoStart
{
return Ok(Some(SERVICE_NOT_AUTO_STARTED));
}
Ok((status.exit_code != ServiceExitCode::Win32(ERROR_SERVICE_NEVER_STARTED)).then_some(NOT_RUNNING))
}
#[cfg(target_os = "linux")]
@@ -27,7 +27,7 @@ import getSystem from '@/utils/get-system'
type Remedy = 'installAndRestart' | 'reinstallAndRestart' | 'restartOnly'
const remedyFor = (reason: ServiceRequestReason): Remedy =>
reason === 'serviceLocationRefused'
reason === 'serviceLocationRefused' || reason === 'serviceNotAutoStarted'
? 'reinstallAndRestart'
: reason === 'sysproxySidecarReady'
? 'restartOnly'
@@ -40,6 +40,8 @@ const EXPLANATION = {
tunNeedsService: 'layout.components.sysproxyPrivilege.tunMessage',
serviceLocationRefused:
'layout.components.serviceMigration.locationRefusedMessage',
serviceNotAutoStarted:
'layout.components.serviceMigration.notAutoStartedMessage',
} as const
const TITLE = {
@@ -47,6 +49,7 @@ const TITLE = {
sysproxySidecarReady: 'layout.components.sysproxyPrivilege.title',
tunNeedsService: 'layout.components.sysproxyPrivilege.tunTitle',
serviceLocationRefused: 'layout.components.serviceMigration.repair',
serviceNotAutoStarted: 'layout.components.serviceMigration.repair',
} as const
const stateToRestore = (
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "لا يمكن للخدمة تشغيل النواة لأن مسار التثبيت أو أذونات المجلد لم تجتز فحص الأمان. أعد تثبيت الخدمة لاستعادة ملفاتها وأذوناتها، ثم حاول مجددًا. قد يطلب Windows موافقة المسؤول.",
"notAutoStartedMessage": "تم تغيير نوع بدء تشغيل خدمة Clash Verge إلى «يدوي» أو «معطّل» (غالبًا بواسطة أدوات التحسين)، لذلك لم تعد تبدأ مع Windows؛ تستخدم هذه الجلسة Sidecar. أعد تثبيت الخدمة لاستعادة التشغيل التلقائي. قد يطلب Windows موافقة المسؤول.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "Der Dienst kann den Kern nicht starten, da sein Installationspfad oder die Verzeichnisberechtigungen die Sicherheitsprüfung nicht bestanden haben. Installieren Sie den Dienst erneut, um Dateien und Berechtigungen wiederherzustellen, und versuchen Sie es erneut. Windows kann eine Administratorbestätigung anfordern.",
"notAutoStartedMessage": "Der Starttyp des Clash-Verge-Dienstes wurde auf „Manuell“ oder „Deaktiviert“ geändert (häufig durch Optimierungsprogramme), daher startet er nicht mehr mit Windows; diese Sitzung verwendet Sidecar. Installieren Sie den Dienst erneut, um den automatischen Start wiederherzustellen. Windows kann eine Administratorbestätigung anfordern.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "The service cannot start the core because its installation path or directory permissions failed a security check. Reinstall the service to restore its files and permissions, then try again. Windows may ask for administrator approval.",
"notAutoStartedMessage": "The Clash Verge service startup type was changed to Manual or Disabled (often by system optimizer tools), so it no longer starts with Windows; this session is using Sidecar. Reinstall the service to restore automatic startup. Windows may ask for administrator approval.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "El servicio no puede iniciar el núcleo porque su ruta de instalación o los permisos del directorio no superaron una comprobación de seguridad. Reinstale el servicio para restaurar sus archivos y permisos e inténtelo de nuevo. Windows puede solicitar autorización de administrador.",
"notAutoStartedMessage": "El tipo de inicio del servicio de Clash Verge se cambió a Manual o Deshabilitado (a menudo por herramientas de optimización), por lo que ya no se inicia con Windows; esta sesión usa Sidecar. Reinstale el servicio para restaurar el inicio automático. Windows puede solicitar autorización de administrador.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "سرویس نمی‌تواند هسته را اجرا کند، زیرا مسیر نصب یا مجوزهای پوشه در بررسی امنیتی تأیید نشده‌اند. برای بازیابی فایل‌ها و مجوزها، سرویس را دوباره نصب و سپس مجدداً تلاش کنید. ممکن است ویندوز تأیید مدیر را درخواست کند.",
"notAutoStartedMessage": "نوع راه‌اندازی سرویس Clash Verge به «دستی» یا «غیرفعال» تغییر کرده است (اغلب توسط ابزارهای بهینه‌ساز)، بنابراین دیگر همراه ویندوز اجرا نمی‌شود؛ این نشست از Sidecar استفاده می‌کند. برای بازگرداندن راه‌اندازی خودکار، سرویس را دوباره نصب کنید. ممکن است ویندوز تأیید مدیر را درخواست کند.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "Layanan tidak dapat memulai inti karena jalur instalasi atau izin direktori tidak lolos pemeriksaan keamanan. Pasang ulang layanan untuk memulihkan berkas dan izinnya, lalu coba lagi. Windows mungkin meminta persetujuan administrator.",
"notAutoStartedMessage": "Jenis startup layanan Clash Verge diubah menjadi Manual atau Nonaktif (sering oleh alat pengoptimal), sehingga tidak lagi berjalan saat Windows dimulai; sesi ini menggunakan Sidecar. Pasang ulang layanan untuk memulihkan startup otomatis. Windows mungkin meminta persetujuan administrator.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "サービスのインストール先またはフォルダーのアクセス許可がセキュリティチェックに合格しなかったため、コアを起動できません。サービスを再インストールしてファイルとアクセス許可を復元し、再試行してください。Windows が管理者の承認を求める場合があります。",
"notAutoStartedMessage": "Clash Verge サービスのスタートアップの種類が「手動」または「無効」に変更されたため(最適化ツールによることが多い)、Windows の起動時に開始されません。このセッションは Sidecar で動作しています。サービスを再インストールすると自動起動に戻ります。Windows が管理者の承認を求める場合があります。",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "서비스 설치 경로나 폴더 권한이 보안 검사를 통과하지 못해 코어를 시작할 수 없습니다. 서비스를 다시 설치하여 파일과 권한을 복원한 후 다시 시도하세요. Windows에서 관리자 승인을 요청할 수 있습니다.",
"notAutoStartedMessage": "Clash Verge 서비스의 시작 유형이 수동 또는 사용 안 함으로 변경되어(최적화 도구가 변경하는 경우가 많음) Windows 시작 시 실행되지 않습니다. 이번 세션은 Sidecar로 실행 중입니다. 서비스를 다시 설치하면 자동 시작이 복원됩니다. Windows에서 관리자 승인을 요청할 수 있습니다.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "Служба не может запустить ядро: путь установки или права доступа к каталогу не прошли проверку безопасности. Переустановите службу, чтобы восстановить файлы и права доступа, затем повторите попытку. Windows может запросить подтверждение администратора.",
"notAutoStartedMessage": "Тип запуска службы Clash Verge изменён на «Вручную» или «Отключена» (часто это делают программы-оптимизаторы), поэтому она больше не запускается вместе с Windows; в этом сеансе используется Sidecar. Переустановите службу, чтобы вернуть автоматический запуск. Windows может запросить подтверждение администратора.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "Hizmetin kurulum yolu veya dizin izinleri güvenlik denetiminden geçemediği için çekirdek başlatılamıyor. Dosyaları ve izinleri geri yüklemek için hizmeti yeniden kurup tekrar deneyin. Windows yönetici onayı isteyebilir.",
"notAutoStartedMessage": "Clash Verge hizmetinin başlangıç türü El ile veya Devre dışı olarak değiştirildi (genellikle optimizasyon araçları tarafından), bu nedenle artık Windows ile başlamıyor; bu oturum Sidecar kullanıyor. Otomatik başlatmayı geri yüklemek için hizmeti yeniden kurun. Windows yönetici onayı isteyebilir.",
"title": "Hizmet güncellemesi gerekli",
"message": "Clash Verge hizmet sürümü, bu uygulamanın gerektirdiği sürümle eşleşmiyor. Mevcut hizmeti kaldırıp gerekli sürümü yüklemek için hizmeti yeniden kurun ya da bu oturumda Sidecar ile devam edin.",
"checkingMessage": "Sistem hizmetinin durumu ve geçerli çalışma modu denetleniyor. Lütfen bekleyin…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "Хезмәтнең урнаштыру юлы яки каталог рөхсәтләре иминлек тикшерүен узмаганга, төшне эшләтеп булмый. Файлларны һәм рөхсәтләрне торгызу өчен хезмәтне яңадан урнаштырыгыз һәм кабатлап карагыз. Windows администратор раславын сорарга мөмкин.",
"notAutoStartedMessage": "Clash Verge хезмәтенең башлау төре «Кулдан» яки «Сүндерелгән» итеп үзгәртелгән (еш кына оптимизация программалары тарафыннан), шуңа күрә ул Windows белән бергә башланмый; бу сеанс Sidecar куллана. Автоматик башлауны кайтару өчен хезмәтне яңадан урнаштырыгыз. Windows администратор раславын сорарга мөмкин.",
"title": "Service update required",
"message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "系统服务的安装路径或目录权限未通过安全检查,无法启动内核。请重新安装服务以恢复服务文件和权限,随后重试。Windows 可能会请求管理员授权。",
"notAutoStartedMessage": "Clash Verge 系统服务的启动类型已被改为手动或禁用(常见于优化软件的开机加速功能),开机后不会自动运行,本次已改用 Sidecar。请重新安装服务以恢复自动启动。Windows 可能会请求管理员授权。",
"title": "需要更新系统服务",
"message": "Clash Verge 系统服务版本与当前应用要求不符。请选择「重新安装服务」卸载现有服务并安装所需版本,也可以仅在本次会话继续使用 Sidecar。",
"checkingMessage": "正在确认系统服务状态及当前运行模式,请稍候……",
+1
View File
@@ -22,6 +22,7 @@
},
"serviceMigration": {
"locationRefusedMessage": "系統服務的安裝路徑或目錄權限未通過安全檢查,無法啟動核心。請重新安裝服務以還原服務檔案和權限,然後重試。Windows 可能會要求管理員授權。",
"notAutoStartedMessage": "Clash Verge 系統服務的啟動類型已被改為手動或停用(常見於最佳化軟體的開機加速功能),開機後不會自動執行,本次已改用 Sidecar。請重新安裝服務以恢復自動啟動。Windows 可能會要求管理員授權。",
"title": "需要更新系統服務",
"message": "Clash Verge 系統服務版本與目前應用程式要求不符。請選擇「重新安裝服務」移除現有服務並安裝所需版本,也可以僅在本次工作階段繼續使用 Sidecar。",
"checkingMessage": "正在確認系統服務狀態及目前執行模式,請稍候……",
@@ -124,6 +124,8 @@ export const handleNoticeMessage = (
{ reason: notice.reason },
0,
)
} else if (notice?.kind === 'notAutoStarted') {
requestService({ reason: 'serviceNotAutoStarted' })
} else if (notice) {
showNotice.warning(
'settings.feedback.notifications.clashService.sidecarFallback',
+1
View File
@@ -189,6 +189,7 @@ export async function takeDnsOverrideNotice() {
export type ServiceFallbackNotice =
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
export async function takeServiceFallbackNotice() {
return invoke<ServiceFallbackNotice | null>('take_service_fallback_notice')
+1
View File
@@ -4,6 +4,7 @@ export type ServiceRequestReason =
| 'sysproxySidecarReady'
| 'tunNeedsService'
| 'serviceLocationRefused'
| 'serviceNotAutoStarted'
export interface ServiceRequest {
readonly reason: ServiceRequestReason
+1
View File
@@ -127,6 +127,7 @@ export const translationKeys = [
'layout.components.navigation.menu.collapseNavBar',
'layout.components.navigation.menu.expandNavBar',
'layout.components.serviceMigration.locationRefusedMessage',
'layout.components.serviceMigration.notAutoStartedMessage',
'layout.components.serviceMigration.title',
'layout.components.serviceMigration.message',
'layout.components.serviceMigration.checkingMessage',
+1
View File
@@ -227,6 +227,7 @@ export interface TranslationResources {
}
locationRefusedMessage: string
message: string
notAutoStartedMessage: string
reinstall: string
repair: string
success: string