mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 20:32:21 +08:00
fix(linux): keep Service upgrade notices until the frontend takes them
Silent start creates no window, so emitted notices were dropped while the upgrade marker was already acknowledged. Keep both upgrade notices in backend state, replay the state warning on subscription, and acknowledge the marker only after every notice has been taken. Refs #8515
This commit is contained in:
@@ -141,6 +141,13 @@ pub fn take_service_repair_notice() -> bool {
|
||||
crate::core::service::take_service_repair_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(clippy::missing_const_for_fn))]
|
||||
pub fn take_service_upgrade_state_notice() -> Option<String> {
|
||||
crate::core::service::take_service_upgrade_state_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_service_owner_notice() -> Option<String> {
|
||||
|
||||
@@ -42,6 +42,8 @@ static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(
|
||||
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);
|
||||
#[cfg(target_os = "linux")]
|
||||
static PENDING_SERVICE_UPGRADE: Mutex<Option<PendingServiceUpgrade>> = Mutex::new(None);
|
||||
|
||||
/// Why the Service was unavailable when the core fell back to Sidecar.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
@@ -93,6 +95,17 @@ pub(crate) async fn notify_incomplete_service_upgrade() {
|
||||
}
|
||||
let status = SERVICE_MANAGER.current().await;
|
||||
let notice = service_upgrade_notice(&marker, matches!(status, ServiceStatus::Ready));
|
||||
if !notice.reinstall && notice.unrestored_state.is_none() {
|
||||
acknowledge_service_upgrade(&ack, id);
|
||||
return;
|
||||
}
|
||||
// Without a window (silent start) the events are dropped; the frontend replays them on subscription.
|
||||
*PENDING_SERVICE_UPGRADE.lock() = Some(PendingServiceUpgrade {
|
||||
ack,
|
||||
id: id.to_owned(),
|
||||
reinstall: notice.reinstall,
|
||||
unrestored_state: notice.unrestored_state.map(str::to_owned),
|
||||
});
|
||||
if notice.reinstall {
|
||||
logging!(warn, Type::Service, "安装包升级后服务未就绪: {status:?}");
|
||||
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(SERVICE_UPGRADE_INCOMPLETE.to_owned());
|
||||
@@ -100,13 +113,51 @@ pub(crate) async fn notify_incomplete_service_upgrade() {
|
||||
}
|
||||
if let Some(state) = notice.unrestored_state {
|
||||
logging!(warn, Type::Service, "安装包升级后未能恢复服务原状态: {state}");
|
||||
Handle::notice(NoticeStatus::ServiceCoreUpgradeStateNotRestored, state);
|
||||
Handle::notice(NoticeStatus::ServiceCoreUpgradeStateNotRestored, "");
|
||||
}
|
||||
if let Err(error) = std::fs::write(&ack, id) {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn acknowledge_service_upgrade(ack: &Path, id: &str) {
|
||||
if let Err(error) = std::fs::write(ack, id) {
|
||||
logging!(warn, Type::Service, "无法记录服务升级确认: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An upgrade whose notices are acknowledged on disk only after the frontend has taken all of them.
|
||||
#[cfg(target_os = "linux")]
|
||||
struct PendingServiceUpgrade {
|
||||
ack: PathBuf,
|
||||
id: String,
|
||||
reinstall: bool,
|
||||
unrestored_state: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn settle_service_upgrade<T>(take: impl FnOnce(&mut PendingServiceUpgrade) -> T) -> Option<T> {
|
||||
let (taken, settled) = {
|
||||
let mut pending = PENDING_SERVICE_UPGRADE.lock();
|
||||
let upgrade = pending.as_mut()?;
|
||||
let taken = take(upgrade);
|
||||
let settled = !upgrade.reinstall && upgrade.unrestored_state.is_none();
|
||||
(taken, if settled { pending.take() } else { None })
|
||||
};
|
||||
if let Some(upgrade) = settled {
|
||||
acknowledge_service_upgrade(&upgrade.ack, &upgrade.id);
|
||||
}
|
||||
Some(taken)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn take_service_upgrade_state_notice() -> Option<String> {
|
||||
settle_service_upgrade(|upgrade| upgrade.unrestored_state.take()).flatten()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const fn take_service_upgrade_state_notice() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct ServiceUpgradeNotice<'a> {
|
||||
@@ -126,6 +177,8 @@ fn service_upgrade_notice(marker: &str, ready: bool) -> ServiceUpgradeNotice<'_>
|
||||
pub(crate) fn take_service_fallback_notice() -> Option<ServiceFallbackNotice> {
|
||||
let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?;
|
||||
Some(if reason == SERVICE_UPGRADE_INCOMPLETE {
|
||||
#[cfg(target_os = "linux")]
|
||||
settle_service_upgrade(|upgrade| upgrade.reinstall = false);
|
||||
ServiceFallbackNotice::UpgradeIncomplete
|
||||
} else if reason.starts_with(CORE_REJECTED_PREFIX) {
|
||||
ServiceFallbackNotice::CoreRejected(reason)
|
||||
@@ -2183,13 +2236,16 @@ pub static SERVICE_MANAGER: ServiceManager = ServiceManager;
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used, clippy::panic, reason = "tests assert by panicking")]
|
||||
mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::{
|
||||
PENDING_SERVICE_UPGRADE, PendingServiceUpgrade, ServiceUpgradeNotice, service_upgrade_notice,
|
||||
settle_service_upgrade, take_service_upgrade_state_notice,
|
||||
};
|
||||
use super::{
|
||||
ServiceHealth, ServiceStatus, capture_generation_before, claim_owner_recovery_generation,
|
||||
generate_service_session_token, mark_service_unavailable_after_owner_loss, owner_recovery_policy,
|
||||
service_core_path_for, session_matches_status,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::{ServiceUpgradeNotice, service_upgrade_notice};
|
||||
#[cfg(unix)]
|
||||
use super::{service_core_path_for_with_publisher, service_tool_path_for};
|
||||
use crate::core::runstate::{FakeEnv, OwnerRecoveryReason, PendingAction, RunStateStore};
|
||||
@@ -2868,4 +2924,26 @@ mod tests {
|
||||
notice(true, Some("disabled,active"))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn upgrade_marker_is_acknowledged_only_after_every_notice_is_taken() {
|
||||
let ack = std::env::temp_dir().join(format!("verge-service-upgrade-ack-{}", std::process::id()));
|
||||
let _ = std::fs::remove_file(&ack);
|
||||
*PENDING_SERVICE_UPGRADE.lock() = Some(PendingServiceUpgrade {
|
||||
ack: ack.clone(),
|
||||
id: "7".to_owned(),
|
||||
reinstall: true,
|
||||
unrestored_state: Some("disabled,active".to_owned()),
|
||||
});
|
||||
|
||||
assert_eq!(take_service_upgrade_state_notice().as_deref(), Some("disabled,active"));
|
||||
assert!(!ack.exists());
|
||||
assert_eq!(take_service_upgrade_state_notice(), None);
|
||||
|
||||
settle_service_upgrade(|upgrade| upgrade.reinstall = false);
|
||||
assert_eq!(std::fs::read_to_string(&ack).ok().as_deref(), Some("7"));
|
||||
assert!(PENDING_SERVICE_UPGRADE.lock().is_none());
|
||||
let _ = std::fs::remove_file(&ack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
cmd::get_core_startup_error,
|
||||
cmd::take_service_repair_notice,
|
||||
cmd::take_service_owner_notice,
|
||||
cmd::take_service_upgrade_state_notice,
|
||||
cmd::take_discarded_keys_notice,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
|
||||
@@ -111,6 +111,8 @@ export const commands = {
|
||||
__TAURI_INVOKE<boolean>('take_service_repair_notice'),
|
||||
takeServiceOwnerNotice: () =>
|
||||
__TAURI_INVOKE<string | null>('take_service_owner_notice'),
|
||||
takeServiceUpgradeStateNotice: () =>
|
||||
__TAURI_INVOKE<string | null>('take_service_upgrade_state_notice'),
|
||||
takeDiscardedKeysNotice: () =>
|
||||
__TAURI_INVOKE<string | null>('take_discarded_keys_notice'),
|
||||
getDnsConfigContent: () =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
takeDnsOverrideNotice,
|
||||
takeServiceFallbackNotice,
|
||||
takeServiceRepairNotice,
|
||||
takeServiceUpgradeStateNotice,
|
||||
} from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { revalidateQueries } from '@/services/query-client'
|
||||
@@ -27,6 +28,7 @@ vi.mock('@/services/cmds', () => ({
|
||||
takeServiceFallbackNotice: vi.fn().mockResolvedValue(null),
|
||||
takeServiceRepairNotice: vi.fn().mockResolvedValue(false),
|
||||
takeServiceOwnerNotice: vi.fn().mockResolvedValue(null),
|
||||
takeServiceUpgradeStateNotice: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
vi.mock('@/services/service-request', () => ({ requestService: vi.fn() }))
|
||||
vi.mock('@/services/notice-service', () => ({
|
||||
@@ -198,3 +200,23 @@ it('explains a startup core rejection instead of the generic fallback notice', a
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
it('replays a pending Service upgrade state warning once the frontend subscribes', async () => {
|
||||
vi.mocked(takeServiceUpgradeStateNotice)
|
||||
.mockResolvedValueOnce('disabled,active')
|
||||
.mockResolvedValue(null)
|
||||
|
||||
const bus = await mountBus()
|
||||
bus.onSubscribed()
|
||||
bus.handlers['verge://notice-message']([
|
||||
'service_core::upgrade_state_not_restored',
|
||||
'',
|
||||
])
|
||||
await flush()
|
||||
|
||||
expect(showNotice.warning).toHaveBeenCalledExactlyOnceWith(
|
||||
'settings.feedback.notifications.clashService.upgradeStateNotRestored',
|
||||
{ state: 'disabled,active' },
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -105,6 +105,7 @@ export const createEventBus = (deps: BusHandlerDeps) => {
|
||||
deps.handleNotice(['service_core::sidecar_fallback', ''])
|
||||
deps.handleNotice(['service_core::repair_required', ''])
|
||||
deps.handleNotice(['service_core::app_data_not_owned', ''])
|
||||
deps.handleNotice(['service_core::upgrade_state_not_restored', ''])
|
||||
deps.handleNotice(['core_start::error', ''])
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,10 @@ export async function takeServiceOwnerNotice() {
|
||||
return commands.takeServiceOwnerNotice()
|
||||
}
|
||||
|
||||
export async function takeServiceUpgradeStateNotice() {
|
||||
return commands.takeServiceUpgradeStateNotice()
|
||||
}
|
||||
|
||||
export async function takeDiscardedKeysNotice() {
|
||||
return commands.takeDiscardedKeysNotice()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
takeServiceFallbackNotice,
|
||||
takeServiceOwnerNotice,
|
||||
takeServiceRepairNotice,
|
||||
takeServiceUpgradeStateNotice,
|
||||
} from '@/services/cmds'
|
||||
import type { NoticeStatus } from '@/services/contract'
|
||||
import { hideNotice, showNotice } from '@/services/notice-service'
|
||||
@@ -140,12 +141,24 @@ const handlers: Record<
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::upgrade_state_not_restored': (msg) =>
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.upgradeStateNotRestored',
|
||||
{ state: msg },
|
||||
0,
|
||||
),
|
||||
'service_core::upgrade_state_not_restored': () => {
|
||||
void takeServiceUpgradeStateNotice()
|
||||
.then((state) => {
|
||||
if (state) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.upgradeStateNotRestored',
|
||||
{ state },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service upgrade state notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
|
||||
Reference in New Issue
Block a user