From 52d616302ca518256b8873aab892af08f8cea5c0 Mon Sep 17 00:00:00 2001 From: wonfen Date: Fri, 2 Oct 2026 10:36:29 +0800 Subject: [PATCH] fix(service): explain a startup core rejection instead of the generic fallback When the startup probe finds the service refusing the approved core, the app falls back to Sidecar with only the generic "could not start the core" notice. The reason and repair guide appeared only after a reinstall, which cannot fix a writable volume root (#8045). Carry the fallback reason to the window and show the same permission notice as a rejected installation when the core was refused. Refs #8045 --- Changelog.md | 4 +++ src-tauri/src/cmd/service.rs | 2 +- src-tauri/src/core/manager/lifecycle.rs | 2 +- src-tauri/src/core/runstate/mod.rs | 2 +- src-tauri/src/core/runstate/probe.rs | 5 +++- src-tauri/src/core/service.rs | 29 ++++++++++++++----- .../_layout/hooks/use-layout-events.test.ts | 28 ++++++++++++++++-- .../_layout/utils/notification-handlers.ts | 10 +++++-- src/services/cmds.ts | 6 +++- 9 files changed, 71 insertions(+), 17 deletions(-) diff --git a/Changelog.md b/Changelog.md index 23d2922dc..3f3908a96 100644 --- a/Changelog.md +++ b/Changelog.md @@ -22,4 +22,8 @@ 🚀 优化改进 +**🖥️ Windows** + +- 优化 Windows 服务安全检查未通过时的启动提示:显示具体原因,并提供修复文档链接 + diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 50a5e0182..bf5aa4d85 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -113,7 +113,7 @@ pub async fn continue_with_sidecar() -> CmdResult { } #[tauri::command] -pub fn take_service_fallback_notice() -> bool { +pub fn take_service_fallback_notice() -> Option { crate::core::service::take_service_fallback_notice() } diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index d8c5784f2..1e2b43d82 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -678,7 +678,7 @@ impl CoreManager { SERVICE_MANAGER.withdraw_sidecar_allowance(); self.rollback_failed_start().await; } else { - crate::core::service::notify_service_fallback(); + crate::core::service::notify_service_fallback(&reason); } result.map_err(|error| error.context(format!("Service failure before Sidecar startup: {reason}"))) } diff --git a/src-tauri/src/core/runstate/mod.rs b/src-tauri/src/core/runstate/mod.rs index e06979ed5..bc1a9b4d8 100644 --- a/src-tauri/src/core/runstate/mod.rs +++ b/src-tauri/src/core/runstate/mod.rs @@ -28,7 +28,7 @@ pub use env::FakeEnv; pub use env::{RealEnv, RunStateEnv}; pub use health::{PendingAction, RunState, RunStateView, ServiceHealth}; pub use owner::{OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch}; -pub use probe::{ServiceVersionCheck, ServiceVersionReply, classify_service_version_reply}; +pub use probe::{CORE_REJECTED_PREFIX, ServiceVersionCheck, ServiceVersionReply, classify_service_version_reply}; use crate::core::manager::RunningMode; use health::StoredService; diff --git a/src-tauri/src/core/runstate/probe.rs b/src-tauri/src/core/runstate/probe.rs index 3fd6d95e7..93b284708 100644 --- a/src-tauri/src/core/runstate/probe.rs +++ b/src-tauri/src/core/runstate/probe.rs @@ -27,6 +27,9 @@ pub enum ServiceVersionCheck { CoreUnavailable(String), } +/// Leads the Unavailable reason when the Service refused the approved core itself. +pub const CORE_REJECTED_PREFIX: &str = "approved core was rejected: "; + /// What a live probe of the currently-installed Service told us. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CurrentServiceProbe { @@ -47,7 +50,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer return match &reply.core { Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready, Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => { - ServiceVersionCheck::CoreUnavailable(format!("approved core was rejected: {reason}")) + ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}")) } _ => ServiceVersionCheck::CoreUnavailable( "the selected approved core is missing or does not match; reinstall the service to repair it".into(), diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index d61f8de1e..975ec597c 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -9,8 +9,8 @@ use crate::{ owner_identity::current_owner_credentials, proxy_control, runstate::{ - OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE, ReadyWaitError, - RunState, RunStateEnv, RunStateStore, ServiceHealth, + CORE_REJECTED_PREFIX, OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE, + ReadyWaitError, RunState, RunStateEnv, RunStateStore, ServiceHealth, }, runtime_bundle::{RemoteProviderRef, collect_runtime_bundle, remote_providers_of}, tray::Tray, @@ -39,18 +39,31 @@ use std::{ static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0); static ACTIVE_SERVICE_SESSION: Lazy>> = Lazy::new(|| Mutex::new(None)); -static PENDING_SERVICE_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false); +static PENDING_SERVICE_FALLBACK_NOTICE: Mutex> = Mutex::new(None); static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false); static PENDING_SERVICE_OWNER_NOTICE: Mutex> = Mutex::new(None); +/// Why the Service was unavailable when the core fell back to Sidecar. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "kind", content = "reason", rename_all = "camelCase")] +pub enum ServiceFallbackNotice { + Unavailable, + CoreRejected(String), +} + #[cfg(target_os = "windows")] -pub(crate) fn notify_service_fallback() { - PENDING_SERVICE_FALLBACK_NOTICE.store(true, Ordering::Relaxed); +pub(crate) fn notify_service_fallback(reason: &str) { + *PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned()); Handle::notice_message("service_core::sidecar_fallback", ""); } -pub(crate) fn take_service_fallback_notice() -> bool { - PENDING_SERVICE_FALLBACK_NOTICE.swap(false, Ordering::Relaxed) +pub(crate) fn take_service_fallback_notice() -> Option { + let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?; + Some(if reason.starts_with(CORE_REJECTED_PREFIX) { + ServiceFallbackNotice::CoreRejected(reason) + } else { + ServiceFallbackNotice::Unavailable + }) } pub(crate) fn take_service_repair_notice() -> bool { @@ -919,7 +932,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<() // PAC follows the Running Mode; the caller opens it via `core_started(Service)`. start_owner_monitor(); tracing::Span::current().record("outcome", "started"); - PENDING_SERVICE_FALLBACK_NOTICE.store(false, Ordering::Relaxed); + PENDING_SERVICE_FALLBACK_NOTICE.lock().take(); PENDING_SERVICE_REPAIR_NOTICE.store(false, Ordering::Relaxed); PENDING_SERVICE_OWNER_NOTICE.lock().take(); logging!( diff --git a/src/pages/_layout/hooks/use-layout-events.test.ts b/src/pages/_layout/hooks/use-layout-events.test.ts index ee8392117..b1fc2f0b6 100644 --- a/src/pages/_layout/hooks/use-layout-events.test.ts +++ b/src/pages/_layout/hooks/use-layout-events.test.ts @@ -4,6 +4,7 @@ import { type RunState, getCoreStartupError, takeDnsOverrideNotice, + takeServiceFallbackNotice, takeServiceRepairNotice, } from '@/services/cmds' import { subscribeVergeEvents } from '@/services/events' @@ -31,13 +32,13 @@ vi.mock('@/services/cmds', () => ({ getCoreStartupError: vi.fn().mockResolvedValue(null), takeDiscardedKeysNotice: vi.fn().mockResolvedValue(null), takeDnsOverrideNotice: vi.fn().mockResolvedValue(false), - takeServiceFallbackNotice: vi.fn().mockResolvedValue(false), + takeServiceFallbackNotice: vi.fn().mockResolvedValue(null), takeServiceRepairNotice: vi.fn().mockResolvedValue(false), takeServiceOwnerNotice: vi.fn().mockResolvedValue(null), })) vi.mock('@/services/service-request', () => ({ requestService: vi.fn() })) vi.mock('@/services/notice-service', () => ({ - showNotice: { info: vi.fn(), error: vi.fn() }, + showNotice: { info: vi.fn(), error: vi.fn(), warning: vi.fn() }, })) vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn(), @@ -184,3 +185,26 @@ it('offers service reinstallation for a startup path refusal even before listene reason: 'serviceLocationRefused', }) }) + +it('explains a startup core rejection instead of the generic fallback notice', async () => { + const reason = + 'approved core was rejected: core path "C:\\" has an untrusted write ACE' + vi.mocked(takeServiceFallbackNotice) + .mockResolvedValueOnce({ kind: 'coreRejected', reason }) + .mockResolvedValue(null) + + useLayoutEvents(([status, message]) => { + handleNoticeMessage(status, message, (key) => key, vi.fn()) + }) + + const [handlers, onSubscribed] = vi.mocked(subscribeVergeEvents).mock.calls[0] + onSubscribed?.() + handlers['verge://notice-message']?.(['service_core::sidecar_fallback', '']) + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(showNotice.warning).toHaveBeenCalledExactlyOnceWith( + 'settings.feedback.notifications.clashService.permissionFallback', + { reason }, + 0, + ) +}) diff --git a/src/pages/_layout/utils/notification-handlers.ts b/src/pages/_layout/utils/notification-handlers.ts index e45fb5568..25a6d614b 100644 --- a/src/pages/_layout/utils/notification-handlers.ts +++ b/src/pages/_layout/utils/notification-handlers.ts @@ -117,8 +117,14 @@ export const handleNoticeMessage = ( }, 'service_core::sidecar_fallback': () => { void takeServiceFallbackNotice() - .then((pending) => { - if (pending) { + .then((notice) => { + if (notice?.kind === 'coreRejected') { + showNotice.warning( + 'settings.feedback.notifications.clashService.permissionFallback', + { reason: notice.reason }, + 0, + ) + } else if (notice) { showNotice.warning( 'settings.feedback.notifications.clashService.sidecarFallback', ) diff --git a/src/services/cmds.ts b/src/services/cmds.ts index b2a057913..c2df176fb 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -186,8 +186,12 @@ export async function takeDnsOverrideNotice() { return invoke('take_dns_override_notice') } +export type ServiceFallbackNotice = + | { kind: 'unavailable' } + | { kind: 'coreRejected'; reason: string } + export async function takeServiceFallbackNotice() { - return invoke('take_service_fallback_notice') + return invoke('take_service_fallback_notice') } export interface CoreFailure {