mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-28 17:22:15 +08:00
* 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
371 lines
13 KiB
Rust
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());
|
|
}
|
|
}
|
|
}
|