From 726b2b362fe4e10fc4ff3df546bdc9a5f2ade284 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 31 Oct 2026 22:39:19 +0800 Subject: [PATCH] fix(service): sync core-updated geo databases before validation --- Cargo.lock | 4 +- Changelog.md | 1 + src-tauri/Cargo.toml | 2 +- src-tauri/src/core/service.rs | 76 +++++++++++++++++++++++++++++++++- src-tauri/src/core/validate.rs | 3 ++ 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26b6bf095..179be4dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,8 +1229,8 @@ dependencies = [ [[package]] name = "clash_verge_service_ipc" -version = "2.7.6" -source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.6#b2a7427d6681c8d3780ab83a73f4dc21da2f22d6" +version = "2.7.7" +source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.7#3ed29700b093190b5e7addcaa892e77f172af544" dependencies = [ "anyhow", "kode-bridge", diff --git a/Changelog.md b/Changelog.md index 69314efb6..dd88cac4f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -33,6 +33,7 @@ - 修复 代理 在全局模式下无法展开节点组 - 修复 自定义 CSS 无法修改编辑器字体和字号的问题 - 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动的问题 +- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验的问题 **🍎 macOS** diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 56c58db2c..fa96f1e8f 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -98,7 +98,7 @@ tracing = { workspace = true } tauri-plugin-devtools = { version = "2.2.2", optional = true } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" } async-trait = "0.1.92" -clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [ +clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [ "client", ] } arc-swap = "1.9.2" diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 7d9242087..8835e5ff1 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -13,7 +13,7 @@ use crate::{ CORE_REJECTED_PREFIX, OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE, ReadyWaitError, RunState, RunStateEnv, RunStateStore, ServiceHealth, }, - runtime_bundle::{RemoteProviderRef, collect_runtime_bundle, remote_providers_of}, + runtime_bundle::{GEO_ASSETS, RemoteProviderRef, collect_runtime_bundle, remote_providers_of}, tray::Tray, }, process::AsyncHandler, @@ -101,6 +101,7 @@ struct ActiveServiceSession { proof: OwnerSessionProof, supports_runtime_staging: bool, supports_runtime_file_read: bool, + supports_runtime_geo_read: bool, } fn generate_service_session_token() -> Result { @@ -132,6 +133,13 @@ pub(crate) fn active_service_supports_runtime_file_read() -> bool { .is_some_and(|session| session.supports_runtime_file_read) } +fn active_service_supports_runtime_geo_read() -> bool { + ACTIVE_SERVICE_SESSION + .lock() + .as_ref() + .is_some_and(|session| session.supports_runtime_geo_read) +} + pub(crate) fn clear_active_service_session() { ACTIVE_SERVICE_SESSION.lock().take(); } @@ -140,6 +148,7 @@ pub(crate) fn clear_active_service_session() { struct ServiceCapabilities { runtime_staging: bool, runtime_file_read: bool, + runtime_geo_read: bool, } impl ServiceCapabilities { @@ -147,6 +156,7 @@ impl ServiceCapabilities { Self { runtime_staging: info.supports_runtime_staging(), runtime_file_read: info.supports_runtime_file_read(), + runtime_geo_read: info.supports_runtime_geo_read(), } } } @@ -956,6 +966,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<() }, supports_runtime_staging: capabilities.runtime_staging, supports_runtime_file_read: capabilities.runtime_file_read, + supports_runtime_geo_read: capabilities.runtime_geo_read, }); // PAC follows the Running Mode; the caller opens it via `core_started(Service)`. @@ -1206,6 +1217,69 @@ async fn sync_runtime_providers_by_service() -> Result { Ok(outcome) } +/// Pulls geo databases the service core rewrote, because validation reads the app dir. +pub(crate) async fn sync_runtime_geo_by_service() { + if !active_service_supports_runtime_geo_read() { + return; + } + let (Ok(credentials), Ok(session), Ok(app_dir)) = ( + current_owner_credentials(), + active_service_session(), + dirs::app_home_dir(), + ) else { + return; + }; + for asset in GEO_ASSETS { + if let Err(error) = sync_runtime_geo(&credentials, &session, asset, &app_dir.join(asset)).await { + logging!(warn, Type::Service, "failed to sync geo database {asset}: {error:#}"); + } + } +} + +async fn sync_runtime_geo( + credentials: &OwnerCredentials, + session: &OwnerSessionProof, + asset: &str, + target: &Path, +) -> Result<()> { + let Ok(local) = tokio::fs::metadata(target).await else { + return Ok(()); + }; + let Some((Some(_), _, Some(runtime_mtime_ns))) = read_chunk(credentials, session, asset, u64::MAX).await? else { + return Ok(()); + }; + let local_mtime_ns = local + .modified()? + .duration_since(UNIX_EPOCH) + .map_or(0, |since_epoch| since_epoch.as_nanos()); + // A newer local file is a change the next staging still has to copy into the runtime. + if u128::from(runtime_mtime_ns) <= local_mtime_ns { + return Ok(()); + } + let (temp, file) = create_sync_temp(target).await?; + let published = async { + let Fetch::Complete { + mtime_ns: Some(mtime_ns), + .. + } = fetch_runtime_file(credentials, session, asset, file).await? + else { + return Ok(false); + }; + // Matching the runtime copy's time lets the service skip copying these bytes straight back. + std::fs::File::options() + .write(true) + .open(&temp)? + .set_modified(UNIX_EPOCH + Duration::from_nanos(mtime_ns))?; + tokio::fs::rename(&temp, target).await?; + anyhow::Ok(true) + } + .await; + if !matches!(published, Ok(true)) { + remove_temp(&temp).await; + } + published.map(|_| ()) +} + // The on-disk YAML may describe a rejected apply; use the committed runtime. fn applied_remote_providers(runtime: &Draft, app_dir: &Path) -> Result> { let applied = runtime.data_arc(); diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index b244127d1..cfa8d7a6c 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -324,6 +324,7 @@ impl CoreConfigValidator { return Self::validate_file_syntax_outcome(config_path).await; } + crate::core::service::sync_runtime_geo_by_service().await; let is_script = match Self::is_script_file(config_path).await { Ok(result) => result, Err(err) => { @@ -425,6 +426,8 @@ impl CoreConfigValidator { } logging!(info, Type::Validate, "生成临时配置文件用于验证"); + // The test run reads the app dir, which never sees updates made by a service core. + crate::core::service::sync_runtime_geo_by_service().await; let fingerprint = validation_fingerprint(yaml).await; if *LAST_VALIDATED.lock() == Some(fingerprint) { logging!(info, Type::Validate, "配置与上次通过验证的字节一致,跳过内核检查");