Files
clash-verge-rev/src-tauri/src/config/config.rs
T
Tunglies 8328e34208 refactor(backend): declarative effects (#8128)
* refactor(core): type notice statuses behind the frozen wire contract

* refactor(utils): unified retry and strategy-list helpers

* refactor(feat): declarative effect registry for verge and clash patches

* refactor(core): derive refresh announcements from transaction commit

* refactor(core): fold tray, hotkey, autostart updates into effect ensures

* refactor(utils): migrate remaining bounded retry loops

* docs: add src-tauri architecture guardrails

* refactor(core): coalesce burst refresh announcements

* refactor(core): complete bounded service retry migration

* ci(backend): add architecture ban gates

* test(feat): freeze representative effect registry mappings

* docs(backend): record dead wire contract entries

* chore(core): document watcher exit conditions

* fix(backend): preserve applied state across later failures

* fix(config): retain runtime after an applied core replacement

* refactor(backend): flatten refresh publication and retry policies

* docs(backend): focus guardrails on architecture ownership

* refactor(effects): use one execution order

* fix(config): publish retained runtime and reconcile proxy settings
2026-10-07 07:38:55 +00:00

371 lines
13 KiB
Rust

use super::{IClashTemp, IProfiles, IVerge, MixedPort};
use crate::core::notify::NoticeStatus;
use crate::{
config::{PrfItem, profiles_append_item_to_safe, runtime::IRuntime},
constants::{files, timing},
core::{
CoreManager,
handle::{self, Handle},
listener::MIXED_PORT_KEY,
runtime_bundle::resolve_provider_path_conflicts,
tray,
validate::CoreConfigValidator,
},
enhance,
process::AsyncHandler,
utils::{dirs, help},
};
use anyhow::{Result, anyhow};
use backon::{ExponentialBuilder, Retryable as _};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf};
use tokio::sync::{Mutex, MutexGuard, OnceCell};
use tokio::time::sleep;
pub(crate) struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
}
static CONFIG_WRITE_LOCK: Mutex<()> = Mutex::const_new(());
impl Config {
async fn global() -> &'static Self {
static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
.get_or_init(|| async {
Self {
clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::new(IProfiles::new().await),
runtime_config: Draft::new(IRuntime::default()),
}
})
.await
}
pub async fn clash() -> Draft<IClashTemp> {
Self::global().await.clash_config.clone()
}
pub async fn verge() -> Draft<IVerge> {
Self::global().await.verge_config.clone()
}
pub async fn profiles() -> Draft<IProfiles> {
Self::global().await.profiles_config.clone()
}
pub async fn runtime() -> Draft<IRuntime> {
Self::global().await.runtime_config.clone()
}
/// Serializes transactions sharing configuration draft layers.
pub(crate) async fn lock_config_write() -> MutexGuard<'static, ()> {
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?;
let verge = Self::verge().await.latest_arc();
clash_verge_i18n::sync_locale(verge.language.as_deref());
Ok(())
}
pub(crate) async fn disable_tun_and_persist() -> Result<()> {
let verge = Self::verge().await;
verge.edit_draft(|draft| {
draft.enable_tun_mode = Some(false);
});
verge.apply();
verge.data_arc().save_file().await?;
Handle::refresh_verge();
let _ = tray::Tray::global().update_menu().await;
Ok(())
}
pub async fn init_runtime_config() -> Result<()> {
let fallback_applied = match Self::resolve_startup_mixed_port().await {
Ok(applied) => applied,
Err(error) => {
Self::block_startup_core(&error);
return Err(error);
}
};
let validation_result = if fallback_applied {
None
} else {
Self::generate_and_validate().await?
};
if let Some((msg_type, msg_content)) = validation_result {
sleep(timing::STARTUP_ERROR_DELAY).await;
handle::Handle::notice(msg_type, msg_content.as_str());
}
Self::runtime().await.apply();
logging_error!(Type::Config, Self::sync_dns_override().await);
Ok(())
}
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_arc().items.is_none() {
logging!(
warn,
Type::Config,
"Profile items 无法加载,跳过默认项初始化以保留现有配置文件"
);
return Ok(());
}
if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()));
profiles_append_item_to_safe(&profiles, merge_item).await?;
}
if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()));
profiles_append_item_to_safe(&profiles, script_item).await?;
}
Ok(())
}
async fn generate_and_validate() -> Result<Option<(NoticeStatus, String)>> {
if let Err(err) = Self::generate().await {
let error_msg: String = err.to_string().into();
logging!(error, Type::Config, "生成运行时配置失败: {}", error_msg);
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg)
.await?;
return Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg)));
}
logging!(debug, Type::Config, "生成运行时配置成功");
// Run file first: startup rewrites it via use_default_config when validation fails.
let config_result = match Self::runtime_config_yaml().await {
Ok(yaml) => Self::write_runtime_file(&yaml).await.map(|_| yaml),
Err(error) => Err(error),
};
if let Ok(yaml) = &config_result {
logging!(debug, Type::Config, "开始验证配置");
match CoreConfigValidator::global().validate_config_outcome_with(yaml).await {
Ok(outcome) if outcome.is_valid() => {
logging!(debug, Type::Config, "配置验证成功");
Ok(None)
}
Ok(outcome) => {
let error_msg: String = outcome.to_string().into();
logging!(
warn,
Type::Config,
"[首次启动] 配置验证未通过,使用默认最小配置启动: {}",
error_msg
);
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg)
.await?;
Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg)))
}
Err(err) => {
logging!(warn, Type::Config, "验证过程执行失败: {err:#}");
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateProcessTerminated, "")
.await?;
Ok(Some((NoticeStatus::ConfigValidateProcessTerminated, String::new())))
}
}
} else {
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateError, "")
.await?;
Ok(Some((NoticeStatus::ConfigValidateError, String::new())))
}
}
pub async fn generate_file() -> Result<PathBuf> {
let yaml = Self::runtime_config_yaml().await?;
Self::write_runtime_file(&yaml).await
}
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
let runtime = Self::runtime().await;
AsyncHandler::spawn_blocking(move || {
let runtime_lastest = runtime.latest_arc();
let runtime_data = runtime.data_arc();
let config = runtime_lastest
.config
.as_ref()
.or_else(|| runtime_data.config.as_ref())
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
let yaml_str = serde_yaml_ng::to_string(config)?;
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
})
.await
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
}
pub(crate) async fn write_runtime_file(yaml: &str) -> Result<PathBuf> {
let path = dirs::app_home_dir()?.join(files::RUNTIME_CONFIG);
help::save_yaml_str(&path, yaml).await?;
Ok(path)
}
pub async fn generate() -> Result<()> {
let profiles = Self::profiles().await.latest_arc();
Self::generate_with_profiles(&profiles).await
}
pub(crate) async fn generate_with_profiles(profiles: &IProfiles) -> Result<()> {
let (mut config, exists_keys, logs, dns_override) = enhance::enhance(profiles).await?;
resolve_provider_path_conflicts(&mut config, &dirs::app_home_dir()?)?;
sanitize_tunnels_proxy(&mut config);
// Apply only to generated core config so the saved choice survives the next launch.
if let Some(port) = MixedPort::session_fallback() {
config.insert(MIXED_PORT_KEY.into(), port.into());
}
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(config),
dns_override: Some(dns_override),
exists_keys,
chain_logs: logs,
}
});
Ok(())
}
pub async fn verify_config_initialization() {
if Self::startup_core_block_reason().is_some() {
return;
}
let backoff = ExponentialBuilder::default()
.with_min_delay(std::time::Duration::from_millis(100))
.with_max_delay(std::time::Duration::from_secs(2))
.with_factor(2.0)
.with_max_times(10);
if let Err(e) = (|| async {
if Self::runtime().await.latest_arc().config.is_some() {
return Ok::<(), anyhow::Error>(());
}
Self::generate().await
})
.retry(backoff)
.await
{
logging!(error, Type::Setup, "Config init verification failed: {e:#}");
}
}
/// Commits drafts during exit/restart/shutdown so user changes are not lost.
pub async fn apply_all_and_save_file() {
logging!(debug, Type::Config, "save all draft data");
let save_clash_task = AsyncHandler::spawn(|| async {
let clash = Self::clash().await;
clash.apply();
logging_error!(Type::Config, clash.data_arc().save_config().await);
});
let save_verge_task = AsyncHandler::spawn(|| async {
let verge = Self::verge().await;
verge.apply();
logging_error!(Type::Config, verge.data_arc().save_file().await);
});
let save_profiles_task = AsyncHandler::spawn(|| async {
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
let profiles = Self::profiles().await;
profiles.apply();
logging_error!(Type::Config, profiles.data_arc().save_file().await);
});
let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task);
logging!(info, Type::Config, "save all draft data finished");
}
}
fn sanitize_tunnels_proxy(config: &mut Mapping) {
if !config
.get("tunnels")
.and_then(|v| v.as_sequence())
.is_some_and(|t| tunnels_need_validation(t))
{
return;
}
let mut valid: HashSet<String> = HashSet::with_capacity(64);
collect_names(config, "proxies", &mut valid);
collect_names(config, "proxy-groups", &mut valid);
valid.insert("DIRECT".into());
valid.insert("REJECT".into());
let Some(tunnels) = config.get_mut("tunnels").and_then(|v| v.as_sequence_mut()) else {
return;
};
for item in tunnels {
let Some(tunnel) = item.as_mapping_mut() else { continue };
let Some(proxy_name) = tunnel.get("proxy").and_then(|v| v.as_str()) else {
continue;
};
if proxy_name == "DIRECT" || proxy_name == "REJECT" {
continue;
}
if !valid.contains(proxy_name) {
tunnel.remove("proxy");
}
}
}
fn tunnels_need_validation(tunnels: &[Value]) -> bool {
tunnels.iter().any(|item| {
item.as_mapping()
.and_then(|t| t.get("proxy"))
.and_then(|p| p.as_str())
.is_some_and(|name| name != "DIRECT" && name != "REJECT")
})
}
fn collect_names(config: &Mapping, list_key: &str, out: &mut HashSet<String>) {
let Some(Value::Sequence(seq)) = config.get(list_key) else {
return;
};
for item in seq {
let Value::Mapping(map) = item else {
continue;
};
if let Some(Value::String(n)) = map.get("name")
&& !n.is_empty()
{
out.insert(n.into());
}
}
}