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::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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,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(¤t_profile_uid) {
|
let current_item = match profiles.get_item(¤t_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;
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user