mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 00:13:44 +08:00
feat: Enhance configuration validation and error handling
- Improve config validation process with detailed logging and error tracking - Add more robust error handling in profile updates and config patches - Implement comprehensive config validation using clash core subprocess
This commit is contained in:
parent
16caccde51
commit
1291c38d58
@ -31,7 +31,7 @@ serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
once_cell = "1.19"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
window-shadows = { version = "0.2.2" }
|
||||
|
@ -13,6 +13,8 @@ use sysproxy::{Autoproxy, Sysproxy};
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use std::fs;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
@ -66,29 +68,64 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||
pub async fn patch_profiles_config(
|
||||
profiles: IProfiles
|
||||
) -> CmdResult {
|
||||
println!("[cmd配置patch] 开始修改配置文件");
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
||||
|
||||
// 更新profiles配置
|
||||
println!("[cmd配置patch] 正在更新配置草稿");
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[cmd配置patch] 配置更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
let _ = tray::Tray::global().update_tooltip();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
|
||||
// 发送成功通知
|
||||
handle::Handle::notice_message("operation_success", "配置patch成功");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[cmd配置patch] 更新配置失败: {}", err);
|
||||
Config::profiles().discard();
|
||||
log::error!(target: "app", "{err}");
|
||||
Err(format!("{err}"))
|
||||
println!("[cmd配置patch] 错误详情: {}", err);
|
||||
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult {
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String
|
||||
) -> CmdResult {
|
||||
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
@ -97,7 +134,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
wrap_err!(timer::Timer::global().refresh())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -125,17 +162,60 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content)
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
// 直接验证保存的配置文件
|
||||
let clash_core = Config::verge().latest().clash_core.clone().unwrap_or("verge-mihomo".into());
|
||||
let test_dir = wrap_err!(dirs::app_home_dir())?.join("test");
|
||||
let test_dir = test_dir.to_string_lossy();
|
||||
let file_path_str = file_path.to_string_lossy();
|
||||
|
||||
println!("[cmd配置save] 开始验证配置文件: {}", file_path_str);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let output = wrap_err!(app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)
|
||||
.map_err(|e| e.to_string())?
|
||||
.args(["-t", "-d", test_dir.as_ref(), "-f", file_path_str.as_ref()])
|
||||
.output()
|
||||
.await)?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
if has_error {
|
||||
println!("[cmd配置save] 编辑验证失败 {}", stderr);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &*stderr);
|
||||
return Err(format!("cmd配置save失败 {}", stderr));
|
||||
}
|
||||
|
||||
println!("[cmd配置save] 验证成功");
|
||||
handle::Handle::notice_message("operation_success", "配置更新成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
@ -145,29 +145,119 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新proxies那些
|
||||
/// 如果涉及端口和外部控制则需要重启
|
||||
/// 使用子进程验证配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
println!("[core配置验证] 开始验证配置");
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
println!("[core配置验证] 配置文件路径: {}", config_path);
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
println!("[core配置验证] 使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let test_dir = dirs::app_home_dir()?.join("test");
|
||||
let test_dir = dirs::path_to_str(&test_dir)?;
|
||||
println!("[core配置验证] 测试目录: {}", test_dir);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
println!("[core配置验证] 运行子进程验证配置");
|
||||
let output = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
.args(["-t", "-d", test_dir, "-f", config_path])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// 检查进程退出状态和错误输出
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
println!("[core配置验证] 退出状态: {:?}", output.status);
|
||||
if !stderr.is_empty() {
|
||||
println!("[core配置验证] 错误输出: {}", stderr);
|
||||
}
|
||||
if !stdout.is_empty() {
|
||||
println!("[core配置验证] 标准输出: {}", stdout);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
let error_msg = if stderr.is_empty() {
|
||||
if let Some(code) = output.status.code() {
|
||||
handle::Handle::notice_message("config_validate::error", &code.to_string());
|
||||
String::new()
|
||||
} else {
|
||||
handle::Handle::notice_message("config_validate::process_terminated", "");
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
handle::Handle::notice_message("config_validate::stderr_error", &*stderr);
|
||||
String::new()
|
||||
};
|
||||
Ok((false, error_msg))
|
||||
} else {
|
||||
handle::Handle::notice_message("config_validate::success", "");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新proxies等配置
|
||||
pub async fn update_config(&self) -> Result<()> {
|
||||
log::debug!(target: "app", "try to update clash config");
|
||||
// 更新订阅
|
||||
println!("[core配置更新] 开始更新配置");
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
println!("[core配置更新] 生成新的配置内容");
|
||||
Config::generate().await?;
|
||||
|
||||
// 2. 生成临时文件并进行验证
|
||||
println!("[core配置更新] 生成临时配置文件用于验证");
|
||||
let temp_config = Config::generate_file(ConfigType::Check)?;
|
||||
let temp_config = dirs::path_to_str(&temp_config)?;
|
||||
println!("[core配置更新] 临时配置文件路径: {}", temp_config);
|
||||
|
||||
// 检查订阅是否正常
|
||||
self.check_config().await?;
|
||||
// 3. 验证配置
|
||||
let (is_valid, error_msg) = match self.validate_config().await {
|
||||
Ok((valid, msg)) => (valid, msg),
|
||||
Err(e) => {
|
||||
println!("[core配置更新] 验证过程发生错误: {}", e);
|
||||
Config::runtime().discard(); // 验证失败时丢弃新配置
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新运行时订阅
|
||||
let path = Config::generate_file(ConfigType::Run)?;
|
||||
let path = dirs::path_to_str(&path)?;
|
||||
if !is_valid {
|
||||
println!("[core配置更新] 配置验证未通过,保持当前配置不变");
|
||||
Config::runtime().discard(); // 验证失败时丢弃新配置
|
||||
return Err(anyhow::anyhow!(error_msg));
|
||||
}
|
||||
|
||||
// 发送请求 发送5次
|
||||
// 4. 验证通过后,生成正式的运行时配置
|
||||
println!("[core配置更新] 验证通过,生成运行时配置");
|
||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||
let run_path = dirs::path_to_str(&run_path)?;
|
||||
|
||||
// 5. 应用新配置
|
||||
println!("[core配置更新] 应用新配置");
|
||||
for i in 0..10 {
|
||||
match clash_api::put_configs(path).await {
|
||||
Ok(_) => break,
|
||||
match clash_api::put_configs(run_path).await {
|
||||
Ok(_) => {
|
||||
println!("[core配置更新] 配置应用成功");
|
||||
Config::runtime().apply(); // 应用成功时保存新配置
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if i < 9 {
|
||||
println!("[core配置更新] 第{}次重试应用配置", i + 1);
|
||||
log::info!(target: "app", "{err}");
|
||||
} else {
|
||||
bail!(err);
|
||||
println!("[core配置更新] 配置应用失败: {}", err);
|
||||
Config::runtime().discard(); // 应用失败时丢弃新配置
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::feat;
|
||||
use crate::core::CoreManager;
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
@ -172,7 +173,17 @@ impl Timer {
|
||||
/// the task runner
|
||||
async fn async_task(uid: String) {
|
||||
log::info!(target: "app", "running timer task `{uid}`");
|
||||
crate::log_err!(feat::update_profile(uid, None).await);
|
||||
|
||||
// 使用更轻量级的更新方式
|
||||
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
||||
log::error!(target: "app", "timer task update error: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有更新成功后才刷新配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "timer task refresh error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,12 +133,14 @@ pub fn toggle_system_proxy() {
|
||||
// 切换代理文件
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match cmds::patch_profiles_config_by_profile_index(profile_index).await {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
match cmds::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
handle::Handle::refresh_verge();
|
||||
},
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -379,6 +381,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
/// 更新某个profile
|
||||
/// 如果更新当前订阅就激活订阅
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
@ -386,33 +390,44 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
None // 直接更新
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
println!("[订阅更新] 更新订阅配置");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
Some(uid) == profiles.get_current()
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ const NoticeInner = (props: InnerProps) => {
|
||||
|
||||
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
|
||||
const unlisten = appWindow.onThemeChanged((e) =>
|
||||
setIsDark(e.payload === "dark")
|
||||
setIsDark(e.payload === "dark"),
|
||||
);
|
||||
|
||||
return () => {
|
||||
@ -105,25 +105,55 @@ let parent: HTMLDivElement = null!;
|
||||
|
||||
// @ts-ignore
|
||||
export const Notice: NoticeInstance = (props) => {
|
||||
const { type, message, duration } = props;
|
||||
|
||||
// 验证必要的参数
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
parent = document.createElement("div");
|
||||
parent.setAttribute("id", "notice-container"); // 添加 id 便于调试
|
||||
document.body.appendChild(parent);
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
parent.appendChild(container);
|
||||
|
||||
const root = createRoot(container);
|
||||
|
||||
const onUnmount = () => {
|
||||
root.unmount();
|
||||
if (parent) setTimeout(() => parent.removeChild(container), 500);
|
||||
if (parent && container.parentNode === parent) {
|
||||
setTimeout(() => {
|
||||
parent.removeChild(container);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
root.render(<NoticeInner {...props} onClose={onUnmount} />);
|
||||
root.render(
|
||||
<NoticeInner
|
||||
type={type}
|
||||
message={message}
|
||||
duration={duration || 1500}
|
||||
onClose={onUnmount}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
(["info", "error", "success"] as const).forEach((type) => {
|
||||
Notice[type] = (message, duration) => {
|
||||
setTimeout(() => Notice({ type, message, duration }), 0);
|
||||
Notice[type] = (message: ReactNode, duration?: number) => {
|
||||
// 确保消息不为空
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接调用,不使用 setTimeout
|
||||
Notice({
|
||||
type,
|
||||
message,
|
||||
duration: duration || 1500, // 确保有默认值
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@ -435,5 +435,7 @@
|
||||
"Direct Mode": "直连模式",
|
||||
"Enable Tray Speed": "启用托盘速率",
|
||||
"Lite Mode": "轻量模式",
|
||||
"Lite Mode Info": "关闭GUI界面,仅保留内核运行"
|
||||
"Lite Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||
"Config Validation Failed": "订阅配置校验失败,请检查配置文件",
|
||||
"Config Validation Process Terminated": "验证进程被终止"
|
||||
}
|
||||
|
@ -81,6 +81,15 @@ const Layout = () => {
|
||||
case "set_config::error":
|
||||
Notice.error(msg);
|
||||
break;
|
||||
case "config_validate::error":
|
||||
Notice.error(t("Config Validation Failed"));
|
||||
break;
|
||||
case "config_validate::process_terminated":
|
||||
Notice.error(t("Config Validation Process Terminated"));
|
||||
break;
|
||||
case "config_validate::stderr_error":
|
||||
Notice.error(msg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
importProfile,
|
||||
enhanceProfiles,
|
||||
restartCore,
|
||||
//restartCore,
|
||||
getRuntimeLogs,
|
||||
deleteProfile,
|
||||
updateProfile,
|
||||
@ -400,8 +400,8 @@ const ProfilePage = () => {
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
await onEnhance(false);
|
||||
await restartCore();
|
||||
Notice.success(t("Clash Core Restarted"), 1000);
|
||||
// await restartCore();
|
||||
// Notice.success(t("Clash Core Restarted"), 1000);
|
||||
}
|
||||
}}
|
||||
onDelete={() => onDelete(item.uid)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user