From b7c4f7d39b6542da6bafbb4bd45d8b963d304bd0 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Tue, 25 Aug 2026 07:19:17 +0000 Subject: [PATCH] refactor: api cleanup (#7812) * refactor(config): unify YAML persistence * refactor(config): remove redundant config APIs * refactor(config): remove unused config APIs * refactor(config): narrow profile APIs * fix(config): preserve profiles on invalid reorder * refactor(startup): narrow resolve helpers * refactor(config): unify mixed port defaults * refactor(network): simplify response APIs * refactor(config): make profile builders infallible * refactor(config): make profile activation infallible * refactor(api): simplify internal runtime APIs * refactor: consolidate internal config and window APIs --- src-tauri/src/cmd/clash.rs | 20 +-- src-tauri/src/cmd/profile.rs | 4 +- src-tauri/src/cmd/save_profile.rs | 2 +- src-tauri/src/config/clash.rs | 54 ++----- src-tauri/src/config/config.rs | 21 +-- src-tauri/src/config/mixed_port.rs | 3 - src-tauri/src/config/mod.rs | 3 +- src-tauri/src/config/port.rs | 10 +- src-tauri/src/config/prfitem.rs | 89 +++++------ src-tauri/src/config/profiles.rs | 169 ++++++++++----------- src-tauri/src/config/runtime.rs | 5 - src-tauri/src/config/verge.rs | 9 +- src-tauri/src/core/manager/mod.rs | 2 +- src-tauri/src/core/timer.rs | 6 +- src-tauri/src/core/tray/mod.rs | 2 +- src-tauri/src/enhance/chain.rs | 6 +- src-tauri/src/enhance/mod.rs | 2 +- src-tauri/src/feat/core_upgrade.rs | 6 +- src-tauri/src/feat/profile.rs | 15 +- src-tauri/src/lib.rs | 7 +- src-tauri/src/utils/dirs.rs | 14 +- src-tauri/src/utils/help.rs | 73 ++++++--- src-tauri/src/utils/mod.rs | 2 +- src-tauri/src/utils/network.rs | 35 +---- src-tauri/src/utils/resolve/mod.rs | 61 ++++---- src-tauri/src/utils/resolve/scheme.rs | 2 +- src-tauri/src/utils/server.rs | 34 +---- src-tauri/src/utils/window_manager.rs | 27 +--- src/components/setting/mods/dns-viewer.tsx | 7 +- 29 files changed, 277 insertions(+), 413 deletions(-) diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 8ecb31b5f..e16046540 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -203,27 +203,17 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { } #[tauri::command] -pub fn check_dns_config_exists() -> CmdResult { - use crate::utils::dirs; - - let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); - - Ok(dns_path.exists()) -} - -#[tauri::command] -pub async fn get_dns_config_content() -> CmdResult { +pub async fn get_dns_config_content() -> CmdResult> { use crate::utils::dirs; use tokio::fs; let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); - if !fs::try_exists(&dns_path).await.stringify_err()? { - return Err("DNS config file not found".into()); + match fs::read_to_string(&dns_path).await { + Ok(content) => Ok(Some(content.into())), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(CommandFailure::plain(error)), } - - let content = fs::read_to_string(&dns_path).await.stringify_err()?.into(); - Ok(content) } #[tauri::command] diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index ef4de1d54..3d8dbf3ff 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -177,7 +177,7 @@ pub async fn delete_profile(index: String) -> CmdResult { } }; if should_update { - logging_error!(Type::Config, profiles::activate_selected_nodes()); + profiles::activate_selected_nodes(); } drop(config_update_guard); drop(profile_write_guard); @@ -241,7 +241,7 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult bool { - let Some(current_uid) = profiles.get_current() else { + let Some(current_uid) = profiles.current.as_ref() else { return false; }; if current_uid == index { diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 835862a80..9ddc5558d 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -1,4 +1,3 @@ -use crate::config::Config; use crate::constants::{network, tun as tun_const}; use crate::utils::dirs::{path_to_str, sidecar_ipc_path}; use crate::utils::{dirs, help}; @@ -118,7 +117,7 @@ impl IClashTemp { let mixed_port = Self::guard_mixed_port(&config); let socks_port = Self::guard_socks_port(&config); let port = Self::guard_port(&config); - let ctrl = Self::guard_external_controller(&config); + let ctrl = Self::guard_server_ctrl(&config); #[cfg(unix)] let external_controller_unix = Self::guard_external_controller_ipc(); #[cfg(windows)] @@ -154,16 +153,6 @@ impl IClashTemp { Self::guard_mixed_port(&self.0) } - #[allow(unused)] - pub fn get_socks_port(&self) -> u16 { - Self::guard_socks_port(&self.0) - } - - #[allow(unused)] - pub fn get_port(&self) -> u16 { - Self::guard_port(&self.0) - } - pub fn get_client_info(&self) -> ClashInfo { let config = &self.0; @@ -192,7 +181,7 @@ impl IClashTemp { }) } #[cfg(not(target_os = "windows"))] - pub fn guard_redir_port(config: &Mapping) -> u16 { + fn guard_redir_port(config: &Mapping) -> u16 { let mut port = config .get("redir-port") .and_then(|value| match value { @@ -208,7 +197,7 @@ impl IClashTemp { } #[cfg(target_os = "linux")] - pub fn guard_tproxy_port(config: &Mapping) -> u16 { + fn guard_tproxy_port(config: &Mapping) -> u16 { let mut port = config .get("tproxy-port") .and_then(|value| match value { @@ -223,7 +212,7 @@ impl IClashTemp { port } - pub fn guard_mixed_port(config: &Mapping) -> u16 { + fn guard_mixed_port(config: &Mapping) -> u16 { let raw_value = config.get("mixed-port"); let mut port = raw_value @@ -232,16 +221,16 @@ impl IClashTemp { Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) - .unwrap_or(7897); + .unwrap_or(network::ports::DEFAULT_MIXED); if port == 0 { - port = 7897; + port = network::ports::DEFAULT_MIXED; } port } - pub fn guard_socks_port(config: &Mapping) -> u16 { + fn guard_socks_port(config: &Mapping) -> u16 { let mut port = config .get("socks-port") .and_then(|value| match value { @@ -256,7 +245,7 @@ impl IClashTemp { port } - pub fn guard_port(config: &Mapping) -> u16 { + fn guard_port(config: &Mapping) -> u16 { let mut port = config .get("port") .and_then(|value| match value { @@ -271,7 +260,7 @@ impl IClashTemp { port } - pub fn guard_server_ctrl(config: &Mapping) -> String { + pub(super) fn guard_server_ctrl(config: &Mapping) -> String { config .get("external-controller") .and_then(|value| match value.as_str() { @@ -290,28 +279,7 @@ impl IClashTemp { .unwrap_or_else(|| "127.0.0.1:9097".into()) } - pub fn guard_external_controller(config: &Mapping) -> String { - // 在初始化阶段,直接返回配置中的值,不进行额外检查 - // 这样可以避免在配置加载期间的循环依赖 - Self::guard_server_ctrl(config) - } - - pub async fn guard_external_controller_with_setting(config: &Mapping) -> String { - // 检查 enable_external_controller 设置,用于运行时配置生成 - let enable_external_controller = Config::verge() - .await - .latest_arc() - .enable_external_controller - .unwrap_or(false); - - if enable_external_controller { - Self::guard_server_ctrl(config) - } else { - "".into() - } - } - - pub fn guard_client_ctrl(config: &Mapping) -> String { + fn guard_client_ctrl(config: &Mapping) -> String { let value = Self::guard_server_ctrl(config); match SocketAddr::from_str(value.as_str()) { Ok(mut socket) => { @@ -324,7 +292,7 @@ impl IClashTemp { } } - pub fn guard_external_controller_ipc() -> String { + pub(crate) fn guard_external_controller_ipc() -> String { // 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致 sidecar_ipc_path() .ok() diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index c03ede8e0..3fa1ec3c9 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -27,7 +27,7 @@ use std::{ use tokio::sync::{Mutex, MutexGuard, OnceCell}; use tokio::time::sleep; -pub struct Config { +pub(crate) struct Config { clash_config: Draft, verge_config: Draft, profiles_config: Draft, @@ -38,7 +38,7 @@ static TUN_SESSION_SUPPRESSED: AtomicBool = AtomicBool::new(false); static CONFIG_WRITE_LOCK: Mutex<()> = Mutex::const_new(()); impl Config { - pub async fn global() -> &'static Self { + async fn global() -> &'static Self { static CONFIG: OnceCell = OnceCell::const_new(); CONFIG .get_or_init(|| async { @@ -46,7 +46,7 @@ impl Config { 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::new()), + runtime_config: Draft::new(IRuntime::default()), } }) .await @@ -73,11 +73,6 @@ impl Config { CONFIG_WRITE_LOCK.lock().await } - pub async fn init_config() -> Result<()> { - Self::init_config_before_window().await?; - Self::init_runtime_config().await - } - pub async fn init_config_before_window() -> Result<()> { Self::ensure_default_profile_items().await?; @@ -151,7 +146,7 @@ impl Config { } async fn ensure_default_profile_items_for(profiles: &Draft) -> Result<()> { - if profiles.latest_arc().get_items().is_none() { + if profiles.latest_arc().items.is_none() { logging!( warn, Type::Config, @@ -161,11 +156,11 @@ impl Config { } if profiles.latest_arc().get_item("Merge").is_err() { - let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?; + 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()))?; + let script_item = &mut PrfItem::from_script(Some("Script".into())); profiles_append_item_to_safe(profiles, script_item).await?; } Ok(()) @@ -381,7 +376,7 @@ fn collect_names(config: &Mapping, list_key: &str, out: &mut HashSet) { } #[derive(Debug)] -pub enum ConfigType { +pub(crate) enum ConfigType { Run, Check, } @@ -405,7 +400,7 @@ mod tests { tokio::fs::remove_dir_all(&profiles_dir).await?; assert!(profile_was_preserved); - assert!(profiles.data_arc().get_items().is_none()); + assert!(profiles.data_arc().items.is_none()); Ok(()) } } diff --git a/src-tauri/src/config/mixed_port.rs b/src-tauri/src/config/mixed_port.rs index 85ebbe4e2..32414ce9e 100644 --- a/src-tauri/src/config/mixed_port.rs +++ b/src-tauri/src/config/mixed_port.rs @@ -8,9 +8,6 @@ use anyhow::Result; use super::Config; use crate::core::handle::Handle; -/// The port mihomo listens on when nothing else is configured. -pub const DEFAULT_MIXED_PORT: u16 = 7897; - /// A startup fallback for this session only; persistence would make the port climb across launches. static SESSION_FALLBACK: AtomicU16 = AtomicU16::new(0); diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index 173a4b304..d537bb7c3 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -10,7 +10,8 @@ pub mod runtime; pub(crate) mod snapshot; mod verge; -pub use self::{clash::*, config::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; +pub(crate) use self::config::{Config, ConfigType}; +pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; diff --git a/src-tauri/src/config/port.rs b/src-tauri/src/config/port.rs index 85aa55fbc..ec97c3d0a 100644 --- a/src-tauri/src/config/port.rs +++ b/src-tauri/src/config/port.rs @@ -36,14 +36,6 @@ static STARTUP_CORE_BLOCK_REASON: Lazy>> = Lazy::new(|| Mut impl Config { pub(crate) async fn resolve_startup_mixed_port() -> Result { - Self::resolve_startup_mixed_port_inner().await - } - - pub(crate) async fn retry_startup_mixed_port_fallback() -> Result { - Self::resolve_startup_mixed_port_inner().await - } - - async fn resolve_startup_mixed_port_inner() -> Result { let _config_write = Self::lock_config_write().await; let clash = Self::clash().await.latest_arc(); let verge = Self::verge().await.latest_arc(); @@ -287,7 +279,7 @@ fn configured_listener_ports(clash: &IClashTemp, verge: &IVerge) -> HashSet } } - if let Ok(controller) = SocketAddr::from_str(IClashTemp::guard_external_controller(&clash.0).as_str()) { + if let Ok(controller) = SocketAddr::from_str(IClashTemp::guard_server_ctrl(&clash.0).as_str()) { ports.insert(controller.port()); } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 96b0a3215..c677215c5 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -104,7 +104,7 @@ pub struct PrfOption { } impl PrfOption { - pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option { + pub(crate) fn merge(one: Option<&Self>, other: Option<&Self>) -> Option { match (one, other) { (Some(a_ref), Some(b_ref)) => { let mut result = a_ref.clone(); @@ -132,11 +132,7 @@ impl PrfOption { impl PrfItem { /// Builds an item from a partial value that must include `itype`. - pub async fn from(item: &Self, file_data: Option) -> Result { - if item.itype.is_none() { - bail!("type should not be null"); - } - + pub(super) async fn from(item: &Self, file_data: Option) -> Result { let itype = item .itype .as_ref() @@ -162,7 +158,7 @@ impl PrfItem { } } - pub async fn from_local( + async fn from_local( name: String, desc: String, file_data: Option, @@ -179,27 +175,27 @@ impl PrfItem { let mut groups = opt_ref.and_then(|o| o.groups.clone()); if merge.is_none() { - let merge_item = &mut Self::from_merge(None)?; + let merge_item = &mut Self::from_merge(None); profiles::profiles_append_item_safe(merge_item).await?; merge = merge_item.uid.clone(); } if script.is_none() { - let script_item = &mut Self::from_script(None)?; + let script_item = &mut Self::from_script(None); profiles::profiles_append_item_safe(script_item).await?; script = script_item.uid.clone(); } if rules.is_none() { - let rules_item = &mut Self::from_rules()?; + let rules_item = &mut Self::from_rules(); profiles::profiles_append_item_safe(rules_item).await?; rules = rules_item.uid.clone(); } if proxies.is_none() { - let proxies_item = &mut Self::from_proxies()?; + let proxies_item = &mut Self::from_proxies(); profiles::profiles_append_item_safe(proxies_item).await?; proxies = proxies_item.uid.clone(); } if groups.is_none() { - let groups_item = &mut Self::from_groups()?; + let groups_item = &mut Self::from_groups(); profiles::profiles_append_item_safe(groups_item).await?; groups = groups_item.uid.clone(); } @@ -227,7 +223,7 @@ impl PrfItem { }) } - pub async fn from_url( + pub(crate) async fn from_url( url: &str, name: Option<&String>, desc: Option<&String>, @@ -257,7 +253,7 @@ impl PrfItem { let url = fix_dirty_url(url)?; let resp = match NetworkManager::new() - .get_with_interrupt( + .get( url.as_str(), proxy_type, Some(timeout), @@ -349,7 +345,7 @@ impl PrfItem { let name = name .map(|s| s.to_owned()) .unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into())); - let data = resp.text_with_charset()?; + let data = resp.text(); let data = data.trim_start_matches('\u{feff}'); @@ -360,27 +356,27 @@ impl PrfItem { } if merge.is_none() { - let merge_item = &mut Self::from_merge(None)?; + let merge_item = &mut Self::from_merge(None); profiles::profiles_append_item_safe(merge_item).await?; merge = merge_item.uid.clone(); } if script.is_none() { - let script_item = &mut Self::from_script(None)?; + let script_item = &mut Self::from_script(None); profiles::profiles_append_item_safe(script_item).await?; script = script_item.uid.clone(); } if rules.is_none() { - let rules_item = &mut Self::from_rules()?; + let rules_item = &mut Self::from_rules(); profiles::profiles_append_item_safe(rules_item).await?; rules = rules_item.uid.clone(); } if proxies.is_none() { - let proxies_item = &mut Self::from_proxies()?; + let proxies_item = &mut Self::from_proxies(); profiles::profiles_append_item_safe(proxies_item).await?; proxies = proxies_item.uid.clone(); } if groups.is_none() { - let groups_item = &mut Self::from_groups()?; + let groups_item = &mut Self::from_groups(); profiles::profiles_append_item_safe(groups_item).await?; groups = groups_item.uid.clone(); } @@ -410,7 +406,7 @@ impl PrfItem { }) } - pub fn from_merge(uid: Option) -> Result { + pub(super) fn from_merge(uid: Option) -> Self { let (id, template) = if let Some(uid) = uid { (uid, tmpl::ITEM_MERGE.into()) } else { @@ -418,76 +414,76 @@ impl PrfItem { }; let file = format!("{id}.yaml").into(); - Ok(Self { + Self { uid: Some(id), itype: Some("merge".into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(template), ..Default::default() - }) + } } - pub fn from_script(uid: Option) -> Result { + pub(super) fn from_script(uid: Option) -> Self { let id = if let Some(uid) = uid { uid } else { help::get_uid("s").into() }; let file = format!("{id}.js").into(); // js ext - Ok(Self { + Self { uid: Some(id), itype: Some("script".into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_SCRIPT.into()), ..Default::default() - }) + } } - pub fn from_rules() -> Result { + fn from_rules() -> Self { let uid = help::get_uid("r").into(); let file = format!("{uid}.yaml").into(); // yaml ext - Ok(Self { + Self { uid: Some(uid), itype: Some("rules".into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_RULES.into()), ..Default::default() - }) + } } - pub fn from_proxies() -> Result { + fn from_proxies() -> Self { let uid = help::get_uid("p").into(); let file = format!("{uid}.yaml").into(); // yaml ext - Ok(Self { + Self { uid: Some(uid), itype: Some("proxies".into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_PROXIES.into()), ..Default::default() - }) + } } - pub fn from_groups() -> Result { + fn from_groups() -> Self { let uid = help::get_uid("g").into(); let file = format!("{uid}.yaml").into(); // yaml ext - Ok(Self { + Self { uid: Some(uid), itype: Some("groups".into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_GROUPS.into()), ..Default::default() - }) + } } - pub async fn read_file(&self) -> Result { + pub(crate) async fn read_file(&self) -> Result { let file = self .file .as_ref() @@ -498,37 +494,26 @@ impl PrfItem { .with_context(|| format!("failed to read the file \"{}\"", path.display()))?; Ok(content.into()) } - - pub async fn save_file(&self, data: String) -> Result<()> { - let file = self - .file - .as_ref() - .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; - let path = dirs::app_profiles_dir()?.join(file.as_str()); - fs::write(path, data.as_bytes()) - .await - .context("failed to save the file") - } } impl PrfItem { - pub fn current_merge(&self) -> Option<&String> { + pub(crate) fn current_merge(&self) -> Option<&String> { self.option.as_ref().and_then(|o| o.merge.as_ref()) } - pub fn current_script(&self) -> Option<&String> { + pub(crate) fn current_script(&self) -> Option<&String> { self.option.as_ref().and_then(|o| o.script.as_ref()) } - pub fn current_rules(&self) -> Option<&String> { + pub(crate) fn current_rules(&self) -> Option<&String> { self.option.as_ref().and_then(|o| o.rules.as_ref()) } - pub fn current_proxies(&self) -> Option<&String> { + pub(crate) fn current_proxies(&self) -> Option<&String> { self.option.as_ref().and_then(|o| o.proxies.as_ref()) } - pub fn current_groups(&self) -> Option<&String> { + pub(crate) fn current_groups(&self) -> Option<&String> { self.option.as_ref().and_then(|o| o.groups.as_ref()) } } diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 86c7fd54d..c62661803 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -63,14 +63,6 @@ pub struct IProfilePreview<'a> { pub is_current: bool, } -/// 清理结果 -#[derive(Debug, Clone)] -pub struct CleanupResult { - pub total_files: usize, - pub deleted_files: usize, - pub failed_deletions: usize, -} - pub(crate) struct ProfileDeletePlan { files: Vec, } @@ -135,7 +127,7 @@ impl IProfiles { } pub async fn save_file(&self) -> Result<()> { - help::save_yaml_atomic(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await + help::save_yaml(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await } /// 只修改current,valid和chain @@ -154,14 +146,6 @@ impl IProfiles { } } - pub const fn get_current(&self) -> Option<&String> { - self.current.as_ref() - } - - pub const fn get_items(&self) -> Option<&Vec> { - self.items.as_ref() - } - pub fn get_item(&self, uid: impl AsRef) -> Result<&PrfItem> { let uid_str = uid.as_ref(); @@ -178,7 +162,7 @@ impl IProfiles { bail!("failed to get the profile item \"uid:{}\"", uid_str); } - pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> { + async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> { let uid = &item.uid; if uid.is_none() { bail!("the uid should not be null"); @@ -215,31 +199,23 @@ impl IProfiles { Ok(()) } - pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> { - let mut items = self.items.take().unwrap_or_default(); - let mut old_index = None; - let mut new_index = None; - - for (i, _) in items.iter().enumerate() { - if items[i].uid.as_ref() == Some(active_id) { - old_index = Some(i); - } - if items[i].uid.as_ref() == Some(over_id) { - new_index = Some(i); - } + async fn reorder(&mut self, active_id: &str, over_id: &str) -> Result<()> { + { + let Some(items) = self.items.as_mut() else { + return Ok(()); + }; + let old_index = items.iter().rposition(|item| item.uid.as_deref() == Some(active_id)); + let new_index = items.iter().rposition(|item| item.uid.as_deref() == Some(over_id)); + let (Some(old_idx), Some(new_idx)) = (old_index, new_index) else { + return Ok(()); + }; + let item = items.remove(old_idx); + items.insert(new_idx, item); } - - let (old_idx, new_idx) = match (old_index, new_index) { - (Some(old), Some(new)) => (old, new), - _ => return Ok(()), - }; - let item = items.remove(old_idx); - items.insert(new_idx, item); - self.items = Some(items); self.save_file().await } - pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> { + async fn patch_item(&mut self, uid: &str, item: &PrfItem) -> Result<()> { if let Some(file) = &item.file { Self::validate_profile_file(file)?; } @@ -247,7 +223,7 @@ impl IProfiles { let mut items = self.items.take().unwrap_or_default(); for each in items.iter_mut() { - if each.uid.as_ref() == Some(uid) { + if each.uid.as_deref() == Some(uid) { patch!(each, item, itype); patch!(each, item, name); patch!(each, item, desc); @@ -284,7 +260,7 @@ impl IProfiles { } /// Updates fields returned by a remote profile refresh. - pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> { + async fn update_item(&mut self, uid: &str, item: &mut PrfItem) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } @@ -292,10 +268,8 @@ impl IProfiles { let _ = self.get_item(uid)?; if let Some(items) = self.items.as_mut() { - let some_uid = Some(uid.clone()); - for each in items.iter_mut() { - if each.uid == some_uid { + if each.uid.as_deref() == Some(uid) { each.extra = item.extra; each.updated = item.updated; each.home = item.home.to_owned(); @@ -343,8 +317,8 @@ impl IProfiles { raised } - pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> { - let current = self.current.as_ref().unwrap_or(uid).clone(); + pub(crate) fn plan_delete_item(&mut self, uid: &str) -> Result<(bool, ProfileDeletePlan)> { + let deleting_current = self.current.as_deref().is_none_or(|current| current == uid); let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| { [ op.merge.clone(), @@ -359,7 +333,7 @@ impl IProfiles { let mut items = self.items.take().unwrap_or_default(); let mut files = Vec::new(); - if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.as_str())) { + if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid)) { files.push(file); } @@ -369,7 +343,7 @@ impl IProfiles { } } - if current == *uid { + if deleting_current { self.current = None; for item in items.iter() { if item.itype == Some("remote".into()) || item.itype == Some("local".into()) { @@ -380,7 +354,7 @@ impl IProfiles { } self.items = Some(items); - Ok((current == *uid, ProfileDeletePlan { files })) + Ok((deleting_current, ProfileDeletePlan { files })) } /// 获取current指向的订阅内容 @@ -400,17 +374,13 @@ impl IProfiles { } } - pub fn is_current_profile_index(&self, index: &String) -> bool { - self.current.as_ref() == Some(index) - } - pub fn profiles_preview(&self) -> Option>> { self.items.as_ref().map(|items| { items .iter() .filter_map(|e| { if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) { - let is_current = self.is_current_profile_index(uid); + let is_current = self.current.as_ref() == Some(uid); let preview = IProfilePreview { uid, name, is_current }; Some(preview) } else { @@ -421,17 +391,6 @@ impl IProfiles { }) } - pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> { - if let Some(items) = &self.items { - for item in items { - if item.uid.as_ref() == Some(uid) { - return item.name.as_ref(); - } - } - } - None - } - /// 以 app 中的 profile 列表为准,删除不再需要的文件 pub async fn cleanup_orphaned_files(&self) -> Result<()> { let profiles_dir = dirs::app_profiles_dir()?; @@ -495,19 +454,13 @@ impl IProfiles { } } - let result = CleanupResult { - total_files, - deleted_files, - failed_deletions, - }; - logging!( info, Type::Config, "Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}", - result.total_files, - result.deleted_files, - result.failed_deletions + total_files, + deleted_files, + failed_deletions ); Ok(()) @@ -581,15 +534,15 @@ impl IProfiles { } } -// These helpers avoid holding a parking_lot guard across `await`. +// These helpers serialize asynchronous operations against committed profile data. use crate::config::Config; -pub async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option) -> Result<()> { +pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option) -> Result<()> { let item = &mut PrfItem::from(item, file_data).await?; profiles_append_item_safe(item).await } -pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { +pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { let profiles = Config::profiles().await; profiles_append_item_to_safe(&profiles, item).await } @@ -603,7 +556,7 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft, it .await } -pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> { +pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -613,7 +566,7 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result< .await } -pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> { +pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -623,7 +576,7 @@ pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Resu .await } -pub async fn profiles_save_file_safe() -> Result<()> { +pub(crate) async fn profiles_save_file_safe() -> Result<()> { Config::profiles() .await .with_data_modify(|profiles| async move { @@ -633,7 +586,7 @@ pub async fn profiles_save_file_safe() -> Result<()> { .await } -pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> { +pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -765,7 +718,7 @@ fn reconcile_selected_nodes( } /// Cancels restoration so it cannot overwrite a newer selection or profile snapshot. -pub fn supersede_selected_activation() { +pub(crate) fn supersede_selected_activation() { ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel); } @@ -866,7 +819,7 @@ async fn update_tray_after_activation(generation: u64) { } /// Records a backend-made selection so the next core start restores it. -pub async fn record_selected_node(group_name: &str, node: &str) -> Result<()> { +pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> { let group_name = String::from(group_name); let node = String::from(node); let recorded = Config::profiles() @@ -918,7 +871,7 @@ fn remove_selected_node(selected: &mut Vec, group_name: &str) -> bo selected.len() != original_len } -pub async fn forget_selected_node(group_name: &str) -> Result<()> { +pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> { let cleared = Config::profiles() .await .with_data_modify(move |mut profiles| async move { @@ -1150,22 +1103,21 @@ async fn activate_selected_nodes_worker( /// Whether an activation may also prune the records it cannot match. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SelectionRepair { +enum SelectionRepair { /// Prune records after a fully loaded profile switch. Prune, /// Preserve records during startup while provider-backed groups may still be absent. KeepRecords, } -pub fn activate_selected_nodes() -> Result<()> { +pub(crate) fn activate_selected_nodes() { // The first-pass signal is for callers that wait; a profile switch does not, and dropping // the receiver simply makes the send a no-op. drop(activate_selected_nodes_with(SelectionRepair::Prune)); - Ok(()) } /// Restores available selections before the system proxy points at the core, then retries the rest. -pub async fn restore_selected_nodes() { +pub(crate) async fn restore_selected_nodes() { let first_pass = activate_selected_nodes_with(SelectionRepair::KeepRecords); if tokio::time::timeout(SELECTED_NODES_FIRST_PASS_BUDGET, first_pass) .await @@ -1199,7 +1151,7 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot let result = async { // A draft may be a profile switch that has not passed validation yet. let profiles = Config::profiles().await.data_arc(); - let current = profiles.get_current().context("no current profile running")?.clone(); + let current = profiles.current.clone().context("no current profile running")?; let selected = profiles .get_item(¤t) .context("failed to get current profile")? @@ -1262,6 +1214,43 @@ mod tests { } } + #[tokio::test] + async fn missing_reorder_ids_preserve_ordered_items() -> Result<()> { + let mut profiles = IProfiles { + current: Some("a".into()), + items: Some(vec![ + PrfItem { + uid: Some("a".into()), + ..PrfItem::default() + }, + PrfItem { + uid: Some("b".into()), + ..PrfItem::default() + }, + PrfItem { + uid: Some("c".into()), + ..PrfItem::default() + }, + ]), + }; + let expected = Some(vec![Some("a"), Some("b"), Some("c")]); + + profiles.reorder("missing", "b").await?; + let ordered_uids = profiles + .items + .as_ref() + .map(|items| items.iter().map(|item| item.uid.as_deref()).collect::>()); + assert_eq!(ordered_uids, expected); + + profiles.reorder("a", "missing").await?; + let ordered_uids = profiles + .items + .as_ref() + .map(|items| items.iter().map(|item| item.uid.as_deref()).collect::>()); + assert_eq!(ordered_uids, expected); + Ok(()) + } + #[test] fn raising_intervals_only_touches_those_scheduled_too_often() { let mut profiles = IProfiles { @@ -1313,7 +1302,7 @@ mod tests { ]), }; - let (should_update, plan) = profiles.plan_delete_item(&"a".into())?; + let (should_update, plan) = profiles.plan_delete_item("a")?; assert!(should_update); assert_eq!(profiles.current.as_deref(), Some("b")); diff --git a/src-tauri/src/config/runtime.rs b/src-tauri/src/config/runtime.rs index 977a164d1..7155989e2 100644 --- a/src-tauri/src/config/runtime.rs +++ b/src-tauri/src/config/runtime.rs @@ -16,11 +16,6 @@ pub struct IRuntime { } impl IRuntime { - #[inline] - pub fn new() -> Self { - Self::default() - } - #[inline] pub fn patch_config(&mut self, patch: &Mapping) { let config = if let Some(config) = self.config.as_mut() { diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 85eadfb95..5aeefbedc 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::{ config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, + constants::network, utils::{dirs, help}, }; use anyhow::Result; @@ -265,7 +266,7 @@ impl IVerge { help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?; logging!(info, Type::Config, "配置文件修正完成,需要重新加载配置"); - Self::reload_config_after_fix(config).await?; + Self::reload_config_after_fix(config).await; } else { logging!(info, Type::Config, "clash_core配置验证通过: {:?}", config.clash_core); } @@ -273,7 +274,7 @@ impl IVerge { Ok(()) } - async fn reload_config_after_fix(updated_config: Self) -> Result<()> { + async fn reload_config_after_fix(updated_config: Self) { logging!( info, Type::Config, @@ -286,8 +287,6 @@ impl IVerge { *d = updated_config; }); config_draft.apply(); - - Ok(()) } pub fn get_valid_clash_core(&self) -> String { @@ -357,7 +356,7 @@ impl IVerge { verge_tproxy_port: Some(7896), #[cfg(target_os = "linux")] verge_tproxy_enabled: Some(false), - verge_mixed_port: Some(7897), + verge_mixed_port: Some(network::ports::DEFAULT_MIXED), verge_socks_port: Some(7898), verge_socks_enabled: Some(false), verge_port: Some(7899), diff --git a/src-tauri/src/core/manager/mod.rs b/src-tauri/src/core/manager/mod.rs index f62947e8e..c7b5b4191 100644 --- a/src-tauri/src/core/manager/mod.rs +++ b/src-tauri/src/core/manager/mod.rs @@ -263,7 +263,7 @@ impl CoreManager { crate::config::Config::notify_startup_mixed_port_fallback(); return Err(start_error); } - match crate::config::Config::retry_startup_mixed_port_fallback().await { + match crate::config::Config::resolve_startup_mixed_port().await { Ok(true) => { retries += 1; logging!( diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 582d3b5d6..cf647ac9c 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -153,7 +153,7 @@ impl Timer { // Redundant with first_delay == ZERO, kept as the immediate path; mark_task_running // no-ops whichever of the two arrives second. let cur_timestamp = chrono::Local::now().timestamp(); - if let Some(items) = Config::profiles().await.data_arc().get_items() { + if let Some(items) = Config::profiles().await.data_arc().items.as_ref() { for item in items.iter() { if let Some(option) = item.option.as_ref() && option.allow_auto_update.unwrap_or(true) @@ -204,7 +204,7 @@ impl Timer { } async fn gen_map(&self) -> HashMap { - if let Some(items) = Config::profiles().await.data_arc().get_items() { + if let Some(items) = Config::profiles().await.data_arc().items.as_ref() { return Self::gen_map_from_items(items); } @@ -446,7 +446,7 @@ impl Timer { let task_interval = *self.timer_map.read().get(uid)?; let profiles = Config::profiles().await; let profiles_guard = profiles.latest_arc(); - let items = profiles_guard.get_items()?; + let items = profiles_guard.items.as_ref()?; let profile = items.iter().find(|item| item.uid.as_deref() == Some(uid))?; let updated = profile.updated.unwrap_or(0) as i64; diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 2cbc5e176..36d67fe23 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -294,7 +294,7 @@ impl Tray { { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); - if let Some(current_profile_uid) = profiles.get_current() + if let Some(current_profile_uid) = profiles.current.as_ref() && let Ok(profile) = profiles.get_item(current_profile_uid) { current_profile_name = match &profile.name { diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 868e6ce29..797b02cd7 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -54,21 +54,21 @@ impl AsyncChainItemFrom for Option { data: ChainType::Merge(help::read_mapping(&path).await.ok()?), }), "rules" => { - let seq_map = help::read_seq_map(&path).await.ok()?; + let seq_map = help::read_yaml::(&path).await.ok()?; Some(ChainItem { uid, data: ChainType::Rules(seq_map), }) } "proxies" => { - let seq_map = help::read_seq_map(&path).await.ok()?; + let seq_map = help::read_yaml::(&path).await.ok()?; Some(ChainItem { uid, data: ChainType::Proxies(seq_map), }) } "groups" => { - let seq_map = help::read_seq_map(&path).await.ok()?; + let seq_map = help::read_yaml::(&path).await.ok()?; Some(ChainItem { uid, data: ChainType::Groups(seq_map), diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 6cec40344..c988f8213 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -158,7 +158,7 @@ async fn get_config_values() -> ConfigValues { #[allow(clippy::cognitive_complexity)] async fn collect_profile_items(profiles: &IProfiles) -> Result { - let current_profile_uid = match profiles.get_current().cloned() { + let current_profile_uid = match profiles.current.clone() { Some(uid) => uid, None => return Ok(ProfileItems::default()), }; diff --git a/src-tauri/src/feat/core_upgrade.rs b/src-tauri/src/feat/core_upgrade.rs index 398cd0f47..a0e87393e 100644 --- a/src-tauri/src/feat/core_upgrade.rs +++ b/src-tauri/src/feat/core_upgrade.rs @@ -185,13 +185,13 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string:: for proxy in [ProxyType::Localhost, ProxyType::System, ProxyType::None] { let attempt = NetworkManager::new() - .get_with_interrupt(&url, proxy, Some(VERSION_TIMEOUT_SECS), None, false) + .get(&url, proxy, Some(VERSION_TIMEOUT_SECS), None, false) .await; match attempt { Ok(response) if response.status().is_success() => { // An error page answered with 200 must not become a version, nor reach the URL. - let version = response.text_with_charset()?.trim().to_owned(); + let version = response.text().trim().to_owned(); if !is_usable_version(&version) { last_error = Some(anyhow!("{url} returned an unusable version")); continue; @@ -220,7 +220,7 @@ async fn download_package(proxy: ProxyType, alpha: bool, version: &str) -> Resul logging!(info, Type::Core, "core upgrade: downloading {url}"); NetworkManager::new() - .get_bytes_with_interrupt(&url, proxy, Some(PACKAGE_TIMEOUT_SECS), MAX_PACKAGE_BYTES) + .get_bytes(&url, proxy, Some(PACKAGE_TIMEOUT_SECS), MAX_PACKAGE_BYTES) .await .with_context(|| format!("failed to download {url}")) } diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index e263aa3db..4c0f8cd15 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -1,6 +1,6 @@ use crate::{ cmd, - config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, + config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe}, core::{CoreManager, handle, tray, validate::ValidationOutcome}, utils::help::{mask_err, mask_url}, }; @@ -124,13 +124,14 @@ async fn perform_profile_update( let mut merged_opt = PrfOption::merge(opt, option); let is_current = { let profiles = Config::profiles().await; - profiles.latest_arc().is_current_profile_index(uid) + profiles.latest_arc().current.as_ref() == Some(uid) }; let profiles = Config::profiles().await; let profiles_arc = profiles.latest_arc(); let profile_name = profiles_arc - .get_name_by_uid(uid) - .cloned() + .get_item(uid) + .ok() + .and_then(|item| item.name.clone()) .unwrap_or_else(|| String::from("UnKnown Profile")); let mut last_err; @@ -138,7 +139,7 @@ async fn perform_profile_update( match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { Ok(mut item) => { logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功"); - profiles_draft_update_item_safe(uid, &mut item).await?; + profiles_update_item_safe(uid, &mut item).await?; return Ok(is_current); } Err(err) => { @@ -158,7 +159,7 @@ async fn perform_profile_update( match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { Ok(mut item) => { logging!(info, Type::Config, "[订阅更新] 使用 Clash代理 更新订阅配置成功"); - profiles_draft_update_item_safe(uid, &mut item).await?; + profiles_update_item_safe(uid, &mut item).await?; handle::Handle::notice_message("update_with_clash_proxy", profile_name); drop(last_err); return Ok(is_current); @@ -180,7 +181,7 @@ async fn perform_profile_update( match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { Ok(mut item) => { logging!(info, Type::Config, "[订阅更新] 使用 系统代理 更新订阅配置成功"); - profiles_draft_update_item_safe(uid, &mut item).await?; + profiles_update_item_safe(uid, &mut item).await?; handle::Handle::notice_message("update_with_clash_proxy", profile_name); drop(last_err); return Ok(is_current); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 45e446fad..4043bbcb2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -80,10 +80,8 @@ mod app_init { app.deep_link().on_open_url(|event| { let urls = event.urls(); AsyncHandler::spawn(move || async move { - if let Some(url) = urls.first() - && let Err(e) = resolve::resolve_scheme(url.as_ref()).await - { - logging!(error, Type::Setup, "Failed to resolve scheme: {}", e); + if let Some(url) = urls.first() { + resolve::resolve_scheme(url.as_ref()).await; } }); }); @@ -169,7 +167,6 @@ mod app_init { cmd::forget_selected_node, cmd::save_dns_config, cmd::apply_dns_config, - cmd::check_dns_config_exists, cmd::get_dns_config_content, cmd::validate_dns_config, cmd::get_clash_logs, diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index b4193137a..c0a6021e8 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -3,7 +3,10 @@ use anyhow::Result; use async_trait::async_trait; use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use tauri::Manager as _; #[cfg(not(feature = "verge-dev"))] @@ -198,12 +201,9 @@ pub fn clash_latest_log() -> Result { } } -pub fn path_to_str(path: &PathBuf) -> Result<&str> { - let path_str = path - .as_os_str() - .to_str() - .ok_or_else(|| anyhow::anyhow!("failed to get path from {:?}", path))?; - Ok(path_str) +pub fn path_to_str(path: &Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| anyhow::anyhow!("failed to get path from {:?}", path)) } pub fn get_encryption_key() -> Result> { diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 7085fda0b..999b2f257 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,15 +1,17 @@ -use crate::{config::with_encryption, enhance::seq::SeqMap}; +use crate::config::with_encryption; use anyhow::{Context as _, Result, anyhow, bail}; use clash_verge_logging::{Type, logging}; use nanoid::nanoid; +use scopeguard::{ScopeGuard, guard}; use serde::{Serialize, de::DeserializeOwned}; use serde_yaml_ng::{Mapping, Value}; use std::{ path::{Path, PathBuf}, str::FromStr, }; +use tokio::io::AsyncWriteExt as _; -pub async fn read_yaml(path: &PathBuf) -> Result { +pub async fn read_yaml(path: &Path) -> Result { if !tokio::fs::try_exists(path).await.unwrap_or(false) { bail!("file not found \"{}\"", path.display()); } @@ -19,7 +21,7 @@ pub async fn read_yaml(path: &PathBuf) -> Result { Ok(with_encryption(|| async { serde_yaml_ng::from_str::(&yaml_str) }).await?) } -pub async fn read_mapping(path: &PathBuf) -> Result { +pub async fn read_mapping(path: &Path) -> Result { if !tokio::fs::try_exists(path).await.unwrap_or(false) { bail!("file not found \"{}\"", path.display()); } @@ -49,11 +51,7 @@ pub async fn read_mapping(path: &PathBuf) -> Result { } } -pub async fn read_seq_map(path: &PathBuf) -> Result { - read_yaml(path).await -} - -pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { +pub async fn save_yaml(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> { let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?; let yaml_str = match prefix { @@ -61,20 +59,55 @@ pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Op None => data_str, }; - tokio::fs::write(path, yaml_str.as_bytes()) - .await - .with_context(|| format!("failed to save file \"{}\"", path.display()))?; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - Ok(()) -} + let (temporary, file) = loop { + let temporary = path.with_extension(format!("tmp-{}-{}", std::process::id(), nanoid!())); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary) + { + Ok(file) => break (temporary, file), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(error).with_context(|| { + format!( + "failed to create temporary file \"{}\" for \"{}\"", + temporary.display(), + path.display() + ) + }); + } + } + }; -pub async fn save_yaml_atomic(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> { - let temporary = path.with_extension("delete.tmp"); - save_yaml(&temporary, data, prefix).await?; - if let Err(error) = super::server::replace_file_atomic(&temporary, path) { - let _ = tokio::fs::remove_file(&temporary).await; - return Err(error).with_context(|| format!("failed to replace file \"{}\"", path.display())); + let temporary = guard(temporary, |path| { + let _ = std::fs::remove_file(path); + }); + let mut file = tokio::fs::File::from_std(file); + file.write_all(yaml_str.as_bytes()) + .await + .with_context(|| format!("failed to write temporary file for \"{}\"", path.display()))?; + file.flush() + .await + .with_context(|| format!("failed to flush temporary file for \"{}\"", path.display()))?; + drop(file); + + match tokio::fs::symlink_metadata(path).await { + Ok(metadata) if metadata.file_type().is_file() => { + tokio::fs::set_permissions(temporary.as_path(), metadata.permissions()) + .await + .with_context(|| format!("failed to preserve permissions for \"{}\"", path.display()))?; + } + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(error).with_context(|| format!("failed to inspect file \"{}\"", path.display())); + } } + + std::fs::rename(temporary.as_path(), path) + .with_context(|| format!("failed to replace file \"{}\"", path.display()))?; + let _ = ScopeGuard::into_inner(temporary); Ok(()) } diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 1a4ba0669..9d0881e6c 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -7,7 +7,7 @@ pub mod init; pub mod linux; #[cfg(target_os = "macos")] pub mod macos_launch_guard; -pub mod network; +pub(crate) mod network; pub mod notification; pub mod port; pub mod resolve; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index aa1488e19..d325fecbd 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -18,7 +18,7 @@ pub struct HttpResponse { } impl HttpResponse { - pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { + const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { Self { status, headers, body } } @@ -30,8 +30,8 @@ impl HttpResponse { &self.headers } - pub fn text_with_charset(&self) -> Result<&str> { - Ok(&self.body) + pub fn text(&self) -> &str { + &self.body } } @@ -50,12 +50,6 @@ enum TlsRootMode { pub struct NetworkManager; -impl Default for NetworkManager { - fn default() -> Self { - Self::new() - } -} - impl NetworkManager { pub const fn new() -> Self { Self @@ -161,23 +155,6 @@ impl NetworkManager { detail.contains("protocolversion") || detail.contains("protocol version") } - pub async fn create_request( - &self, - proxy_type: ProxyType, - timeout_secs: Option, - user_agent: Option, - accept_invalid_certs: bool, - ) -> Result { - self.create_request_with_tls_mode( - proxy_type, - timeout_secs, - user_agent, - accept_invalid_certs, - TlsRootMode::PlatformVerifier, - ) - .await - } - async fn get_with_tls_mode( &self, url: &str, @@ -279,7 +256,7 @@ impl NetworkManager { self.build_client(proxy_url, headers, accept_invalid_certs, timeout_secs, tls_root_mode) } - pub async fn get_with_interrupt( + pub async fn get( &self, url: &str, proxy_type: ProxyType, @@ -355,8 +332,8 @@ impl NetworkManager { Ok(body) } - /// Downloads a binary body, mirroring the TLS fallback of [`Self::get_with_interrupt`]. - pub async fn get_bytes_with_interrupt( + /// Downloads a binary body, mirroring the TLS fallback of [`Self::get`]. + pub async fn get_bytes( &self, url: &str, proxy_type: ProxyType, diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index f7b6c55e9..0c749288c 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -21,13 +21,13 @@ use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_signal; pub mod dns; -pub mod scheme; -pub mod window; -pub mod window_script; +mod scheme; +pub(crate) mod window; +mod window_script; static RESOLVE_DONE: AtomicBool = AtomicBool::new(false); -pub fn init_work_dir_and_logger() -> anyhow::Result<()> { +pub(crate) fn init_work_dir_and_logger() -> anyhow::Result<()> { AsyncHandler::block_on(async { init_work_config().await; logging!(info, Type::Setup, "Initializing logger"); @@ -36,16 +36,16 @@ pub fn init_work_dir_and_logger() -> anyhow::Result<()> { }) } -pub fn resolve_setup_sync() { +pub(crate) fn resolve_setup_sync() { AsyncHandler::spawn(|| async { AsyncHandler::spawn_blocking(init_scheme); - AsyncHandler::spawn_blocking(init_embed_server); + AsyncHandler::spawn_blocking(server::embed_server); #[cfg(target_os = "linux")] AsyncHandler::spawn_blocking(watch_linux_theme_changed); }); } -pub fn resolve_setup_async() { +pub(crate) fn resolve_setup_async() { AsyncHandler::spawn(|| async { logging!(info, Type::ClashVergeRev, "Version: {}", env!("CARGO_PKG_VERSION")); @@ -100,21 +100,16 @@ pub async fn resolve_reset_async() -> Result<(), anyhow::Error> { Ok(()) } -pub(super) fn init_scheme() { +fn init_scheme() { logging_error!(Type::Setup, init::init_scheme()); } -pub async fn resolve_scheme(param: &str) -> Result<()> { +pub(crate) async fn resolve_scheme(param: &str) { logging_error!(Type::Setup, scheme::resolve_scheme(param).await); - Ok(()) -} - -pub(super) fn init_embed_server() { - server::embed_server(); } #[cfg(target_os = "linux")] -pub(crate) fn watch_linux_theme_changed() { +fn watch_linux_theme_changed() { match dark_light::subscribe() { Ok(watcher) => { for mode in watcher.iter() { @@ -137,28 +132,28 @@ pub(crate) fn watch_linux_theme_changed() { } } -pub(super) async fn init_resources() { +async fn init_resources() { logging_error!(Type::Setup, init::init_resources().await); } -pub(super) async fn init_startup_script() { +async fn init_startup_script() { logging_error!(Type::Setup, init::startup_script().await); } -pub(super) async fn init_timer() { +async fn init_timer() { logging_error!(Type::Setup, Timer::global().init().await); } -pub(super) async fn init_hotkey() { +async fn init_hotkey() { let skip_register_hotkeys = !Config::verge().await.latest_arc().enable_global_hotkey.unwrap_or(true); logging_error!(Type::Setup, Hotkey::global().init(skip_register_hotkeys).await); } -pub(super) async fn init_auto_lightweight_boot() { +async fn init_auto_lightweight_boot() { logging_error!(Type::Setup, auto_lightweight_boot().await); } -pub(super) async fn init_auto_backup() { +async fn init_auto_backup() { logging_error!(Type::Setup, AutoBackupManager::global().init().await); } @@ -184,37 +179,37 @@ async fn init_silent_updater() { logging!(info, Type::Setup, "Silent updater initialized"); } -pub fn init_signal() { +pub(crate) fn init_signal() { logging!(info, Type::Setup, "Initializing signal handlers..."); clash_verge_signal::register(feat::quit); } -pub async fn init_work_config() { +async fn init_work_config() { logging_error!(Type::Setup, init::init_config().await); } -pub(super) async fn init_tray() { +async fn init_tray() { logging_error!(Type::Setup, Tray::global().init().await); } -pub(super) async fn init_verge_config() { +async fn init_verge_config() { logging_error!(Type::Setup, Config::init_runtime_config().await); } -pub(super) async fn init_verge_config_before_window() -> bool { +async fn init_verge_config_before_window() -> bool { let result = Config::init_config_before_window().await; let success = result.is_ok(); logging_error!(Type::Setup, result); success } -pub(super) async fn init_service_manager() { +async fn init_service_manager() { clash_verge_service_ipc::set_config(Some(ServiceManager::config())).await; SERVICE_MANAGER.detect_startup_status().await; } -pub(super) async fn init_core_manager() -> bool { +async fn init_core_manager() -> bool { match CoreManager::global().init().await { Ok(initialized) => initialized, Err(error) => { @@ -224,27 +219,27 @@ pub(super) async fn init_core_manager() -> bool { } } -pub(super) async fn refresh_tray_menu() { +async fn refresh_tray_menu() { logging_error!(Type::Setup, Tray::global().update_part().await); } -pub(super) async fn init_window() { +async fn init_window() { let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false); WindowManager::create_window(!is_silent_start).await; } #[cfg(target_os = "macos")] -pub(super) async fn resolve_dock_show() { +async fn resolve_dock_show() { let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false); if is_silent_start { Handle::global().set_activation_policy_accessory(); } } -pub fn resolve_done() { +fn resolve_done() { RESOLVE_DONE.store(true, Ordering::Release); } -pub fn is_resolve_done() -> bool { +pub(crate) fn is_resolve_done() -> bool { RESOLVE_DONE.load(Ordering::Acquire) } diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index a174e2469..158c43291 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -130,7 +130,7 @@ async fn post_import_updates(uid: &String, had_current_profile: bool) { false } else { let profiles = Config::profiles().await; - profiles.latest_arc().is_current_profile_index(uid) + profiles.latest_arc().current.as_ref() == Some(uid) }; if should_update_core { diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index bc2782b82..993896179 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -6,7 +6,7 @@ use crate::{ utils::{dirs, window_manager::WindowManager}, }; use anyhow::{Context as _, Result, bail}; -use clash_verge_logging::{Type, logging, logging_error}; +use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; use parking_lot::Mutex; use reqwest::ClientBuilder; @@ -250,7 +250,7 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) { )); } AsyncHandler::spawn(|| async move { - logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); + resolve::resolve_scheme(&query.param).await; }); Ok::<_, warp::Rejection>(warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)) }); @@ -426,35 +426,7 @@ fn write_instance_record(path: &Path, record: &InstanceRecord) -> Result<()> { file.set_permissions(std::fs::Permissions::from_mode(0o600))?; } drop(file); - replace_file_atomic(&temporary, path).context("failed to replace singleton record")?; - Ok(()) -} - -#[cfg(not(windows))] -pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { - std::fs::rename(source, destination)?; - Ok(()) -} - -#[cfg(windows)] -pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { - use std::os::windows::ffi::OsStrExt as _; - use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW}; - - let mut source: Vec = source.as_os_str().encode_wide().collect(); - source.push(0); - let mut destination: Vec = destination.as_os_str().encode_wide().collect(); - destination.push(0); - if unsafe { - MoveFileExW( - source.as_ptr(), - destination.as_ptr(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, - ) - } == 0 - { - return Err(std::io::Error::last_os_error().into()); - } + std::fs::rename(&temporary, path).context("failed to replace singleton record")?; Ok(()) } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 87bf3a531..e35e14bdb 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -50,7 +50,7 @@ impl WindowManager { handle::Handle::global().set_activation_policy_regular(); } - pub fn get_main_window_with_state() -> (Option>, WindowState) { + fn get_main_window_with_state() -> (Option>, WindowState) { let Some(window) = Self::get_main_window() else { return (None, WindowState::NotExist); }; @@ -73,28 +73,7 @@ impl WindowManager { } pub fn get_main_window_state() -> WindowState { - match Self::get_main_window() { - Some(window) => { - let is_minimized = window.is_minimized().unwrap_or(false); - let is_visible = window.is_visible().unwrap_or(false); - let is_focused = window.is_focused().unwrap_or(false); - - if is_minimized { - return WindowState::Minimized; - } - - if !is_visible { - return WindowState::Hidden; - } - - if is_focused { - WindowState::VisibleFocused - } else { - WindowState::VisibleUnfocused - } - } - None => WindowState::NotExist, - } + Self::get_main_window_with_state().1 } pub fn get_main_window() -> Option> { @@ -262,7 +241,7 @@ impl WindowManager { window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false) } - pub fn is_main_window_minimized(window: Option<&WebviewWindow>) -> bool { + fn is_main_window_minimized(window: Option<&WebviewWindow>) -> bool { window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false) } diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index 7c6004b64..487aff211 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -467,13 +467,12 @@ export function DnsViewer({ ref }: { ref?: Ref }) { const initDnsConfig = useCallback(async () => { try { - const dnsConfigExists = await invoke( - 'check_dns_config_exists', + const dnsConfig = await invoke( + 'get_dns_config_content', {}, ) - if (dnsConfigExists) { - const dnsConfig = await invoke('get_dns_config_content', {}) + if (dnsConfig !== null) { const config = yaml.load(dnsConfig) as any updateValuesFromConfig(config)