mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
fix: make profile deletion rollback-safe
This commit is contained in:
@@ -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<Option<String>>
|
||||
|
||||
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
|
||||
|
||||
@@ -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<PrfOption>) -> 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<ValidationO
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let target_profile = profiles.current.as_ref();
|
||||
|
||||
|
||||
@@ -244,7 +244,12 @@ impl Config {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<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 {
|
||||
($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<bool> {
|
||||
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::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
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::<Vec<_>>()
|
||||
});
|
||||
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<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<()> {
|
||||
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()),
|
||||
|
||||
@@ -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<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<()> {
|
||||
@@ -234,11 +292,15 @@ impl CoreManager {
|
||||
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 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(())
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -24,6 +24,10 @@ use std::os::windows::io::OwnedHandle;
|
||||
|
||||
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_GENERATION_STEP: u64 = 1 << 1;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProfileItems> {
|
||||
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<ProfileItems> {
|
||||
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<ProfileItems> {
|
||||
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<String>, HashMap<String, ResultLog>)> {
|
||||
pub async fn enhance(profiles: &IProfiles) -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> {
|
||||
// gather config values
|
||||
let cfg_vals = get_config_values().await;
|
||||
let ConfigValues {
|
||||
@@ -732,7 +723,7 @@ pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, Resu
|
||||
} = cfg_vals;
|
||||
|
||||
// collect profile items
|
||||
let profile = collect_profile_items().await?;
|
||||
let profile = collect_profile_items(profiles).await?;
|
||||
let config = profile.config;
|
||||
let merge_item = profile.merge_item;
|
||||
let script_item = profile.script_item;
|
||||
|
||||
@@ -130,6 +130,7 @@ pub async fn restore_webdav_backup(filename: String) -> 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(())
|
||||
|
||||
@@ -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();
|
||||
if let Err(rollback_error) = rollback_proxy_ports(&snapshots, was_running).await {
|
||||
return Err(anyhow!(
|
||||
@@ -233,9 +233,9 @@ async fn rollback_proxy_ports(snapshots: &[FileSnapshot], was_running: bool) ->
|
||||
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(())
|
||||
|
||||
@@ -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<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(())
|
||||
}
|
||||
|
||||
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] = [
|
||||
'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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user