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 {