mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-26 22:22:13 +08:00
refactor: structure config validation outcomes
- Replace boolean/string validation results with structured ValidationOutcome values across config validation, profile switching, DNS validation, and script validation. Callers can now distinguish valid, invalid, skipped, and busy states, with invalid results carrying an error kind and message. - Route core config updates through structured outcomes and checked forced updates. Generation and validation failures now discard runtime drafts and surface validation failures instead of relying on empty success messages. - Preserve existing frontend ergonomics by keeping boolean service wrappers where callers only need success, while direct DNS and script validation consumers receive ValidationOutcome. Failed profile editor saves reload the backend-restored file content and synchronize Monaco models so reopened editors show the restored state.
This commit is contained in:
+147
-44
@@ -1,7 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -15,6 +19,103 @@ pub struct CoreConfigValidator {
|
||||
is_processing: AtomicBool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ValidationErrorKind {
|
||||
FileMissing,
|
||||
FileRead,
|
||||
YamlSyntax,
|
||||
YamlMapping,
|
||||
ScriptSyntax,
|
||||
ScriptMissingMain,
|
||||
CoreRejected,
|
||||
ProcessTerminated,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl ValidationErrorKind {
|
||||
pub fn from_message(message: &str) -> Self {
|
||||
let lower = message.to_ascii_lowercase();
|
||||
|
||||
if lower.contains("file not found") {
|
||||
Self::FileMissing
|
||||
} else if lower.contains("failed to read") || lower.contains("无法读取") {
|
||||
Self::FileRead
|
||||
} else if lower.contains("script must contain a main function") {
|
||||
Self::ScriptMissingMain
|
||||
} else if lower.contains("script syntax error") {
|
||||
Self::ScriptSyntax
|
||||
} else if lower.contains("mapping values are not allowed")
|
||||
|| lower.contains("failed to transform to yaml mapping")
|
||||
|| lower.contains("failed to apply merge")
|
||||
{
|
||||
Self::YamlMapping
|
||||
} else if lower.contains("yaml syntax error") || lower.contains("did not find expected key") {
|
||||
Self::YamlSyntax
|
||||
} else if lower.contains("timeout") || lower.contains("超时") {
|
||||
Self::Timeout
|
||||
} else if lower.contains("terminated") || lower.contains("被终止") {
|
||||
Self::ProcessTerminated
|
||||
} else {
|
||||
Self::CoreRejected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ValidationSkipReason {
|
||||
Exiting,
|
||||
Debounced,
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidationSkipReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Exiting => write!(f, "application is exiting"),
|
||||
Self::Debounced => write!(f, "debounced"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ValidationOutcome {
|
||||
Valid,
|
||||
Invalid { kind: ValidationErrorKind, message: String },
|
||||
Skipped { reason: ValidationSkipReason },
|
||||
Busy,
|
||||
}
|
||||
|
||||
impl ValidationOutcome {
|
||||
pub fn invalid(kind: ValidationErrorKind, message: impl Into<String>) -> Self {
|
||||
Self::Invalid {
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_from_message(message: impl Into<String>) -> Self {
|
||||
let message = message.into();
|
||||
Self::invalid(ValidationErrorKind::from_message(&message), message)
|
||||
}
|
||||
|
||||
pub const fn is_valid(&self) -> bool {
|
||||
matches!(self, Self::Valid)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidationOutcome {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Valid => write!(f, "configuration is valid"),
|
||||
Self::Invalid { message, .. } => write!(f, "{message}"),
|
||||
Self::Skipped { reason } => write!(f, "Configuration validation skipped: {reason}"),
|
||||
Self::Busy => write!(f, "Configuration validation is already running"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreConfigValidator {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
@@ -98,16 +199,16 @@ impl CoreConfigValidator {
|
||||
}
|
||||
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
|
||||
async fn validate_file_syntax_outcome(config_path: &str) -> Result<ValidationOutcome> {
|
||||
logging!(info, Type::Validate, "开始检查文件: {}", config_path);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match fs::read_to_string(config_path).await {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {err}").into();
|
||||
let error_msg: String = format!("Failed to read file: {err}").into();
|
||||
logging!(error, Type::Validate, "无法读取文件: {}", error_msg);
|
||||
return Ok((false, error_msg));
|
||||
return Ok(ValidationOutcome::invalid_from_message(error_msg));
|
||||
}
|
||||
};
|
||||
// 对YAML文件尝试解析,只检查语法正确性
|
||||
@@ -115,27 +216,25 @@ impl CoreConfigValidator {
|
||||
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Validate, "YAML语法检查通过");
|
||||
Ok((true, String::new()))
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {err}").into();
|
||||
let error_msg: String = format!("YAML syntax error: {err}").into();
|
||||
logging!(error, Type::Validate, "YAML语法错误: {}", error_msg);
|
||||
Ok((false, error_msg))
|
||||
Ok(ValidationOutcome::invalid_from_message(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证脚本文件语法
|
||||
async fn validate_script_file(path: &str) -> Result<(bool, String)> {
|
||||
async fn validate_script_file_outcome(path: &str) -> Result<ValidationOutcome> {
|
||||
// 读取脚本内容
|
||||
let content = match fs::read_to_string(path).await {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {err}").into();
|
||||
let error_msg: String = format!("Failed to read script file: {err}").into();
|
||||
logging!(warn, Type::Validate, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
return Ok(ValidationOutcome::invalid_from_message(error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -158,53 +257,50 @@ impl CoreConfigValidator {
|
||||
{
|
||||
let error_msg = "Script must contain a main function";
|
||||
logging!(warn, Type::Validate, "脚本缺少main函数: {}", path);
|
||||
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||
return Ok((false, error_msg.into()));
|
||||
return Ok(ValidationOutcome::invalid_from_message(error_msg));
|
||||
}
|
||||
|
||||
Ok((true, String::new()))
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {err}").into();
|
||||
let error_msg: String = format!("Script syntax error: {err}").into();
|
||||
logging!(warn, Type::Validate, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
Ok(ValidationOutcome::invalid_from_message(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证指定的配置文件
|
||||
pub async fn validate_config_file(config_path: &str, is_merge_file: Option<bool>) -> Result<(bool, String)> {
|
||||
pub async fn validate_config_file_outcome(
|
||||
config_path: &str,
|
||||
is_merge_file: Option<bool>,
|
||||
) -> Result<ValidationOutcome> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, "应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
return Ok(ValidationOutcome::Skipped {
|
||||
reason: ValidationSkipReason::Exiting,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
let error_msg = format!("File not found: {config_path}").into();
|
||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
let error_msg: String = format!("File not found: {config_path}").into();
|
||||
return Ok(ValidationOutcome::invalid_from_message(error_msg));
|
||||
}
|
||||
|
||||
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
|
||||
if is_merge_file.unwrap_or(false) {
|
||||
logging!(info, Type::Validate, "检测到Merge文件,仅进行语法检查: {}", config_path);
|
||||
return Self::validate_file_syntax(config_path).await;
|
||||
return Self::validate_file_syntax_outcome(config_path).await;
|
||||
}
|
||||
|
||||
// 检查是否为脚本文件
|
||||
let is_script = if config_path.ends_with(".js") {
|
||||
true
|
||||
} else {
|
||||
match Self::is_script_file(config_path).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||
logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err);
|
||||
return Self::validate_config_internal(config_path).await;
|
||||
}
|
||||
let is_script = match Self::is_script_file(config_path).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||
logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err);
|
||||
return Self::validate_config_internal_outcome(config_path).await;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,20 +311,22 @@ impl CoreConfigValidator {
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
config_path
|
||||
);
|
||||
return Self::validate_script_file(config_path).await;
|
||||
return Self::validate_script_file_outcome(config_path).await;
|
||||
}
|
||||
|
||||
// 对YAML配置文件使用Clash内核验证
|
||||
logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path);
|
||||
Self::validate_config_internal(config_path).await
|
||||
Self::validate_config_internal_outcome(config_path).await
|
||||
}
|
||||
|
||||
/// 内部验证配置文件的实现
|
||||
async fn validate_config_internal(config_path: &str) -> Result<(bool, String)> {
|
||||
async fn validate_config_internal_outcome(config_path: &str) -> Result<ValidationOutcome> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Validate, "应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
return Ok(ValidationOutcome::Skipped {
|
||||
reason: ValidationSkipReason::Exiting,
|
||||
});
|
||||
}
|
||||
|
||||
logging!(info, Type::Validate, "开始验证配置文件: {}", config_path);
|
||||
@@ -276,19 +374,24 @@ impl CoreConfigValidator {
|
||||
};
|
||||
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
let outcome = if status.code().is_none() {
|
||||
ValidationOutcome::invalid(ValidationErrorKind::ProcessTerminated, error_msg)
|
||||
} else {
|
||||
ValidationOutcome::invalid_from_message(error_msg)
|
||||
};
|
||||
Ok(outcome)
|
||||
} else {
|
||||
logging!(info, Type::Validate, "验证成功");
|
||||
logging!(info, Type::Validate, "-------- 验证结束 --------");
|
||||
Ok((true, String::new()))
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
pub async fn validate_config_outcome(&self) -> Result<ValidationOutcome> {
|
||||
if !self.try_start() {
|
||||
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
|
||||
return Ok((true, String::new()));
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
self.finish();
|
||||
@@ -297,7 +400,7 @@ impl CoreConfigValidator {
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Check).await?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
Self::validate_config_internal(config_path).await
|
||||
Self::validate_config_internal_outcome(config_path).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user