chore: merge dev into declarative effects

This commit is contained in:
Tunglies
2026-10-02 16:20:25 +08:00
9 changed files with 71 additions and 17 deletions
+4
View File
@@ -22,4 +22,8 @@
<summary><strong> 🚀 优化改进 </strong></summary>
**🖥️ Windows**
- 优化 Windows 服务安全检查未通过时的启动提示:显示具体原因,并提供修复文档链接
</details>
+1 -1
View File
@@ -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()
}
+1 -1
View File
@@ -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}")))
}
+1 -1
View File
@@ -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;
+4 -1
View File
@@ -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(),
+21 -8
View File
@@ -10,8 +10,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,
@@ -40,18 +40,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(NoticeStatus::ServiceCoreSidecarFallback, "");
}
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 {
@@ -925,7 +938,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',
)
+5 -1
View File
@@ -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 {