mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
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
This commit is contained in:
@@ -22,4 +22,8 @@
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 优化 Windows 服务安全检查未通过时的启动提示:显示具体原因,并提供修复文档链接
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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::ServiceFallbackNotice> {
|
||||
crate::core::service::take_service_fallback_notice()
|
||||
}
|
||||
|
||||
|
||||
@@ -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}")))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<Mutex<Option<ActiveServiceSession>>> = Lazy::new(|| Mutex::new(None));
|
||||
static PENDING_SERVICE_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
static PENDING_SERVICE_FALLBACK_NOTICE: Mutex<Option<String>> = Mutex::new(None);
|
||||
static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
static PENDING_SERVICE_OWNER_NOTICE: Mutex<Option<String>> = 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<ServiceFallbackNotice> {
|
||||
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!(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -186,8 +186,12 @@ export async function takeDnsOverrideNotice() {
|
||||
return invoke<boolean>('take_dns_override_notice')
|
||||
}
|
||||
|
||||
export type ServiceFallbackNotice =
|
||||
| { kind: 'unavailable' }
|
||||
| { kind: 'coreRejected'; reason: string }
|
||||
|
||||
export async function takeServiceFallbackNotice() {
|
||||
return invoke<boolean>('take_service_fallback_notice')
|
||||
return invoke<ServiceFallbackNotice | null>('take_service_fallback_notice')
|
||||
}
|
||||
|
||||
export interface CoreFailure {
|
||||
|
||||
Reference in New Issue
Block a user