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
+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")]