fix: make profile deletion rollback-safe

This commit is contained in:
Tunglies
2026-08-03 20:00:01 +08:00
parent 903410c12e
commit c5b641e421
14 changed files with 264 additions and 113 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ use crate::feat;
use crate::utils::{dirs, yaml_emitter}; use crate::utils::{dirs, yaml_emitter};
use crate::{ use crate::{
cmd::StringifyErr as _, cmd::StringifyErr as _,
config::{ClashInfo, Config}, config::{ClashInfo, Config, profiles::profiles_save_file_safe},
constants, constants,
core::{ core::{
CoreManager, handle, CoreManager, handle,
@@ -64,7 +64,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
match CoreManager::global().change_core(&clash_core).await { match CoreManager::global().change_core(&clash_core).await {
Ok(_) => { 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 { match CoreManager::global().restart_core().await {
@@ -107,7 +107,7 @@ pub async fn start_core() -> CmdResult {
/// 关闭核心 /// 关闭核心
#[tauri::command] #[tauri::command]
pub async fn stop_core() -> CmdResult { 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() let result = CoreManager::global()
.stop_core() .stop_core()
.await .await
@@ -121,7 +121,7 @@ pub async fn stop_core() -> CmdResult {
/// 重启核心 /// 重启核心
#[tauri::command] #[tauri::command]
pub async fn restart_core() -> CmdResult { 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() let result = CoreManager::global()
.restart_core() .restart_core()
.await .await
+42 -25
View File
@@ -6,7 +6,7 @@ use crate::{
config::{ config::{
Config, IProfiles, PrfItem, PrfOption, Config, IProfiles, PrfItem, PrfOption,
profiles::{ 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_reorder_safe, profiles_save_file_safe,
}, },
profiles_append_item_safe, profiles_append_item_safe,
@@ -153,13 +153,44 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件 /// 删除配置文件
#[tauri::command] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
// 使用Send-safe helper函数 let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let should_update = profiles_delete_item_safe(&index)
.await let profiles = Config::profiles().await;
.with_error_code("PROFILE_DELETE_FAILED")?; let result = profiles
profiles_save_file_safe() .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 .await
.with_error_code("PROFILE_DELETE_FAILED")?; .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 { if let Err(e) = Tray::global().update_tooltip().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}"); logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
} }
@@ -168,28 +199,13 @@ pub async fn delete_profile(index: String) -> CmdResult {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}"); logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
} }
if should_update { if should_update {
match CoreManager::global().update_config_forced().await {
Ok(outcome) if outcome.is_valid() => {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
// 发送配置变更通知 if let Some(current) = current.as_ref() {
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index); logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", current);
handle::Handle::notify_profile_changed(&index); handle::Handle::notify_profile_changed(current);
}
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));
} }
} }
} logging_error!(Type::Timer, Timer::global().refresh().await);
Timer::global()
.refresh()
.await
.with_error_code("PROFILE_DELETE_FAILED")?;
Ok(()) Ok(())
} }
@@ -316,6 +332,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求"); logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy); return Ok(ValidationOutcome::Busy);
} }
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let target_profile = profiles.current.as_ref(); let target_profile = profiles.current.as_ref();
+7 -1
View File
@@ -244,7 +244,12 @@ impl Config {
} }
pub async fn generate() -> Result<()> { pub async fn generate() -> 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); sanitize_tunnels_proxy(&mut config);
@@ -300,6 +305,7 @@ impl Config {
}); });
let save_profiles_task = AsyncHandler::spawn(|| async { let save_profiles_task = AsyncHandler::spawn(|| async {
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
let profiles = Self::profiles().await; let profiles = Self::profiles().await;
profiles.apply(); profiles.apply();
logging_error!(Type::Config, profiles.data_arc().save_file().await); logging_error!(Type::Config, profiles.data_arc().save_file().await);
+63 -31
View File
@@ -28,6 +28,8 @@ use std::{
use tauri_plugin_mihomo::models::{Proxies, ProxyType}; use tauri_plugin_mihomo::models::{Proxies, ProxyType};
use tokio::{fs, task::JoinHandle}; 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. /// Regex to check profile file names, eg.
/// R12345678.yaml (remote) /// R12345678.yaml (remote)
/// L12345678.yaml (local) /// L12345678.yaml (local)
@@ -88,6 +90,23 @@ pub struct CleanupResult {
pub failed_deletions: usize, pub failed_deletions: usize,
} }
pub(crate) struct ProfileDeletePlan {
files: Vec<String>,
}
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 { macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => { ($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() { if ($rv.$key).is_some() {
@@ -130,7 +149,7 @@ impl IProfiles {
} }
pub async fn save_file(&self) -> Result<()> { 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 /// 只修改current,valid和chain
@@ -331,15 +350,9 @@ impl IProfiles {
self.save_file().await self.save_file().await
} }
/// delete item pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> {
/// if delete the current then return true let current = self.current.as_ref().unwrap_or(uid).clone();
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> { let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| {
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.merge.clone(),
op.script.clone(), op.script.clone(),
@@ -349,22 +362,20 @@ impl IProfiles {
] ]
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) });
};
let mut items = self.items.take().unwrap_or_default(); 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())) { 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 { for delete_uid in delete_uids {
if let Some(file) = Self::take_item_file_by_uid(&mut items, delete_uid.as_deref()) { 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 { if current == *uid {
self.current = None; self.current = None;
for item in items.iter() { for item in items.iter() {
@@ -376,8 +387,7 @@ impl IProfiles {
} }
self.items = Some(items); self.items = Some(items);
self.save_file().await?; Ok((current == *uid, ProfileDeletePlan { files }))
Ok(current == *uid)
} }
/// 获取current指向的订阅内容 /// 获取current指向的订阅内容
@@ -626,16 +636,6 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<
.await .await
} }
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
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<()> { pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
Config::profiles() Config::profiles()
.await .await
@@ -795,9 +795,8 @@ fn reconcile_selected_nodes(
/// already holding the newer one, would then disagree with the core. Bumping the generation is /// already holding the newer one, would then disagree with the core. Bumping the generation is
/// how an activation is told it has been overtaken. /// 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 /// Called after a newer selection is written, or before a candidate Core transition whose caller
/// activation that read the older profile is cancelled by this, and one starting afterwards reads /// will reactivate whichever Profiles snapshot ultimately commits.
/// the newer one and needs no cancelling.
pub fn supersede_selected_activation() { pub fn supersede_selected_activation() {
ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel); ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel);
} }
@@ -1277,6 +1276,39 @@ mod tests {
use super::*; use super::*;
use tauri_plugin_mihomo::models::Proxy; 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 { fn selected(group: &str, node: &str) -> PrfSelected {
PrfSelected { PrfSelected {
name: Some(group.into()), name: Some(group.into()),
+68 -6
View File
@@ -1,7 +1,7 @@
use super::{CoreManager, RunningMode}; use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode};
use crate::core::service::StageRequest; use crate::core::service::StageRequest;
use crate::{ use crate::{
config::{Config, ConfigType, runtime::IRuntime}, config::{Config, ConfigType, IProfiles, runtime::IRuntime},
constants::timing, constants::timing,
core::{ core::{
handle, handle,
@@ -55,6 +55,14 @@ enum ConfigApplication {
Fail(String), 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. /// 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 /// 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.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<std::result::Result<ConfigUpdateGuard<'_>, 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<()> { pub async fn update_config_checked(&self) -> Result<()> {
@@ -234,11 +292,15 @@ impl CoreManager {
true true
} }
async fn perform_config_update(&self) -> Result<ValidationOutcome> { async fn perform_config_update(&self, profiles: Option<&IProfiles>) -> Result<ValidationOutcome> {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let transaction = DraftTransaction::begin(vec![&runtime])?; 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(); let message: String = err.to_string().into();
return Ok(ValidationOutcome::invalid_from_message(message)); return Ok(ValidationOutcome::invalid_from_message(message));
} }
@@ -360,7 +422,7 @@ impl CoreManager {
Type::Core, Type::Core,
"Failed to apply configuration by mihomo api, restart core to apply it, error msg: {err}" "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(_) => { Ok(_) => {
logging!(info, Type::Core, "Configuration applied after restart"); logging!(info, Type::Core, "Configuration applied after restart");
Ok(()) Ok(())
+20
View File
@@ -266,6 +266,16 @@ impl CoreManager {
} }
pub async fn start_core(&self) -> Result<()> { 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; let _life = self.lifecycle_lock.lock().await;
run_core_start_transition( run_core_start_transition(
|| self.start_core_inner(), || self.start_core_inner(),
@@ -498,6 +508,16 @@ impl CoreManager {
} }
pub async fn restart_core(&self) -> Result<()> { 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,避免生命周期操作插入。 // 持锁覆盖 stop+start,避免生命周期操作插入。
let _life = self.lifecycle_lock.lock().await; let _life = self.lifecycle_lock.lock().await;
logging!(info, Type::Core, "Restarting core"); logging!(info, Type::Core, "Restarting core");
+4
View File
@@ -24,6 +24,10 @@ use std::os::windows::io::OwnedHandle;
pub(crate) static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = Lazy::new(|| Arc::new(AsyncLogger::new())); pub(crate) static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = 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_ACTIVE_BIT: u64 = 1;
const CORE_READINESS_GENERATION_STEP: u64 = 1 << 1; const CORE_READINESS_GENERATION_STEP: u64 = 1 << 1;
+7 -1
View File
@@ -1,6 +1,6 @@
#[cfg(test)] #[cfg(test)]
use super::claim_core_readiness_generation; use super::claim_core_readiness_generation;
use super::{CoreManager, RunningMode}; use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode};
use crate::{ use crate::{
AsyncHandler, AsyncHandler,
config::Config, config::Config,
@@ -37,6 +37,12 @@ impl CoreManager {
/// ///
/// Repeat calls supersede each other, so every start path may call this without coordinating. /// Repeat calls supersede each other, so every start path may call this without coordinating.
async fn restore_selected_nodes(&self) { 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; crate::config::profiles::restore_selected_nodes().await;
} }
} }
+15 -24
View File
@@ -15,7 +15,7 @@ use self::{
}; };
use crate::utils::dirs; use crate::utils::dirs;
use crate::{ use crate::{
config::{Config, IVerge, PrfItem}, config::{Config, IProfiles, IVerge, PrfItem},
constants, constants,
utils::tmpl, utils::tmpl,
}; };
@@ -157,25 +157,18 @@ async fn get_config_values() -> ConfigValues {
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
async fn collect_profile_items() -> Result<ProfileItems> { async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
let profiles = Config::profiles().await; let current_profile_uid = match profiles.get_current().cloned() {
let profiles_arc = profiles.latest_arc();
drop(profiles);
let current_profile_uid = match profiles_arc.get_current().cloned() {
Some(uid) => uid, Some(uid) => uid,
None => { None => return Ok(ProfileItems::default()),
drop(profiles_arc);
return Ok(ProfileItems::default());
}
}; };
let current = profiles_arc let current = profiles
.current_mapping() .current_mapping()
.await .await
.with_context(|| format!("failed to read current profile \"{current_profile_uid}\""))?; .with_context(|| format!("failed to read current profile \"{current_profile_uid}\""))?;
let current_item = match profiles_arc.get_item(&current_profile_uid) { let current_item = match profiles.get_item(&current_profile_uid) {
Ok(item) => item, Ok(item) => item,
Err(err) => { Err(err) => {
return Err(err).with_context(|| format!("failed to get current profile \"{current_profile_uid}\"")); return Err(err).with_context(|| format!("failed to get current profile \"{current_profile_uid}\""));
@@ -200,38 +193,36 @@ async fn collect_profile_items() -> Result<ProfileItems> {
let name = current_item.name.clone().unwrap_or_default(); 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!( 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(), uid: "".into(),
data: ChainType::Merge(Mapping::new()), 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(), uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.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(), uid: "".into(),
data: ChainType::Rules(SeqMap::default()), 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(), uid: "".into(),
data: ChainType::Proxies(SeqMap::default()), 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(), uid: "".into(),
data: ChainType::Groups(SeqMap::default()), 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(), uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()), 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(), uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},), },),
); );
drop(profiles_arc);
Ok(ProfileItems { Ok(ProfileItems {
config: current, config: current,
merge_item, merge_item,
@@ -713,7 +704,7 @@ async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> M
/// Enhance mode /// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果 /// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> { pub async fn enhance(profiles: &IProfiles) -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> {
// gather config values // gather config values
let cfg_vals = get_config_values().await; let cfg_vals = get_config_values().await;
let ConfigValues { let ConfigValues {
@@ -732,7 +723,7 @@ pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, Resu
} = cfg_vals; } = cfg_vals;
// collect profile items // collect profile items
let profile = collect_profile_items().await?; let profile = collect_profile_items(profiles).await?;
let config = profile.config; let config = profile.config;
let merge_item = profile.merge_item; let merge_item = profile.merge_item;
let script_item = profile.script_item; let script_item = profile.script_item;
+2
View File
@@ -130,6 +130,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let value = backup_storage_path.clone(); let value = backup_storage_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
let mut zip = zip::ZipArchive::new(file)?; let mut zip = zip::ZipArchive::new(file)?;
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
zip.extract(app_home_dir()?)?; zip.extract(app_home_dir()?)?;
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
// Finally remove the temp file (attempt cleanup even if finalize fails) // 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 file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?; let mut zip = zip::ZipArchive::new(file)?;
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
zip.extract(app_home_dir()?)?; zip.extract(app_home_dir()?)?;
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
Ok(()) Ok(())
+3 -3
View File
@@ -91,7 +91,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
}; };
} }
if was_running && let Err(activation_error) = manager.restart_core().await { if was_running && let Err(activation_error) = manager.restart_core_during_config_update().await {
transaction.rollback(); transaction.rollback();
if let Err(rollback_error) = rollback_proxy_ports(&snapshots, was_running).await { if let Err(rollback_error) = rollback_proxy_ports(&snapshots, was_running).await {
return Err(anyhow!( return Err(anyhow!(
@@ -233,9 +233,9 @@ async fn rollback_proxy_ports(snapshots: &[FileSnapshot], was_running: bool) ->
let lifecycle_result = if was_running { let lifecycle_result = if was_running {
let manager = CoreManager::global(); let manager = CoreManager::global();
if matches!(*manager.get_running_mode(), RunningMode::NotRunning) { if matches!(*manager.get_running_mode(), RunningMode::NotRunning) {
manager.start_core().await manager.start_core_during_config_update().await
} else { } else {
manager.restart_core().await manager.restart_core_during_config_update().await
} }
} else { } else {
Ok(()) Ok(())
+14 -3
View File
@@ -4,9 +4,10 @@ use clash_verge_logging::{Type, logging};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "windows")] use std::{
use std::path::Path; path::{Path, PathBuf},
use std::{path::PathBuf, str::FromStr}; str::FromStr,
};
/// read data from yaml as struct T /// read data from yaml as struct T
pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> { pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
@@ -73,6 +74,16 @@ pub async fn save_yaml<T: Serialize + Sync>(path: &PathBuf, data: &T, prefix: Op
Ok(()) Ok(())
} }
pub async fn save_yaml_atomic<T: Serialize + Sync>(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] = [ 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', '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', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+1 -1
View File
@@ -97,7 +97,7 @@ async fn import_subscription(url: &str, name: Option<&String>) {
return; 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); logging!(error, Type::Config, "failed to save imported subscription: {}", e);
handle::Handle::notice_message("import_sub_url::error", e.to_string()); handle::Handle::notice_message("import_sub_url::error", e.to_string());
return; return;
+4 -4
View File
@@ -414,18 +414,18 @@ fn write_instance_record(path: &Path, record: &InstanceRecord) -> Result<()> {
file.set_permissions(std::fs::Permissions::from_mode(0o600))?; file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
} }
drop(file); drop(file);
replace_file_atomic(&temporary, path)?; replace_file_atomic(&temporary, path).context("failed to replace singleton record")?;
Ok(()) Ok(())
} }
#[cfg(not(windows))] #[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)?; std::fs::rename(source, destination)?;
Ok(()) Ok(())
} }
#[cfg(windows)] #[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 std::os::windows::ffi::OsStrExt as _;
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW}; 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 } == 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(()) Ok(())
} }