diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index c32c08d92..d18bf6dd9 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -3,7 +3,7 @@ use crate::feat; use crate::utils::{dirs, yaml_emitter}; use crate::{ cmd::StringifyErr as _, - config::{ClashInfo, Config}, + config::{ClashInfo, Config, profiles::profiles_save_file_safe}, constants, core::{ CoreManager, handle, @@ -64,7 +64,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult> match CoreManager::global().change_core(&clash_core).await { Ok(_) => { - logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await); + logging_error!(Type::Core, profiles_save_file_safe().await); // 切换内核后重启内核 match CoreManager::global().restart_core().await { @@ -107,7 +107,7 @@ pub async fn start_core() -> CmdResult { /// 关闭核心 #[tauri::command] pub async fn stop_core() -> CmdResult { - logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await); + logging_error!(Type::Core, profiles_save_file_safe().await); let result = CoreManager::global() .stop_core() .await @@ -121,7 +121,7 @@ pub async fn stop_core() -> CmdResult { /// 重启核心 #[tauri::command] pub async fn restart_core() -> CmdResult { - logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await); + logging_error!(Type::Core, profiles_save_file_safe().await); let result = CoreManager::global() .restart_core() .await diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 55c1c8a62..2d0c06309 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -6,7 +6,7 @@ use crate::{ config::{ Config, IProfiles, PrfItem, PrfOption, profiles::{ - profiles_append_item_with_filedata_safe, profiles_delete_item_safe, profiles_patch_item_safe, + PROFILE_WRITE_LOCK, profiles_append_item_with_filedata_safe, profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe, }, profiles_append_item_safe, @@ -153,13 +153,44 @@ pub async fn update_profile(index: String, option: Option) -> CmdResu /// 删除配置文件 #[tauri::command] pub async fn delete_profile(index: String) -> CmdResult { - // 使用Send-safe helper函数 - let should_update = profiles_delete_item_safe(&index) - .await - .with_error_code("PROFILE_DELETE_FAILED")?; - profiles_save_file_safe() + let profile_write_guard = PROFILE_WRITE_LOCK.lock().await; + + let profiles = Config::profiles().await; + let result = profiles + .with_data_modify(|mut candidate| async move { + let original = candidate.clone(); + let (should_update, plan) = candidate.plan_delete_item(&index)?; + let guard = if should_update { + match CoreManager::global() + .update_config_forced_with_profiles(&candidate, &original) + .await? + { + Ok(guard) => Some(guard), + Err(outcome) => return Ok((original, Err(outcome))), + } + } else { + candidate.save_file().await?; + None + }; + let current = candidate.current.clone(); + plan.cleanup().await; + Ok((candidate, Ok((should_update, current, guard)))) + }) .await .with_error_code("PROFILE_DELETE_FAILED")?; + let (should_update, current, config_update_guard) = match result { + Ok(result) => result, + Err(outcome) => { + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + return Err(coded_error("PROFILE_DELETE_FAILED", outcome)); + } + }; + if should_update { + logging_error!(Type::Config, profiles::activate_selected_nodes()); + } + drop(config_update_guard); + drop(profile_write_guard); + if let Err(e) = Tray::global().update_tooltip().await { logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}"); } @@ -168,28 +199,13 @@ pub async fn delete_profile(index: String) -> CmdResult { logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}"); } if should_update { - match CoreManager::global().update_config_forced().await { - Ok(outcome) if outcome.is_valid() => { - handle::Handle::refresh_clash(); - // 发送配置变更通知 - logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index); - handle::Handle::notify_profile_changed(&index); - } - Ok(outcome) => { - logging!(warn, Type::Cmd, "删除订阅后更新配置失败: {}", outcome); - handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); - return Err(coded_error("PROFILE_DELETE_FAILED", outcome)); - } - Err(e) => { - logging!(error, Type::Cmd, "{}", e); - return Err(coded_error("PROFILE_DELETE_FAILED", e)); - } + handle::Handle::refresh_clash(); + if let Some(current) = current.as_ref() { + logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", current); + handle::Handle::notify_profile_changed(current); } } - Timer::global() - .refresh() - .await - .with_error_code("PROFILE_DELETE_FAILED")?; + logging_error!(Type::Timer, Timer::global().refresh().await); Ok(()) } @@ -316,6 +332,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult Result<()> { - let (mut config, exists_keys, logs) = enhance::enhance().await?; + 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) = enhance::enhance(profiles).await?; sanitize_tunnels_proxy(&mut config); @@ -300,6 +305,7 @@ impl Config { }); 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); diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index fba888dce..a91cfd68e 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -28,6 +28,8 @@ use std::{ use tauri_plugin_mihomo::models::{Proxies, ProxyType}; use tokio::{fs, task::JoinHandle}; +pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + /// Regex to check profile file names, eg. /// R12345678.yaml (remote) /// L12345678.yaml (local) @@ -88,6 +90,23 @@ pub struct CleanupResult { pub failed_deletions: usize, } +pub(crate) struct ProfileDeletePlan { + files: Vec, +} + +impl ProfileDeletePlan { + pub(crate) async fn cleanup(self) { + let Ok(dir) = dirs::app_profiles_dir() else { + return; + }; + for file in self.files { + if let Err(error) = dir.join(file.as_str()).remove_if_exists().await { + logging!(warn, Type::Config, "清理已删除订阅文件失败: {file} - {error}"); + } + } + } +} + macro_rules! patch { ($lv: expr, $rv: expr, $key: tt) => { if ($rv.$key).is_some() { @@ -130,7 +149,7 @@ impl IProfiles { } pub async fn save_file(&self) -> Result<()> { - help::save_yaml(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await + help::save_yaml_atomic(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await } /// 只修改current,valid和chain @@ -331,40 +350,32 @@ impl IProfiles { self.save_file().await } - /// delete item - /// if delete the current then return true - pub async fn delete_item(&mut self, uid: &String) -> Result { - let current = self.current.as_ref().unwrap_or(uid); - let current = current.clone(); - let delete_uids = { - let item = self.get_item(uid)?; - let option = item.option.as_ref(); - option.map_or(Vec::new(), |op| { - [ - op.merge.clone(), - op.script.clone(), - op.rules.clone(), - op.proxies.clone(), - op.groups.clone(), - ] - .into_iter() - .collect::>() - }) - }; + pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> { + let current = self.current.as_ref().unwrap_or(uid).clone(); + let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| { + [ + op.merge.clone(), + op.script.clone(), + op.rules.clone(), + op.proxies.clone(), + op.groups.clone(), + ] + .into_iter() + .collect::>() + }); let mut items = self.items.take().unwrap_or_default(); + let mut files = Vec::new(); - // remove the main item (if exists) and delete its file if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.as_str())) { - let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + files.push(file); } for delete_uid in delete_uids { if let Some(file) = Self::take_item_file_by_uid(&mut items, delete_uid.as_deref()) { - let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + files.push(file); } } - // delete the original uid if current == *uid { self.current = None; for item in items.iter() { @@ -376,8 +387,7 @@ impl IProfiles { } self.items = Some(items); - self.save_file().await?; - Ok(current == *uid) + Ok((current == *uid, ProfileDeletePlan { files })) } /// 获取current指向的订阅内容 @@ -626,16 +636,6 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result< .await } -pub async fn profiles_delete_item_safe(index: &String) -> Result { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - let deleted = profiles.delete_item(index).await?; - Ok((profiles, deleted)) - }) - .await -} - pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> { Config::profiles() .await @@ -795,9 +795,8 @@ fn reconcile_selected_nodes( /// already holding the newer one, would then disagree with the core. Bumping the generation is /// how an activation is told it has been overtaken. /// -/// Called *after* the profile is written, which is the only placement that needs to exist: an -/// activation that read the older profile is cancelled by this, and one starting afterwards reads -/// the newer one and needs no cancelling. +/// Called after a newer selection is written, or before a candidate Core transition whose caller +/// will reactivate whichever Profiles snapshot ultimately commits. pub fn supersede_selected_activation() { ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel); } @@ -1277,6 +1276,39 @@ mod tests { use super::*; use tauri_plugin_mihomo::models::Proxy; + fn deletion_item(uid: &str, kind: &str, file: &str, merge: Option<&str>) -> PrfItem { + PrfItem { + uid: Some(uid.into()), + itype: Some(kind.into()), + file: Some(file.into()), + option: merge.map(|uid| PrfOption { + merge: Some(uid.into()), + ..PrfOption::default() + }), + ..PrfItem::default() + } + } + + #[test] + fn delete_plan_defers_files_and_selects_replacement() -> Result<()> { + let mut profiles = IProfiles { + current: Some("a".into()), + items: Some(vec![ + deletion_item("a", "remote", "a.yaml", Some("owned")), + deletion_item("owned", "merge", "owned.yaml", None), + deletion_item("b", "local", "b.yaml", None), + ]), + }; + + let (should_update, plan) = profiles.plan_delete_item(&"a".into())?; + + assert!(should_update); + assert_eq!(profiles.current.as_deref(), Some("b")); + assert_eq!(plan.files, vec![String::from("a.yaml"), String::from("owned.yaml")]); + assert!(profiles.get_item("owned").is_err()); + Ok(()) + } + fn selected(group: &str, node: &str) -> PrfSelected { PrfSelected { name: Some(group.into()), diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 94cc74ebb..6da368917 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -1,7 +1,7 @@ -use super::{CoreManager, RunningMode}; +use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode}; use crate::core::service::StageRequest; use crate::{ - config::{Config, ConfigType, runtime::IRuntime}, + config::{Config, ConfigType, IProfiles, runtime::IRuntime}, constants::timing, core::{ handle, @@ -55,6 +55,14 @@ enum ConfigApplication { Fail(String), } +pub(crate) struct ConfigUpdateGuard<'a>(&'a CoreManager); + +impl Drop for ConfigUpdateGuard<'_> { + fn drop(&mut self) { + self.0.finish_config_update(); + } +} + /// Decide how to apply a configuration, given how staging went. /// /// Two branches avoid replacing the Core, for opposite reasons. One is success. The other is a @@ -208,7 +216,57 @@ impl CoreManager { self.set_last_update(Instant::now()); } - self.perform_config_update().await + self.perform_config_update(None).await + } + + pub(crate) async fn update_config_forced_with_profiles( + &self, + candidate: &IProfiles, + rollback: &IProfiles, + ) -> Result, ValidationOutcome>> { + if handle::Handle::global().is_exiting() { + return Ok(Err(ValidationOutcome::Skipped { + reason: ValidationSkipReason::Exiting, + })); + } + if !self.try_start_config_update() { + return Ok(Err(ValidationOutcome::Busy)); + } + let guard = ConfigUpdateGuard(self); + self.set_last_update(Instant::now()); + crate::config::profiles::supersede_selected_activation(); + + let outcome = match PROFILE_SELECTIONS_PENDING_COMMIT + .scope(true, self.perform_config_update(Some(candidate))) + .await + { + Ok(outcome) => outcome, + Err(error) => { + self.restore_profile_config(rollback).await?; + return Err(error); + } + }; + if !outcome.is_valid() { + crate::config::profiles::restore_selected_nodes().await; + return Ok(Err(outcome)); + } + match candidate.save_file().await { + Ok(()) => Ok(Ok(guard)), + Err(error) => { + self.restore_profile_config(rollback).await?; + Err(error) + } + } + } + + async fn restore_profile_config(&self, profiles: &IProfiles) -> Result<()> { + let outcome = self.perform_config_update(Some(profiles)).await?; + if outcome.is_valid() { + crate::config::profiles::restore_selected_nodes().await; + Ok(()) + } else { + Err(anyhow!("failed to restore previous Core configuration: {outcome}")) + } } pub async fn update_config_checked(&self) -> Result<()> { @@ -234,11 +292,15 @@ impl CoreManager { true } - async fn perform_config_update(&self) -> Result { + async fn perform_config_update(&self, profiles: Option<&IProfiles>) -> Result { let runtime = Config::runtime().await; let transaction = DraftTransaction::begin(vec![&runtime])?; - if let Err(err) = Config::generate().await { + let generate = match profiles { + Some(profiles) => Config::generate_with_profiles(profiles).await, + None => Config::generate().await, + }; + if let Err(err) = generate { let message: String = err.to_string().into(); return Ok(ValidationOutcome::invalid_from_message(message)); } @@ -360,7 +422,7 @@ impl CoreManager { Type::Core, "Failed to apply configuration by mihomo api, restart core to apply it, error msg: {err}" ); - match self.restart_core().await { + match self.restart_core_during_config_update().await { Ok(_) => { logging!(info, Type::Core, "Configuration applied after restart"); Ok(()) diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index b80cd53f2..2235ffea8 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -266,6 +266,16 @@ impl CoreManager { } pub async fn start_core(&self) -> Result<()> { + if !self.try_start_config_update() { + anyhow::bail!("configuration update is already running"); + } + defer! { + self.finish_config_update(); + } + self.start_core_during_config_update().await + } + + pub(crate) async fn start_core_during_config_update(&self) -> Result<()> { let _life = self.lifecycle_lock.lock().await; run_core_start_transition( || self.start_core_inner(), @@ -498,6 +508,16 @@ impl CoreManager { } pub async fn restart_core(&self) -> Result<()> { + if !self.try_start_config_update() { + anyhow::bail!("configuration update is already running"); + } + defer! { + self.finish_config_update(); + } + self.restart_core_during_config_update().await + } + + pub(crate) async fn restart_core_during_config_update(&self) -> Result<()> { // 持锁覆盖 stop+start,避免生命周期操作插入。 let _life = self.lifecycle_lock.lock().await; logging!(info, Type::Core, "Restarting core"); diff --git a/src-tauri/src/core/manager/mod.rs b/src-tauri/src/core/manager/mod.rs index 4d3515674..38dd941e7 100644 --- a/src-tauri/src/core/manager/mod.rs +++ b/src-tauri/src/core/manager/mod.rs @@ -24,6 +24,10 @@ use std::os::windows::io::OwnedHandle; pub(crate) static CLASH_LOGGER: Lazy> = Lazy::new(|| Arc::new(AsyncLogger::new())); +tokio::task_local! { + static PROFILE_SELECTIONS_PENDING_COMMIT: bool; +} + const CORE_READINESS_ACTIVE_BIT: u64 = 1; const CORE_READINESS_GENERATION_STEP: u64 = 1 << 1; diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index acea6e44a..b272df1c9 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -1,6 +1,6 @@ #[cfg(test)] use super::claim_core_readiness_generation; -use super::{CoreManager, RunningMode}; +use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode}; use crate::{ AsyncHandler, config::Config, @@ -37,6 +37,12 @@ impl CoreManager { /// /// Repeat calls supersede each other, so every start path may call this without coordinating. async fn restore_selected_nodes(&self) { + if PROFILE_SELECTIONS_PENDING_COMMIT + .try_with(|pending| *pending) + .unwrap_or(false) + { + return; + } crate::config::profiles::restore_selected_nodes().await; } } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 1b5c1e777..33e069b7f 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -15,7 +15,7 @@ use self::{ }; use crate::utils::dirs; use crate::{ - config::{Config, IVerge, PrfItem}, + config::{Config, IProfiles, IVerge, PrfItem}, constants, utils::tmpl, }; @@ -157,25 +157,18 @@ async fn get_config_values() -> ConfigValues { } #[allow(clippy::cognitive_complexity)] -async fn collect_profile_items() -> Result { - let profiles = Config::profiles().await; - let profiles_arc = profiles.latest_arc(); - drop(profiles); - - let current_profile_uid = match profiles_arc.get_current().cloned() { +async fn collect_profile_items(profiles: &IProfiles) -> Result { + let current_profile_uid = match profiles.get_current().cloned() { Some(uid) => uid, - None => { - drop(profiles_arc); - return Ok(ProfileItems::default()); - } + None => return Ok(ProfileItems::default()), }; - let current = profiles_arc + let current = profiles .current_mapping() .await .with_context(|| format!("failed to read current profile \"{current_profile_uid}\""))?; - let current_item = match profiles_arc.get_item(¤t_profile_uid) { + let current_item = match profiles.get_item(¤t_profile_uid) { Ok(item) => item, Err(err) => { return Err(err).with_context(|| format!("failed to get current profile \"{current_profile_uid}\"")); @@ -200,38 +193,36 @@ async fn collect_profile_items() -> Result { let name = current_item.name.clone().unwrap_or_default(); let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!( - chain_item_or_default(profiles_arc.get_item(&merge_uid).ok(), || ChainItem { + chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem { uid: "".into(), data: ChainType::Merge(Mapping::new()), },), - chain_item_or_default(profiles_arc.get_item(&script_uid).ok(), || ChainItem { + chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem { uid: "".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), },), - chain_item_or_default(profiles_arc.get_item(&rules_uid).ok(), || ChainItem { + chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem { uid: "".into(), data: ChainType::Rules(SeqMap::default()), },), - chain_item_or_default(profiles_arc.get_item(&proxies_uid).ok(), || ChainItem { + chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem { uid: "".into(), data: ChainType::Proxies(SeqMap::default()), },), - chain_item_or_default(profiles_arc.get_item(&groups_uid).ok(), || ChainItem { + chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem { uid: "".into(), data: ChainType::Groups(SeqMap::default()), },), - chain_item_or_default(profiles_arc.get_item("Merge").ok(), || ChainItem { + chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem { uid: "Merge".into(), data: ChainType::Merge(Mapping::new()), },), - chain_item_or_default(profiles_arc.get_item("Script").ok(), || ChainItem { + chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem { uid: "Script".into(), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), },), ); - drop(profiles_arc); - Ok(ProfileItems { config: current, merge_item, @@ -713,7 +704,7 @@ async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> M /// Enhance mode /// 返回最终订阅、该订阅包含的键、和script执行的结果 -pub async fn enhance() -> Result<(Mapping, HashSet, HashMap)> { +pub async fn enhance(profiles: &IProfiles) -> Result<(Mapping, HashSet, HashMap)> { // gather config values let cfg_vals = get_config_values().await; let ConfigValues { @@ -732,7 +723,7 @@ pub async fn enhance() -> Result<(Mapping, HashSet, HashMap Result<()> { let value = backup_storage_path.clone(); let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; let mut zip = zip::ZipArchive::new(file)?; + let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; zip.extract(app_home_dir()?)?; let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; // Finally remove the temp file (attempt cleanup even if finalize fails) @@ -313,6 +314,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; let mut zip = zip::ZipArchive::new(file)?; + let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; zip.extract(app_home_dir()?)?; finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; Ok(()) diff --git a/src-tauri/src/feat/listener.rs b/src-tauri/src/feat/listener.rs index 39b40d963..797e0dc80 100644 --- a/src-tauri/src/feat/listener.rs +++ b/src-tauri/src/feat/listener.rs @@ -91,7 +91,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result let lifecycle_result = if was_running { let manager = CoreManager::global(); if matches!(*manager.get_running_mode(), RunningMode::NotRunning) { - manager.start_core().await + manager.start_core_during_config_update().await } else { - manager.restart_core().await + manager.restart_core_during_config_update().await } } else { Ok(()) diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index f4cf5df35..315e3c33f 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -4,9 +4,10 @@ use clash_verge_logging::{Type, logging}; use nanoid::nanoid; use serde::{Serialize, de::DeserializeOwned}; use serde_yaml_ng::{Mapping, Value}; -#[cfg(target_os = "windows")] -use std::path::Path; -use std::{path::PathBuf, str::FromStr}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; /// read data from yaml as struct T pub async fn read_yaml(path: &PathBuf) -> Result { @@ -73,6 +74,16 @@ pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Op Ok(()) } +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())); + } + Ok(()) +} + const ALPHABET: [char; 62] = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index 4d3f5e592..a174e2469 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -97,7 +97,7 @@ async fn import_subscription(url: &str, name: Option<&String>) { return; } - if let Err(e) = Config::profiles().await.data_arc().save_file().await { + if let Err(e) = profiles::profiles_save_file_safe().await { logging!(error, Type::Config, "failed to save imported subscription: {}", e); handle::Handle::notice_message("import_sub_url::error", e.to_string()); return; diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index dc71ae0a9..6c043838b 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -414,18 +414,18 @@ 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)?; + replace_file_atomic(&temporary, path).context("failed to replace singleton record")?; Ok(()) } #[cfg(not(windows))] -fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { +pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { std::fs::rename(source, destination)?; Ok(()) } #[cfg(windows)] -fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { +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}; @@ -441,7 +441,7 @@ fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> { ) } == 0 { - return Err(std::io::Error::last_os_error()).context("failed to replace singleton record"); + return Err(std::io::Error::last_os_error().into()); } Ok(()) }