mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-11 04:42:09 +08:00
fix(backend): preserve applied state across later failures
This commit is contained in:
+124
-103
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user