mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-03 05:22:09 +08:00
refactor(feat): declarative effect registry for verge and clash patches
This commit is contained in:
@@ -70,6 +70,12 @@ impl Config {
|
||||
CONFIG_WRITE_LOCK.lock().await
|
||||
}
|
||||
|
||||
pub(crate) fn try_lock_config_write() -> Result<MutexGuard<'static, ()>> {
|
||||
CONFIG_WRITE_LOCK
|
||||
.try_lock()
|
||||
.map_err(|_| anyhow!("configuration update is already running"))
|
||||
}
|
||||
|
||||
pub async fn init_config_before_window() -> Result<()> {
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
|
||||
@@ -130,6 +130,30 @@ where
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub(crate) fn claim_config_update(
|
||||
&self,
|
||||
_config_write: &tokio::sync::MutexGuard<'_, ()>,
|
||||
) -> Result<ConfigUpdateGuard<'_>> {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
}
|
||||
Ok(ConfigUpdateGuard(self))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_config_in_patch(&self, _update: &ConfigUpdateGuard<'_>) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
anyhow::bail!("application is exiting");
|
||||
}
|
||||
self.set_last_update(Instant::now());
|
||||
Config::generate().await?;
|
||||
let outcome = self.validate_and_apply_draft().await?;
|
||||
if outcome.is_valid() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("{outcome}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn use_default_config(&self, status: NoticeStatus, message: &str) -> Result<()> {
|
||||
use crate::constants::files::RUNTIME_CONFIG;
|
||||
|
||||
@@ -300,6 +324,14 @@ impl CoreManager {
|
||||
|
||||
/// Validates and applies the caller's transaction, committing only on success.
|
||||
async fn validate_and_apply(&self, transaction: DraftTransaction<'_>) -> Result<ValidationOutcome> {
|
||||
let outcome = self.validate_and_apply_draft().await?;
|
||||
if outcome.is_valid() {
|
||||
transaction.commit();
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
async fn validate_and_apply_draft(&self) -> Result<ValidationOutcome> {
|
||||
// One serialization feeds check and run files; the core never applies unvalidated bytes.
|
||||
let yaml = Config::runtime_config_yaml().await?;
|
||||
let outcome = CoreConfigValidator::global()
|
||||
@@ -311,7 +343,6 @@ impl CoreManager {
|
||||
|
||||
let run_path = Config::write_runtime_file(&yaml).await?;
|
||||
self.apply_config(run_path).await?;
|
||||
transaction.commit();
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod config;
|
||||
pub(crate) use config::ConfigUpdateGuard;
|
||||
mod lifecycle;
|
||||
mod state;
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ mod tests {
|
||||
use super::NoticeStatus;
|
||||
|
||||
#[test]
|
||||
fn notice_status_wire_contract() {
|
||||
fn notice_status_wire_contract() -> Result<(), serde_json::Error> {
|
||||
let statuses = [
|
||||
NoticeStatus::Info,
|
||||
NoticeStatus::ImportSubUrlOk,
|
||||
@@ -199,8 +199,8 @@ mod tests {
|
||||
let actual: Vec<_> = statuses
|
||||
.into_iter()
|
||||
.map(serde_json::to_value)
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
.collect::<Result<_, _>>()?;
|
||||
assert_eq!(actual, expected.map(serde_json::Value::from));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ mod wire_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frontend_event_wire_contract() {
|
||||
fn frontend_event_wire_contract() -> Result<(), serde_json::Error> {
|
||||
let uid = String::from("profile");
|
||||
let events = [
|
||||
(
|
||||
@@ -387,7 +387,8 @@ mod wire_tests {
|
||||
for (event, name, payload) in events {
|
||||
let (actual_name, actual_payload) = NotificationSystem::serialize_event(event);
|
||||
assert_eq!(actual_name, name);
|
||||
assert_eq!(actual_payload.unwrap(), payload);
|
||||
assert_eq!(actual_payload?, payload);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+19
-295
@@ -1,279 +1,17 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::{CoreManager, autostart, handle, hotkey, logger, proxy_control, tray},
|
||||
module::{auto_backup::AutoBackupManager, lightweight},
|
||||
};
|
||||
use crate::config::{Config, IVerge};
|
||||
use anyhow::Result;
|
||||
use bitflags::bitflags;
|
||||
use clash_verge_draft::{DraftTransaction, SharedDraft};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_draft::SharedDraft;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use tokio::sync::MutexGuard;
|
||||
|
||||
/// Patch Clash configuration
|
||||
pub async fn patch_clash(patch: &Mapping) -> Result<()> {
|
||||
Config::clash().await.edit_draft(|d| d.patch_config(patch));
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else if patch.get("allow-lan").is_some() {
|
||||
CoreManager::global().update_config_checked().await?;
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
tray::Tray::global().update_menu_and_icon().await;
|
||||
}
|
||||
CoreManager::global().update_config_checked().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().await.apply();
|
||||
// 分离数据获取和异步调用
|
||||
let clash_data = Config::clash().await.data_arc();
|
||||
clash_data.save_config().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().await.discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define update flags as bitflags for better performance
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
struct UpdateFlags: u16 {
|
||||
const RESTART_CORE = 1 << 0;
|
||||
const CLASH_CONFIG = 1 << 1;
|
||||
const VERGE_CONFIG = 1 << 2;
|
||||
const LAUNCH = 1 << 3;
|
||||
const SYS_PROXY = 1 << 4;
|
||||
const SYSTRAY_ICON = 1 << 5;
|
||||
const HOTKEY = 1 << 6;
|
||||
const SYSTRAY_MENU = 1 << 7;
|
||||
const SYSTRAY_TOOLTIP = 1 << 8;
|
||||
const SYSTRAY_CLICK_BEHAVIOR = 1 << 9;
|
||||
const LIGHT_WEIGHT = 1 << 10;
|
||||
const LANGUAGE = 1 << 11;
|
||||
const LOG_LEVEL = 1 << 12;
|
||||
const LOG_FILE = 1 << 13;
|
||||
|
||||
const GROUP_SYS_TRAY = Self::SYSTRAY_MENU.bits()
|
||||
| Self::SYSTRAY_TOOLTIP.bits()
|
||||
| Self::SYSTRAY_ICON.bits();
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_update_flags(patch: &IVerge) -> UpdateFlags {
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = &patch.pac_file_content;
|
||||
let proxy_bypass = &patch.system_proxy_bypass;
|
||||
let language = &patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = &patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||
let tun_tray_icon = patch.tun_tray_icon;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = patch.verge_redir_enabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_port = patch.verge_redir_port;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = patch.verge_tproxy_enabled;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_port = patch.verge_tproxy_port;
|
||||
let socks_enabled = patch.verge_socks_enabled;
|
||||
let socks_port = patch.verge_socks_port;
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let enable_tray_speed: Option<bool> = None;
|
||||
// let enable_tray_icon = patch.enable_tray_icon;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
let tray_event = &patch.tray_event;
|
||||
let home_cards = patch.home_cards.as_ref();
|
||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||
let enable_external_controller = patch.enable_external_controller;
|
||||
let tray_proxy_groups_display_mode = &patch.tray_proxy_groups_display_mode;
|
||||
let tray_inline_outbound_modes = patch.tray_inline_outbound_modes;
|
||||
let enable_proxy_guard = patch.enable_proxy_guard;
|
||||
let proxy_guard_duration = patch.proxy_guard_duration;
|
||||
let log_level = &patch.app_log_level;
|
||||
let log_max_size = patch.app_log_max_size;
|
||||
let log_max_count = patch.app_log_max_count;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let restart_core_needed = socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
|| enable_external_controller.is_some();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let mut restart_core_needed = socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
|| enable_external_controller.is_some();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
restart_core_needed |= redir_enabled.is_some() || redir_port.is_some();
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
restart_core_needed |= tproxy_enabled.is_some() || tproxy_port.is_some();
|
||||
restart_core_needed |= tun_mode == Some(true);
|
||||
}
|
||||
|
||||
let mut update_flags = UpdateFlags::empty();
|
||||
if restart_core_needed {
|
||||
update_flags.insert(UpdateFlags::RESTART_CORE);
|
||||
}
|
||||
if tun_mode.is_some() {
|
||||
update_flags.insert(UpdateFlags::CLASH_CONFIG | UpdateFlags::GROUP_SYS_TRAY);
|
||||
}
|
||||
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||
update_flags.insert(UpdateFlags::VERGE_CONFIG);
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
update_flags.insert(UpdateFlags::LAUNCH);
|
||||
}
|
||||
if system_proxy.is_some() {
|
||||
update_flags.insert(UpdateFlags::SYS_PROXY | UpdateFlags::GROUP_SYS_TRAY);
|
||||
}
|
||||
if proxy_bypass.is_some()
|
||||
|| pac_content.is_some()
|
||||
|| pac.is_some()
|
||||
|| enable_proxy_guard.is_some()
|
||||
|| proxy_guard_duration.is_some()
|
||||
{
|
||||
update_flags.insert(UpdateFlags::SYS_PROXY);
|
||||
}
|
||||
if language.is_some() {
|
||||
update_flags.insert(UpdateFlags::LANGUAGE | UpdateFlags::SYSTRAY_MENU | UpdateFlags::SYSTRAY_TOOLTIP);
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
{
|
||||
update_flags.insert(UpdateFlags::SYSTRAY_ICON);
|
||||
}
|
||||
if patch.hotkeys.is_some() {
|
||||
update_flags.insert(UpdateFlags::HOTKEY | UpdateFlags::SYSTRAY_MENU);
|
||||
}
|
||||
if tray_event.is_some() {
|
||||
update_flags.insert(UpdateFlags::SYSTRAY_CLICK_BEHAVIOR);
|
||||
}
|
||||
if enable_auto_light_weight.is_some() {
|
||||
update_flags.insert(UpdateFlags::LIGHT_WEIGHT);
|
||||
}
|
||||
if tray_proxy_groups_display_mode.is_some() {
|
||||
update_flags.insert(UpdateFlags::SYSTRAY_MENU);
|
||||
}
|
||||
if log_level.is_some() {
|
||||
update_flags.insert(UpdateFlags::LOG_LEVEL);
|
||||
}
|
||||
if log_max_size.is_some() || log_max_count.is_some() {
|
||||
update_flags.insert(UpdateFlags::LOG_FILE);
|
||||
}
|
||||
if tray_inline_outbound_modes.is_some() {
|
||||
update_flags.insert(UpdateFlags::SYSTRAY_MENU);
|
||||
}
|
||||
|
||||
update_flags
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn process_terminated_flags(update_flags: UpdateFlags, patch: &IVerge) -> Result<()> {
|
||||
// Process updates based on flags
|
||||
if update_flags.contains(UpdateFlags::RESTART_CORE) {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::CLASH_CONFIG) {
|
||||
CoreManager::global().update_config_checked().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::LAUNCH) {
|
||||
autostart::update_launch().await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::LANGUAGE)
|
||||
&& let Some(language) = &patch.language
|
||||
{
|
||||
clash_verge_i18n::set_locale(language.as_str());
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::SYS_PROXY) {
|
||||
let manager = CoreManager::global();
|
||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||
// Turning it off only writes OS state, so it must stay available while the Core is down.
|
||||
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?;
|
||||
}
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::HOTKEY)
|
||||
&& let Some(hotkeys) = &patch.hotkeys
|
||||
{
|
||||
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::SYSTRAY_MENU) {
|
||||
tray::Tray::global().update_menu().await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::SYSTRAY_ICON) {
|
||||
tray::Tray::global()
|
||||
.update_icon(&Config::verge().await.latest_arc())
|
||||
.await?;
|
||||
#[cfg(target_os = "macos")]
|
||||
if patch.enable_tray_speed.is_some() {
|
||||
tray::Tray::global().update_speed_task(patch.enable_tray_speed.unwrap_or(false));
|
||||
}
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::SYSTRAY_TOOLTIP) {
|
||||
tray::Tray::global().update_tooltip().await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::SYSTRAY_CLICK_BEHAVIOR) {
|
||||
tray::Tray::global().update_click_behavior().await?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::LIGHT_WEIGHT) {
|
||||
if patch.enable_auto_light_weight_mode.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode().await;
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::LOG_LEVEL) {
|
||||
logger::Logger::global().update_log_level(patch.get_log_level())?;
|
||||
}
|
||||
if update_flags.contains(UpdateFlags::LOG_FILE) {
|
||||
let log_max_size = patch.app_log_max_size.unwrap_or(128);
|
||||
let log_max_count = patch.app_log_max_count.unwrap_or(8);
|
||||
logger::update_log_config(log_max_size, log_max_count).await?;
|
||||
}
|
||||
Ok(())
|
||||
let config_write = Config::try_lock_config_write()?;
|
||||
super::executor::apply(
|
||||
&config_write,
|
||||
super::executor::Patch::Clash(patch),
|
||||
super::effects::clash_effects(patch),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Apply a patch, then reconcile TUN when its setting changes.
|
||||
@@ -289,7 +27,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
||||
|
||||
/// Apply a patch without post-update reconciliation.
|
||||
pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
||||
let config_write = Config::lock_config_write().await;
|
||||
let config_write = Config::try_lock_config_write()?;
|
||||
apply_verge_patch_locked(&config_write, patch, not_save_file).await
|
||||
}
|
||||
|
||||
@@ -300,29 +38,15 @@ pub(super) async fn apply_verge_patch_locked(
|
||||
patch: &IVerge,
|
||||
not_save_file: bool,
|
||||
) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
// Hold the claim across side effects so concurrent transactions cannot share this draft.
|
||||
let transaction = DraftTransaction::begin(vec![&verge])?;
|
||||
verge.edit_draft(|d| d.patch_config(patch));
|
||||
|
||||
let update_flags = determine_update_flags(patch);
|
||||
logging!(debug, Type::Setup, "Determined update flags: {:?}", update_flags);
|
||||
// A failed patch rolls back to what the user already had; it never invents a value for them.
|
||||
process_terminated_flags(update_flags, patch).await?;
|
||||
transaction.commit();
|
||||
announce_verge_change();
|
||||
|
||||
logging_error!(Type::Backup, AutoBackupManager::global().refresh_settings().await);
|
||||
if !not_save_file {
|
||||
// 分离数据获取和异步调用
|
||||
let verge_data = verge.data_arc();
|
||||
verge_data.save_file().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn announce_verge_change() {
|
||||
handle::Handle::refresh_verge();
|
||||
super::executor::apply(
|
||||
_config_write,
|
||||
super::executor::Patch::Verge {
|
||||
patch,
|
||||
persist: !not_save_file,
|
||||
},
|
||||
super::effects::verge_effects(patch),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_verge_config() -> Result<SharedDraft<IVerge>> {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
use crate::config::IVerge;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum Effect {
|
||||
RestartCore,
|
||||
ClashConfig,
|
||||
VergeConfig,
|
||||
Autostart,
|
||||
SystemProxy,
|
||||
TrayIcon,
|
||||
Hotkey,
|
||||
TrayMenu,
|
||||
TrayTooltip,
|
||||
TrayClick,
|
||||
Lightweight,
|
||||
Language,
|
||||
LogLevel,
|
||||
LogFile,
|
||||
}
|
||||
|
||||
pub(super) type Effects = BTreeSet<Effect>;
|
||||
|
||||
macro_rules! select_field {
|
||||
($effects:ident, $field:ident, NoEffect) => { let _ = $field; };
|
||||
($effects:ident, $field:ident, [$($effect:ident),*]) => {
|
||||
if $field.is_some() { $effects.extend([$(Effect::$effect),*]); }
|
||||
};
|
||||
($effects:ident, $field:ident, Tun) => {
|
||||
select_field!($effects, $field, [ClashConfig, TrayMenu, TrayTooltip, TrayIcon]);
|
||||
#[cfg(target_os = "linux")]
|
||||
if *$field == Some(true) { $effects.insert(Effect::RestartCore); }
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! verge_registry {
|
||||
($( $(#[$cfg:meta])* $field:ident => $rule:tt; )*) => {
|
||||
pub(super) fn verge_effects(patch: &IVerge) -> Effects {
|
||||
// No rest pattern: even fields omitted by serde must be classified at compile time.
|
||||
let IVerge { $( $(#[$cfg])* $field, )* } = patch;
|
||||
let mut effects = Effects::new();
|
||||
$( $(#[$cfg])* { select_field!(effects, $field, $rule); } )*
|
||||
effects
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const VERGE_FIELDS: &[&str] = &[ $( $(#[$cfg])* stringify!($field), )* ];
|
||||
};
|
||||
}
|
||||
|
||||
verge_registry! {
|
||||
app_log_level => [LogLevel];
|
||||
app_log_max_size => [LogFile];
|
||||
app_log_max_count => [LogFile];
|
||||
language => [Language, TrayMenu, TrayTooltip];
|
||||
theme_mode => NoEffect;
|
||||
tray_event => [TrayClick];
|
||||
env_type => NoEffect;
|
||||
start_page => NoEffect;
|
||||
startup_script => NoEffect;
|
||||
traffic_graph => NoEffect;
|
||||
enable_memory_usage => NoEffect;
|
||||
enable_group_icon => NoEffect;
|
||||
pause_render_traffic_stats_on_blur => NoEffect;
|
||||
common_tray_icon => [TrayIcon];
|
||||
#[cfg(target_os = "macos")]
|
||||
tray_icon => [TrayIcon];
|
||||
menu_icon => NoEffect;
|
||||
menu_order => NoEffect;
|
||||
proxy_group_tools_position => NoEffect;
|
||||
notice_position => NoEffect;
|
||||
collapse_navbar => NoEffect;
|
||||
sysproxy_tray_icon => [TrayIcon];
|
||||
tun_tray_icon => [TrayIcon];
|
||||
enable_tun_mode => Tun;
|
||||
enable_auto_launch => [Autostart];
|
||||
enable_silent_start => NoEffect;
|
||||
enable_system_proxy => [SystemProxy, TrayMenu, TrayTooltip, TrayIcon];
|
||||
enable_proxy_guard => [SystemProxy];
|
||||
enable_bypass_check => NoEffect;
|
||||
enable_dns_settings => NoEffect;
|
||||
profile_dns_settings => NoEffect;
|
||||
use_default_bypass => NoEffect;
|
||||
system_proxy_bypass => [SystemProxy];
|
||||
proxy_guard_duration => [SystemProxy];
|
||||
proxy_auto_config => [SystemProxy];
|
||||
pac_file_content => [SystemProxy];
|
||||
proxy_host => NoEffect;
|
||||
theme_setting => NoEffect;
|
||||
web_ui_list => NoEffect;
|
||||
clash_core => NoEffect;
|
||||
hotkeys => [Hotkey, TrayMenu];
|
||||
enable_global_hotkey => [VergeConfig];
|
||||
home_cards => [VergeConfig];
|
||||
auto_close_connection => NoEffect;
|
||||
auto_check_update => NoEffect;
|
||||
default_latency_test => NoEffect;
|
||||
default_latency_timeout => NoEffect;
|
||||
enable_auto_delay_detection => NoEffect;
|
||||
auto_delay_detection_interval_minutes => NoEffect;
|
||||
enable_builtin_enhanced => NoEffect;
|
||||
proxy_layout_column => NoEffect;
|
||||
test_list => NoEffect;
|
||||
auto_log_clean => NoEffect;
|
||||
enable_auto_backup_schedule => NoEffect;
|
||||
auto_backup_interval_hours => NoEffect;
|
||||
auto_backup_on_change => NoEffect;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_port => [RestartCore];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_enabled => [RestartCore];
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_port => [RestartCore];
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_enabled => [RestartCore];
|
||||
verge_mixed_port => [RestartCore];
|
||||
verge_socks_port => [RestartCore];
|
||||
verge_socks_enabled => [RestartCore];
|
||||
verge_port => [RestartCore];
|
||||
verge_http_enabled => [RestartCore];
|
||||
webdav_url => NoEffect;
|
||||
webdav_username => NoEffect;
|
||||
webdav_password => NoEffect;
|
||||
#[cfg(target_os = "macos")]
|
||||
enable_tray_speed => [TrayIcon];
|
||||
tray_proxy_groups_display_mode => [TrayMenu];
|
||||
tray_inline_outbound_modes => [TrayMenu];
|
||||
enable_auto_light_weight_mode => [Lightweight];
|
||||
auto_light_weight_minutes => NoEffect;
|
||||
enable_hover_jump_navigator => NoEffect;
|
||||
hover_jump_navigator_delay => NoEffect;
|
||||
enable_external_controller => [RestartCore];
|
||||
}
|
||||
|
||||
pub(super) fn clash_effects(patch: &serde_yaml_ng::Mapping) -> Effects {
|
||||
use clash_verge_logging::{Type, logging};
|
||||
let mut effects = Effects::new();
|
||||
for key in patch.keys() {
|
||||
effects.extend(match key.as_str() {
|
||||
Some("secret" | "external-controller") => &[Effect::RestartCore][..],
|
||||
Some("allow-lan") => &[Effect::ClashConfig][..],
|
||||
Some("mode") => &[Effect::ClashConfig, Effect::TrayMenu, Effect::TrayIcon][..],
|
||||
_ => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Unregistered Clash configuration key {key:?}; reloading configuration"
|
||||
);
|
||||
&[Effect::ClashConfig][..]
|
||||
}
|
||||
});
|
||||
}
|
||||
// These branches were exclusive: restart supersedes reload, and allow-lan precedes mode.
|
||||
if effects.contains(&Effect::RestartCore) {
|
||||
effects.retain(|effect| *effect == Effect::RestartCore);
|
||||
} else if patch.contains_key("allow-lan") {
|
||||
effects.retain(|effect| *effect == Effect::ClashConfig);
|
||||
}
|
||||
if patch.is_empty() {
|
||||
effects.insert(Effect::ClashConfig);
|
||||
}
|
||||
effects
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_serialized_verge_field_is_registered() -> Result<(), serde_json::Error> {
|
||||
let fields: std::collections::BTreeMap<String, serde_json::Value> =
|
||||
serde_json::from_value(serde_json::to_value(IVerge::default())?)?;
|
||||
for field in fields.keys() {
|
||||
assert!(VERGE_FIELDS.contains(&field.as_str()), "unregistered field: {field}");
|
||||
}
|
||||
let unique: BTreeSet<_> = VERGE_FIELDS.iter().collect();
|
||||
assert_eq!(unique.len(), VERGE_FIELDS.len());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use super::effects::{Effect, Effects};
|
||||
use crate::{
|
||||
config::{
|
||||
Config, IVerge,
|
||||
snapshot::{capture_config_files, restore_files},
|
||||
},
|
||||
core::{CoreManager, autostart, handle::Handle, hotkey, logger, manager::ConfigUpdateGuard, proxy_control, tray},
|
||||
module::{auto_backup::AutoBackupManager, lightweight},
|
||||
};
|
||||
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 tokio::sync::MutexGuard;
|
||||
|
||||
pub(super) enum Patch<'a> {
|
||||
Verge { patch: &'a IVerge, persist: bool },
|
||||
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),
|
||||
];
|
||||
|
||||
macro_rules! ensure {
|
||||
($name:ident, $ctx:ident, $body:block) => {
|
||||
fn $name<'a>($ctx: &'a EffectContext<'_>) -> BoxFuture<'a, Result<()>> {
|
||||
Box::pin(async move $body)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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?;
|
||||
Handle::refresh_clash();
|
||||
Ok(())
|
||||
});
|
||||
ensure!(ensure_verge, _ctx, { Ok(()) });
|
||||
ensure!(ensure_autostart, _ctx, { autostart::update_launch().await });
|
||||
ensure!(ensure_language, ctx, {
|
||||
if let Some(language) = &ctx.patch.language {
|
||||
clash_verge_i18n::set_locale(language.as_str());
|
||||
}
|
||||
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, {
|
||||
if let Some(hotkeys) = &ctx.patch.hotkeys {
|
||||
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
ensure!(ensure_tray_menu, _ctx, { tray::Tray::global().update_menu().await });
|
||||
ensure!(ensure_tray_icon, ctx, {
|
||||
tray::Tray::global()
|
||||
.update_icon(&Config::verge().await.latest_arc())
|
||||
.await?;
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(enabled) = ctx.patch.enable_tray_speed {
|
||||
tray::Tray::global().update_speed_task(enabled);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = ctx;
|
||||
Ok(())
|
||||
});
|
||||
ensure!(ensure_tray_tooltip, _ctx, {
|
||||
tray::Tray::global().update_tooltip().await
|
||||
});
|
||||
ensure!(ensure_tray_click, _ctx, {
|
||||
tray::Tray::global().update_click_behavior().await
|
||||
});
|
||||
ensure!(ensure_lightweight, ctx, {
|
||||
if ctx.patch.enable_auto_light_weight_mode.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode().await;
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
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
|
||||
});
|
||||
|
||||
pub(super) async fn apply(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, effects: Effects) -> Result<()> {
|
||||
let manager = CoreManager::global();
|
||||
let update = manager.claim_config_update(config_write)?;
|
||||
let clash = Config::clash().await;
|
||||
let verge = Config::verge().await;
|
||||
let runtime = Config::runtime().await;
|
||||
let transaction = match patch {
|
||||
Patch::Verge { .. } => DraftTransaction::begin(vec![&verge, &runtime])?,
|
||||
Patch::Clash(_) => DraftTransaction::begin(vec![&clash, &runtime])?,
|
||||
};
|
||||
let 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,
|
||||
};
|
||||
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?;
|
||||
}
|
||||
}
|
||||
match patch {
|
||||
Patch::Verge { persist: true, .. } => verge.latest_arc().save_file().await?,
|
||||
Patch::Verge { persist: false, .. } => {}
|
||||
Patch::Clash(_) => clash.latest_arc().save_config().await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
transaction.rollback();
|
||||
return match restore_files(&snapshots).await {
|
||||
Ok(()) => Err(error),
|
||||
Err(rollback_error) => Err(proxy_control::rollback_failure(error, rollback_error)),
|
||||
};
|
||||
}
|
||||
transaction.commit();
|
||||
match patch {
|
||||
Patch::Verge { .. } => {
|
||||
logging_error!(Type::Backup, AutoBackupManager::global().refresh_settings().await);
|
||||
Handle::refresh_verge();
|
||||
}
|
||||
Patch::Clash(_) => Handle::refresh_clash(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_claim_is_exclusive_and_released_on_drop() -> Result<()> {
|
||||
let write_lock = tokio::sync::Mutex::new(());
|
||||
let write = write_lock.lock().await;
|
||||
let manager = CoreManager::default();
|
||||
let first = manager.claim_config_update(&write)?;
|
||||
assert!(manager.claim_config_update(&write).is_err());
|
||||
drop(first);
|
||||
assert!(manager.claim_config_update(&write).is_ok());
|
||||
drop(write);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ mod clash;
|
||||
mod config;
|
||||
mod core_upgrade;
|
||||
mod dns;
|
||||
mod effects;
|
||||
mod executor;
|
||||
mod icon;
|
||||
mod listener;
|
||||
mod profile;
|
||||
|
||||
@@ -42,9 +42,9 @@ pub enum RetryError<E> {
|
||||
Stop(E),
|
||||
}
|
||||
|
||||
pub async fn retry<T, E, Fut: Future<Output = Result<T, RetryError<E>>>>(
|
||||
pub async fn retry<T: Send, E: Send, Fut: Future<Output = Result<T, RetryError<E>>> + Send>(
|
||||
policy: RetryPolicy,
|
||||
mut operation: impl FnMut(usize) -> Fut,
|
||||
mut operation: impl FnMut(usize) -> Fut + Send,
|
||||
) -> Result<T, E> {
|
||||
let mut result = operation(0).await;
|
||||
for (index, delay) in policy.delays().enumerate() {
|
||||
@@ -63,10 +63,16 @@ pub async fn retry<T, E, Fut: Future<Output = Result<T, RetryError<E>>>>(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn try_strategies<S: Copy, T, E, Fut: Future<Output = Result<T, E>>, const N: usize>(
|
||||
pub async fn try_strategies<
|
||||
S: Copy + Send,
|
||||
T: Send,
|
||||
E: Send,
|
||||
Fut: Future<Output = Result<T, E>> + Send,
|
||||
const N: usize,
|
||||
>(
|
||||
first: S,
|
||||
rest: [S; N],
|
||||
mut operation: impl FnMut(S) -> Fut,
|
||||
mut operation: impl FnMut(S) -> Fut + Send,
|
||||
) -> Result<(S, T), E> {
|
||||
retry(
|
||||
RetryPolicy {
|
||||
@@ -74,7 +80,7 @@ pub async fn try_strategies<S: Copy, T, E, Fut: Future<Output = Result<T, E>>, c
|
||||
backoff: Backoff::Fixed(Duration::ZERO),
|
||||
jitter: false,
|
||||
},
|
||||
|attempt| {
|
||||
move |attempt| {
|
||||
let strategy = if attempt == 0 { first } else { rest[attempt - 1] };
|
||||
let future = operation(strategy);
|
||||
async move { future.await.map(|value| (strategy, value)).map_err(RetryError::Retry) }
|
||||
@@ -90,7 +96,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn retries_stop_at_success_terminal_error_or_attempt_limit() {
|
||||
let policy = RetryPolicy {
|
||||
attempts: NonZeroUsize::new(3).unwrap(),
|
||||
attempts: NonZeroUsize::MIN.saturating_add(2),
|
||||
backoff: Backoff::Fixed(Duration::ZERO),
|
||||
jitter: false,
|
||||
};
|
||||
@@ -138,7 +144,7 @@ mod tests {
|
||||
#[test]
|
||||
fn backoff_keeps_the_attempt_budget_and_exponential_cap() {
|
||||
let policy = RetryPolicy {
|
||||
attempts: NonZeroUsize::new(5).unwrap(),
|
||||
attempts: NonZeroUsize::MIN.saturating_add(4),
|
||||
backoff: Backoff::Exponential {
|
||||
base: Duration::from_millis(10),
|
||||
cap: Duration::from_millis(25),
|
||||
|
||||
Reference in New Issue
Block a user