fix(backend): preserve applied state across later failures

This commit is contained in:
Tunglies
2026-10-05 14:14:37 +08:00
parent 045fb42b4d
commit 36a8950129
10 changed files with 141 additions and 186 deletions
+2 -2
View File
@@ -143,9 +143,9 @@ impl Config {
PENDING_DNS_OVERRIDE_NOTICE.store(true, Ordering::Relaxed);
Handle::notice(NoticeStatus::DnsOverrideAutoDisabled, "");
}
verge.data_arc().save_file().await?;
let result = verge.data_arc().save_file().await;
Handle::refresh_verge();
Ok(())
result
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, bail};
use std::{io::ErrorKind, path::PathBuf};
pub(crate) struct FileSnapshot {
path: PathBuf,
pub(crate) path: PathBuf,
content: Option<Vec<u8>>,
}
-57
View File
@@ -1,57 +0,0 @@
use crate::{
config::{Config, IVerge},
core::{autostart, hotkey, lightweight, tray},
};
use anyhow::Result;
use futures::future::BoxFuture;
pub(crate) fn ensure_autostart(_patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move { autostart::update_launch().await })
}
pub(crate) fn ensure_hotkey(patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
if let Some(hotkeys) = &patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
}
Ok(())
})
}
pub(crate) fn ensure_tray_menu(_patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move { tray::Tray::global().update_menu().await })
}
pub(crate) fn ensure_tray_icon(patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
tray::Tray::global()
.update_icon(&Config::verge().await.latest_arc())
.await?;
#[cfg(target_os = "macos")]
if let Some(enabled) = patch.enable_tray_speed {
tray::Tray::global().update_speed_task(enabled);
}
#[cfg(not(target_os = "macos"))]
let _ = patch;
Ok(())
})
}
pub(crate) fn ensure_tray_tooltip(_patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move { tray::Tray::global().update_tooltip().await })
}
pub(crate) fn ensure_tray_click(_patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move { tray::Tray::global().update_click_behavior().await })
}
pub(crate) fn ensure_lightweight(patch: &IVerge) -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
if patch.enable_auto_light_weight_mode.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode().await;
} else {
lightweight::disable_auto_light_weight_mode();
}
Ok(())
})
}
+1 -1
View File
@@ -238,7 +238,7 @@ impl CoreManager {
}
}
fn current_core_readiness_generation(&self) -> Option<u64> {
pub(crate) fn current_core_readiness_generation(&self) -> Option<u64> {
active_core_readiness_generation(self.core_readiness_state.load(Ordering::Acquire))
}
-1
View File
@@ -1,4 +1,3 @@
pub(crate) mod app_effects;
pub mod auto_backup;
pub mod autostart;
pub mod backup;
+3 -9
View File
@@ -26,14 +26,6 @@ pub async fn set_dns_override(
profile_uid: String,
enabled: bool,
confirmation: Option<String>,
) -> Result<DnsOverrideOutcome> {
crate::core::notify::after_commit(Box::pin(set_dns_override_inner(profile_uid, enabled, confirmation))).await
}
async fn set_dns_override_inner(
profile_uid: String,
enabled: bool,
confirmation: Option<String>,
) -> Result<DnsOverrideOutcome> {
let _profile_write = PROFILE_WRITE_LOCK.lock().await;
let _config_write = Config::lock_config_write().await;
@@ -83,8 +75,10 @@ async fn set_dns_override_inner(
}
transaction.commit();
verge.data_arc().save_file().await?;
// Core and memory state remain applied even when persistence fails.
let result = verge.data_arc().save_file().await;
announce(Refresh::Verge);
announce(Refresh::Clash);
result?;
Ok(outcome)
}
+2 -3
View File
@@ -5,7 +5,6 @@ use std::collections::BTreeSet;
pub(super) enum Effect {
RestartCore,
ClashConfig,
VergeConfig,
Autostart,
SystemProxy,
TrayIcon,
@@ -90,8 +89,8 @@ verge_registry! {
web_ui_list => NoEffect;
clash_core => NoEffect;
hotkeys => [Hotkey, TrayMenu];
enable_global_hotkey => [VergeConfig];
home_cards => [VergeConfig];
enable_global_hotkey => NoEffect;
home_cards => NoEffect;
auto_close_connection => NoEffect;
auto_check_update => NoEffect;
default_latency_test => NoEffect;
+124 -103
View File
@@ -6,13 +6,13 @@ use crate::{
snapshot::{capture_config_files, restore_files},
},
core::auto_backup::AutoBackupManager,
core::{CoreManager, logger, manager::ConfigUpdateGuard, proxy_control},
core::{CoreManager, autostart, hotkey, lightweight, logger, manager::ConfigUpdateGuard, proxy_control, tray},
};
use anyhow::Result;
use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging_error};
use futures::future::BoxFuture;
use serde_yaml_ng::Mapping;
use std::sync::Arc;
use tokio::sync::MutexGuard;
pub(super) enum Patch<'a> {
@@ -20,101 +20,108 @@ pub(super) enum Patch<'a> {
Clash(&'a Mapping),
}
struct EffectContext<'a> {
patch: &'a IVerge,
manager: &'a CoreManager,
update: &'a ConfigUpdateGuard<'a>,
}
type Ensure = for<'a> fn(&'a EffectContext<'_>) -> BoxFuture<'a, Result<()>>;
const EFFECT_ORDER: &[(Effect, Ensure)] = &[
(Effect::RestartCore, ensure_restart),
(Effect::ClashConfig, ensure_clash),
(Effect::VergeConfig, ensure_verge),
(Effect::Autostart, ensure_autostart),
(Effect::Language, ensure_language),
(Effect::SystemProxy, ensure_system_proxy),
(Effect::Hotkey, ensure_hotkey),
(Effect::TrayMenu, ensure_tray_menu),
(Effect::TrayIcon, ensure_tray_icon),
(Effect::TrayTooltip, ensure_tray_tooltip),
(Effect::TrayClick, ensure_tray_click),
(Effect::Lightweight, ensure_lightweight),
(Effect::LogLevel, ensure_log_level),
(Effect::LogFile, ensure_log_file),
const EFFECT_ORDER: &[Effect] = &[
Effect::RestartCore,
Effect::ClashConfig,
Effect::Autostart,
Effect::Language,
Effect::SystemProxy,
Effect::Hotkey,
Effect::TrayMenu,
Effect::TrayIcon,
Effect::TrayTooltip,
Effect::TrayClick,
Effect::Lightweight,
Effect::LogLevel,
Effect::LogFile,
];
macro_rules! ensure {
($name:ident, $ctx:ident, $body:block) => {
fn $name<'a>($ctx: &'a EffectContext<'_>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move $body)
}
};
async fn ensure_restart(manager: &CoreManager) -> Result<()> {
Config::generate().await?;
let previous = manager.current_core_readiness_generation();
let result = manager.restart_core_during_config_update().await;
if result.is_ok()
|| manager
.current_core_readiness_generation()
.is_some_and(|current| Some(current) != previous)
{
Config::runtime().await.apply();
}
result?;
Ok(())
}
ensure!(ensure_restart, ctx, {
Config::generate().await?;
ctx.manager.restart_core_during_config_update().await
});
ensure!(ensure_clash, ctx, {
ctx.manager.update_config_in_patch(ctx.update).await?;
announce(Refresh::Clash);
Ok(())
});
ensure!(ensure_verge, _ctx, { Ok(()) });
ensure!(ensure_autostart, ctx, {
crate::core::app_effects::ensure_autostart(ctx.patch).await
});
ensure!(ensure_language, ctx, {
if let Some(language) = &ctx.patch.language {
clash_verge_i18n::set_locale(language.as_str());
async fn ensure_effect(
effect: Effect,
patch: &IVerge,
manager: &CoreManager,
update: &ConfigUpdateGuard<'_>,
) -> Result<()> {
match effect {
Effect::RestartCore => {
ensure_restart(manager).await?;
}
Effect::ClashConfig => {
manager.update_config_in_patch(update).await?;
Config::runtime().await.apply();
announce(Refresh::Clash);
}
Effect::Autostart => autostart::update_launch().await?,
Effect::Language => {
if let Some(language) = &patch.language {
clash_verge_i18n::set_locale(language.as_str());
}
}
Effect::SystemProxy => {
let _lifecycle = manager.lifecycle_lock.lock().await;
// Disabling only writes OS state and must remain available when the Core is stopped.
if Config::verge()
.await
.latest_arc()
.enable_system_proxy
.unwrap_or_default()
{
manager.apply_proxy_after_start().await?;
} else {
proxy_control::apply().await?;
proxy_control::refresh_guard().await?;
}
}
Effect::Hotkey => {
if let Some(hotkeys) = &patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
}
}
Effect::TrayMenu => tray::Tray::global().update_menu().await?,
Effect::TrayIcon => {
tray::Tray::global()
.update_icon(&Config::verge().await.latest_arc())
.await?;
#[cfg(target_os = "macos")]
if let Some(enabled) = patch.enable_tray_speed {
tray::Tray::global().update_speed_task(enabled);
}
}
Effect::TrayTooltip => tray::Tray::global().update_tooltip().await?,
Effect::TrayClick => tray::Tray::global().update_click_behavior().await?,
Effect::Lightweight => {
if patch.enable_auto_light_weight_mode.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode().await;
} else {
lightweight::disable_auto_light_weight_mode();
}
}
Effect::LogLevel => logger::Logger::global().update_log_level(patch.get_log_level())?,
Effect::LogFile => {
logger::update_log_config(
patch.app_log_max_size.unwrap_or(128),
patch.app_log_max_count.unwrap_or(8),
)
.await?;
}
}
Ok(())
});
ensure!(ensure_system_proxy, ctx, {
let _lifecycle = ctx.manager.lifecycle_lock.lock().await;
// Disabling only writes OS state and must remain available when the Core is stopped.
if Config::verge()
.await
.latest_arc()
.enable_system_proxy
.unwrap_or_default()
{
ctx.manager.apply_proxy_after_start().await
} else {
proxy_control::apply().await?;
proxy_control::refresh_guard().await
}
});
ensure!(ensure_hotkey, ctx, {
crate::core::app_effects::ensure_hotkey(ctx.patch).await
});
ensure!(ensure_tray_menu, ctx, {
crate::core::app_effects::ensure_tray_menu(ctx.patch).await
});
ensure!(ensure_tray_icon, ctx, {
crate::core::app_effects::ensure_tray_icon(ctx.patch).await
});
ensure!(ensure_tray_tooltip, ctx, {
crate::core::app_effects::ensure_tray_tooltip(ctx.patch).await
});
ensure!(ensure_tray_click, ctx, {
crate::core::app_effects::ensure_tray_click(ctx.patch).await
});
ensure!(ensure_lightweight, ctx, {
crate::core::app_effects::ensure_lightweight(ctx.patch).await
});
ensure!(ensure_log_level, ctx, {
logger::Logger::global().update_log_level(ctx.patch.get_log_level())
});
ensure!(ensure_log_file, ctx, {
logger::update_log_config(
ctx.patch.app_log_max_size.unwrap_or(128),
ctx.patch.app_log_max_count.unwrap_or(8),
)
.await
});
}
async fn apply_inner(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effects: Effects) -> Result<()> {
let manager = CoreManager::global();
@@ -126,24 +133,27 @@ async fn apply_inner(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effect
Patch::Verge { .. } => DraftTransaction::begin(vec![&verge, &runtime])?,
Patch::Clash(_) => DraftTransaction::begin(vec![&clash, &runtime])?,
};
let snapshots = capture_config_files().await?;
let original_runtime = runtime.data_arc();
let mut snapshots = capture_config_files().await?;
let empty = IVerge::default();
let context = EffectContext {
patch: match &patch {
Patch::Verge { patch, .. } => patch,
Patch::Clash(_) => &empty,
},
manager,
update: &update,
let verge_patch = match &patch {
Patch::Verge { patch, .. } => patch,
Patch::Clash(_) => &empty,
};
match &patch {
Patch::Verge { patch, .. } => verge.edit_draft(|draft| draft.patch_config(patch)),
Patch::Clash(patch) => clash.edit_draft(|draft| draft.patch_config(patch)),
}
let result: Result<()> = async {
for (effect, ensure) in EFFECT_ORDER {
if effects.contains(effect) {
ensure(&context).await?;
for &effect in EFFECT_ORDER {
if effects.contains(&effect) {
let result = Box::pin(ensure_effect(effect, verge_patch, manager, &update)).await;
if matches!(patch, Patch::Clash(_)) && matches!(effect, Effect::TrayMenu | Effect::TrayIcon) {
// Tray failures must not reject an applied Clash mode change.
logging_error!(Type::Tray, result);
} else {
result?;
}
}
}
match patch {
@@ -156,6 +166,11 @@ async fn apply_inner(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effect
.await;
if let Err(error) = result {
transaction.rollback();
if !Arc::ptr_eq(&original_runtime, &runtime.data_arc()) {
// The Core already loaded this runtime; restoring its file would only hide that state.
let runtime_path = crate::utils::dirs::app_home_dir()?.join(crate::constants::files::RUNTIME_CONFIG);
snapshots.retain(|snapshot| snapshot.path != runtime_path);
}
return match restore_files(&snapshots).await {
Ok(()) => Err(error),
Err(rollback_error) => Err(proxy_control::rollback_failure(error, rollback_error)),
@@ -173,7 +188,13 @@ async fn apply_inner(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effect
}
pub(super) async fn apply(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effects: Effects) -> Result<()> {
crate::core::notify::after_commit(apply_inner(config_write, patch, effects)).await
let runtime = Config::runtime().await;
let original = runtime.data_arc();
let result = crate::core::notify::after_commit(apply_inner(config_write, patch, effects)).await;
if result.is_err() && !Arc::ptr_eq(&original, &runtime.data_arc()) {
announce(Refresh::Clash);
}
result
}
#[cfg(test)]
+1 -5
View File
@@ -27,12 +27,8 @@ pub async fn probe_listener(request: ListenerProbe) -> Result<ListenerProbeOutco
.context("listener probe task failed")
}
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPortsOutcome> {
crate::core::notify::after_commit(Box::pin(save_proxy_ports_inner(settings))).await
}
#[allow(clippy::cognitive_complexity)]
async fn save_proxy_ports_inner(settings: ProxyPortSettings) -> Result<SaveProxyPortsOutcome> {
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPortsOutcome> {
settings.validate()?;
let _config_write = Config::lock_config_write().await;
let manager = CoreManager::global();