From 1bd503a654a99ea778d6c1de4175596615f1f2c4 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 30 Mar 2025 10:12:02 +0800 Subject: [PATCH] feat: add error prompt for initial config loading to prevent switching to invalid subscription --- UPDATELOG.md | 2 +- src-tauri/src/cmd/profile.rs | 108 ++++++++++++++++++++++++------- src-tauri/src/config/profiles.rs | 92 ++------------------------ src-tauri/src/core/handle.rs | 93 ++++++++++++++++++++++++-- src-tauri/src/feat/profile.rs | 8 +++ src-tauri/src/utils/help.rs | 48 ++++++++++---- src-tauri/src/utils/resolve.rs | 18 ++++++ 7 files changed, 244 insertions(+), 125 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index ffea6dd2..fe7a25ca 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -16,7 +16,7 @@ #### 新增了: - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - - 增加载入初始配置文件的错误提示 + - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 #### 优化了: - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 25c67a2e..c12c1105 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,8 +1,8 @@ use super::CmdResult; use crate::{ - config::*, - core::{tray::Tray, *}, - feat, logging, logging_error, ret_err, + config::{Config, IProfiles, PrfItem, PrfOption}, + core::{handle, tray::Tray, CoreManager}, + feat, logging, ret_err, utils::{dirs, help, logging::Type}, wrap_err, }; @@ -10,31 +10,16 @@ use crate::{ /// 获取配置文件列表 #[tauri::command] pub fn get_profiles() -> CmdResult { - let _ = tray::Tray::global().update_menu(); + let _ = Tray::global().update_menu(); Ok(Config::profiles().data().clone()) } /// 增强配置文件 #[tauri::command] pub async fn enhance_profiles() -> CmdResult { - match CoreManager::global().update_config().await { - Ok((true, _)) => { - logging!(info, Type::Cmd, true, "配置更新成功"); - logging_error!(Type::Tray, true, Tray::global().update_tooltip()); - handle::Handle::refresh_clash(); - Ok(()) - } - Ok((false, error_msg)) => { - println!("[enhance_profiles] 配置验证失败: {}", error_msg); - handle::Handle::notice_message("config_validate::error", &error_msg); - Ok(()) - } - Err(e) => { - println!("[enhance_profiles] 更新过程发生错误: {}", e); - handle::Handle::notice_message("config_validate::process_terminated", e.to_string()); - Ok(()) - } - } + wrap_err!(feat::enhance_profiles().await)?; + handle::Handle::refresh_clash(); + Ok(()) } /// 导入配置文件 @@ -83,6 +68,81 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { let current_profile = Config::profiles().latest().current.clone(); logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile); + // 如果要切换配置,先检查目标配置文件是否有语法错误 + if let Some(new_profile) = profiles.current.as_ref() { + if current_profile.as_ref() != Some(new_profile) { + logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile); + + // 获取目标配置文件路径 + let profiles_config = Config::profiles(); + let profiles_data = profiles_config.latest(); + let config_file_result = match profiles_data.get_item(new_profile) { + Ok(item) => { + if let Some(file) = &item.file { + let path = dirs::app_profiles_dir().map(|dir| dir.join(file)); + path.ok() + } else { + None + } + } + Err(e) => { + logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e); + None + } + }; + + // 如果获取到文件路径,检查YAML语法 + if let Some(file_path) = config_file_result { + if !file_path.exists() { + logging!( + error, + Type::Cmd, + true, + "目标配置文件不存在: {}", + file_path.display() + ); + handle::Handle::notice_message( + "config_validate::file_not_found", + &format!("{}", file_path.display()), + ); + return Ok(false); + } + + match std::fs::read_to_string(&file_path) { + Ok(content) => match serde_yaml::from_str::(&content) { + Ok(_) => { + logging!(info, Type::Cmd, true, "目标配置文件语法正确"); + } + Err(err) => { + let error_msg = format!(" {}", err); + logging!( + error, + Type::Cmd, + true, + "目标配置文件存在YAML语法错误:{}", + error_msg + ); + handle::Handle::notice_message( + "config_validate::yaml_syntax_error", + &error_msg, + ); + return Ok(false); + } + }, + Err(err) => { + let error_msg = format!("无法读取目标配置文件: {}", err); + logging!(error, Type::Cmd, true, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::file_read_error", + &error_msg, + ); + return Ok(false); + } + } + } + } + } + // 更新profiles配置 logging!(info, Type::Cmd, true, "正在更新配置草稿"); let _ = Config::profiles().draft().patch_config(profiles); @@ -92,7 +152,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { Ok((true, _)) => { logging!(info, Type::Cmd, true, "配置更新成功"); handle::Handle::refresh_clash(); - let _ = tray::Tray::global().update_tooltip(); + let _ = Tray::global().update_tooltip(); Config::profiles().apply(); wrap_err!(Config::profiles().data().save_file())?; Ok(true) @@ -139,6 +199,8 @@ pub async fn patch_profiles_config_by_profile_index( _app_handle: tauri::AppHandle, profile_index: String, ) -> CmdResult { + logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index); + let profiles = IProfiles { current: Some(profile_index), items: None, diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 8d1f7568..1dd78dfd 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,12 +1,10 @@ use super::{prfitem::PrfItem, PrfOption}; -use crate::{ - logging, - utils::{dirs, help, logging::Type}, -}; +use crate::utils::{dirs, help}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; -use serde_yaml::{Mapping, Value}; +use serde_yaml::Mapping; use std::{fs, io::Write}; + /// Define the `profiles.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct IProfiles { @@ -384,92 +382,16 @@ impl IProfiles { pub fn current_mapping(&self) -> Result { match (self.current.as_ref(), self.items.as_ref()) { (Some(current), Some(items)) => { - logging!( - info, - Type::Config, - true, - "开始获取当前配置文件 current_uid={}", - current - ); - logging!(info, Type::Core, true, "服务可用,直接使用服务模式"); if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { let file_path = match item.file.as_ref() { - Some(file) => { - let path = dirs::app_profiles_dir()?.join(file); - logging!( - info, - Type::Config, - true, - "找到配置文件路径: {}", - path.display() - ); - path - } - None => { - logging!( - error, - Type::Config, - true, - "配置项缺少file字段 uid={}", - current - ); - bail!("failed to get the file field"); - } + Some(file) => dirs::app_profiles_dir()?.join(file), + None => bail!("failed to get the file field"), }; - if !file_path.exists() { - logging!( - error, - Type::Config, - true, - "配置文件不存在: {}", - file_path.display() - ); - } - match help::read_mapping(&file_path) { - Ok(mapping) => { - let key_count = mapping.len(); - logging!( - info, - Type::Config, - true, - "成功读取配置文件, 包含{}个键值对", - key_count - ); - // 打印主要的配置键 - let important_keys = ["proxies", "proxy-groups", "rules"]; - for key in important_keys.iter() { - if mapping.contains_key(&Value::from(*key)) { - logging!(info, Type::Config, true, "配置包含关键字段: {}", key); - } else { - logging!(warn, Type::Config, true, "配置缺少关键字段: {}", key); - } - } - return Ok(mapping); - } - Err(e) => { - logging!(error, Type::Config, true, "读取配置文件失败: {}", e); - // 将错误发送到前端显示 - crate::core::handle::Handle::notice_message( - "config_validate::yaml_syntax_error", - &format!("{}", e), - ); - return Err(e); - } - } + return help::read_mapping(&file_path); } - logging!( - error, - Type::Config, - true, - "未找到当前配置项 uid={}", - current - ); bail!("failed to find the current profile \"uid:{current}\""); } - _ => { - logging!(warn, Type::Config, true, "没有当前配置项,返回空配置"); - Ok(Mapping::new()) - } + _ => Ok(Mapping::new()), } } diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 2cae995f..f887c1a7 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,14 +1,24 @@ use once_cell::sync::OnceCell; use parking_lot::RwLock; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use tauri::{AppHandle, Emitter, Manager, WebviewWindow}; -use crate::{logging_error, utils::logging::Type}; +use crate::{logging, logging_error, utils::logging::Type}; + +/// 存储启动期间的错误消息 +#[derive(Debug, Clone)] +struct ErrorMessage { + status: String, + message: String, +} #[derive(Debug, Default, Clone)] pub struct Handle { pub app_handle: Arc>>, pub is_exiting: Arc>, + /// 存储启动过程中产生的错误消息队列 + startup_errors: Arc>>, + startup_completed: Arc>, } impl Handle { @@ -18,6 +28,8 @@ impl Handle { HANDLE.get_or_init(|| Handle { app_handle: Arc::new(RwLock::new(None)), is_exiting: Arc::new(RwLock::new(false)), + startup_errors: Arc::new(RwLock::new(Vec::new())), + startup_completed: Arc::new(RwLock::new(false)), }) } @@ -70,16 +82,89 @@ impl Handle { } } + /// 通知前端显示消息,如果在启动过程中,则将消息存入启动错误队列 pub fn notice_message, M: Into>(status: S, msg: M) { - if let Some(window) = Self::global().get_window() { + let handle = Self::global(); + let status_str = status.into(); + let msg_str = msg.into(); + + // 检查是否正在启动过程中 + if !*handle.startup_completed.read() { + logging!( + info, + Type::Frontend, + true, + "启动过程中发现错误,加入消息队列: {} - {}", + status_str, + msg_str + ); + + // 将消息添加到启动错误队列 + let mut errors = handle.startup_errors.write(); + errors.push(ErrorMessage { + status: status_str, + message: msg_str, + }); + return; + } + + // 已经完成启动,直接发送消息 + if let Some(window) = handle.get_window() { logging_error!( Type::Frontend, true, - window.emit("verge://notice-message", (status.into(), msg.into())) + window.emit("verge://notice-message", (status_str, msg_str)) ); } } + /// 标记启动已完成,并发送所有启动阶段累积的错误消息 + pub fn mark_startup_completed(&self) { + { + let mut completed = self.startup_completed.write(); + *completed = true; + } + + self.send_startup_errors(); + } + + /// 发送启动时累积的所有错误消息 + fn send_startup_errors(&self) { + let errors = { + let mut errors = self.startup_errors.write(); + std::mem::take(&mut *errors) + }; + + if errors.is_empty() { + return; + } + + logging!( + info, + Type::Frontend, + true, + "发送{}条启动时累积的错误消息", + errors.len() + ); + + // 等待2秒以确保前端已完全加载,延迟发送错误通知 + if let Some(window) = self.get_window() { + let window_clone = window.clone(); + let errors_clone = errors.clone(); + + tauri::async_runtime::spawn(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + + for error in errors_clone { + let _ = + window_clone.emit("verge://notice-message", (error.status, error.message)); + // 每条消息之间间隔500ms,避免消息堆积 + tokio::time::sleep(Duration::from_millis(500)).await; + } + }); + } + } + pub fn set_is_exiting(&self) { let mut is_exiting = self.is_exiting.write(); *is_exiting = true; diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index b552dfef..ffe34c4d 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -82,3 +82,11 @@ pub async fn update_profile(uid: String, option: Option) -> Result<() Ok(()) } + +/// 增强配置 +pub async fn enhance_profiles() -> Result<()> { + crate::core::CoreManager::global() + .update_config() + .await + .map(|_| ()) +} diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 9d1b17c1..78f8e874 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,8 +1,10 @@ use crate::enhance::seq::SeqMap; +use crate::logging; +use crate::utils::logging::Type; use anyhow::{anyhow, bail, Context, Result}; use nanoid::nanoid; use serde::{de::DeserializeOwned, Serialize}; -use serde_yaml::{Mapping, Value}; +use serde_yaml::Mapping; use std::{fs, path::PathBuf, str::FromStr}; /// read data from yaml as struct T @@ -22,19 +24,41 @@ pub fn read_yaml(path: &PathBuf) -> Result { }) } -/// read mapping from yaml fix #165 +/// read mapping from yaml pub fn read_mapping(path: &PathBuf) -> Result { - let mut val: Value = read_yaml(path)?; - val.apply_merge() - .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; + if !path.exists() { + bail!("file not found \"{}\"", path.display()); + } - Ok(val - .as_mapping() - .ok_or(anyhow!( - "failed to transform to yaml mapping \"{}\"", - path.display() - ))? - .to_owned()) + let yaml_str = fs::read_to_string(path) + .with_context(|| format!("failed to read the file \"{}\"", path.display()))?; + + // YAML语法检查 + match serde_yaml::from_str::(&yaml_str) { + Ok(mut val) => { + val.apply_merge() + .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; + + Ok(val + .as_mapping() + .ok_or(anyhow!( + "failed to transform to yaml mapping \"{}\"", + path.display() + ))? + .to_owned()) + } + Err(err) => { + let error_msg = format!("YAML syntax error in {}: {}", path.display(), err); + logging!(error, Type::Config, true, "{}", error_msg); + + crate::core::handle::Handle::notice_message( + "config_validate::yaml_syntax_error", + &error_msg, + ); + + bail!("YAML syntax error: {}", err) + } + } } /// read mapping from yaml fix #165 diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 79b1aa9b..29f831cc 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -239,6 +239,24 @@ pub fn create_window() { // 设置窗口状态监控,实时保存窗口位置和大小 crate::feat::setup_window_state_monitor(&app_handle); + + // 标记前端UI已准备就绪,向前端发送启动完成事件 + let app_handle_clone = app_handle.clone(); + tauri::async_runtime::spawn(async move { + use tauri::Emitter; + + logging!( + info, + Type::Window, + true, + "标记前端UI已准备就绪,开始处理启动错误队列" + ); + handle::Handle::global().mark_startup_completed(); + + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.emit("verge://startup-completed", ()); + } + }); } Err(e) => { logging!(