mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-24 12:42:22 +08:00
chore: sync backend review fixes into contract flow
This commit is contained in:
+7
-4
@@ -6,10 +6,13 @@ These rules supplement the repository's root `AGENTS.md`.
|
||||
its draft and persistence APIs. Patch orchestration must use
|
||||
`feat/effects.rs` and `feat/executor.rs`; refresh notifications must use
|
||||
`core::notify::announce` after successful persistence and commit. This keeps
|
||||
failed transactions from publishing state. Independent transactions retain
|
||||
their boundaries and announce via `after_commit`; runtime-only profile
|
||||
activation announces after successful application. Cross-layer operations
|
||||
must preserve these ownership and transaction boundaries.
|
||||
rolled-back transactions from publishing state. If runtime and memory state
|
||||
are already committed and cannot be rolled back, announce that state even
|
||||
when persistence fails, while preserving the persistence error. Independent
|
||||
transactions retain their boundaries and announce their own committed
|
||||
results; runtime-only profile activation announces after successful
|
||||
application. Cross-layer operations must preserve these ownership and
|
||||
transaction boundaries.
|
||||
2. **Register every new `IVerge` field.** Register the field in the
|
||||
exhaustive effect registry, including an explicit `NoEffect` where
|
||||
appropriate, and preserve its platform attributes. Manual field-to-flag
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
}
|
||||
@@ -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,4 +1,3 @@
|
||||
pub(crate) mod app_effects;
|
||||
pub mod auto_backup;
|
||||
pub mod autostart;
|
||||
pub mod backup;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user