feat: Enhance configuration validation and error handling during app startup

This commit is contained in:
wonfen 2025-02-24 06:21:32 +08:00
parent afc238d60e
commit 23f75598e5
4 changed files with 186 additions and 96 deletions

View File

@ -3,10 +3,12 @@ use crate::{
config::PrfItem,
enhance,
utils::{dirs, help},
core::{handle, CoreManager},
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@ -64,12 +66,69 @@ impl Config {
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
}
crate::log_err!(Self::generate().await);
if let Err(err) = Self::generate_file(ConfigType::Run) {
log::error!(target: "app", "{err}");
// 生成运行时配置
crate::log_err!(Self::generate().await);
// 生成运行时配置文件并验证
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
// 如果不存在就将默认的clash文件拿过来
let config_result = Self::generate_file(ConfigType::Run);
let validation_result = if let Ok(_) = config_result {
// 验证配置文件
println!("[首次启动] 开始验证配置文件");
match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => {
if !is_valid {
println!("[首次启动] 配置验证失败,使用默认配置启动 {}", error_msg);
// 使用默认配置
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
if error_msg.is_empty() {
Some(("config_validate::boot_error", String::new()))
} else {
Some(("config_validate::stderr_error", error_msg))
}
} else {
println!("[首次启动] 配置验证成功");
Some(("config_validate::success", String::new()))
}
}
Err(err) => {
println!("[首次启动] 验证进程执行失败 {}", err);
// 使用默认配置
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
Some(("config_validate::process_terminated", String::new()))
}
}
} else {
println!("[首次启动] 生成配置文件失败,使用默认配置");
// 如果生成失败就将默认的clash文件拿过来
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
if !runtime_path.exists() {
help::save_yaml(
&runtime_path,
@ -77,7 +136,17 @@ impl Config {
Some("# Clash Verge Runtime"),
)?;
}
Some(("config_validate::error", String::new()))
};
// 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result {
tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
});
}
Ok(())
}

View File

@ -18,7 +18,7 @@ interface InnerProps {
}
const NoticeInner = (props: InnerProps) => {
const { type, message, duration = 1500, onClose } = props;
const { type, message, duration, onClose } = props;
const [visible, setVisible] = useState(true);
const [isDark, setIsDark] = useState(false);
const { verge } = useVerge();
@ -72,7 +72,7 @@ const NoticeInner = (props: InnerProps) => {
<Snackbar
open={visible}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={duration}
autoHideDuration={duration === -1 ? null : duration || 1500}
onClose={onAutoClose}
message={msgElement}
sx={{
@ -149,11 +149,11 @@ export const Notice: NoticeInstance = (props) => {
return;
}
// 直接调用,不使用 setTimeout
Notice({
type,
message,
duration: duration || 1500, // 确保有默认值
// 错误类型通知显示 8 秒,其他类型默认 1.5 秒
duration: type === "error" ? 8000 : duration || 1500,
});
};
});

View File

@ -436,6 +436,7 @@
"Enable Tray Speed": "启用托盘速率",
"Lite Mode": "轻量模式",
"Lite Mode Info": "关闭GUI界面仅保留内核运行",
"Config Validation Failed": "订阅配置校验失败,请检查配置文件",
"Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,修改已回滚",
"Boot Config Validation Failed": "启动订阅配置校验失败,使用默认配置启动,请检查订阅配置文件",
"Config Validation Process Terminated": "验证进程被终止"
}

View File

@ -2,7 +2,7 @@ import dayjs from "dayjs";
import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr";
import { useEffect } from "react";
import { useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
@ -36,42 +36,18 @@ dayjs.extend(relativeTime);
const OS = getSystem();
const Layout = () => {
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
// 通知处理函数
const handleNoticeMessage = (
status: string,
msg: string,
t: (key: string) => string,
navigate: (path: string, options?: any) => void,
) => {
console.log("[通知监听] 收到消息:", status, msg);
const { verge } = useVerge();
const { language, start_page } = verge || {};
const navigate = useNavigate();
const location = useLocation();
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
if (!routersEles) return null;
setupCloseListener();
useEffect(() => {
addListener("verge://refresh-clash-config", async () => {
// the clash info may be updated
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
});
// update the verge config
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig"));
// 设置提示监听
addListener("verge://notice-message", ({ payload }) => {
const [status, msg] = payload as [string, string];
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
Notice.success(t("Import Subscription Successful"));
break;
case "import_sub_url::error":
@ -81,6 +57,9 @@ const Layout = () => {
case "set_config::error":
Notice.error(msg);
break;
case "config_validate::boot_error":
Notice.error(t("Boot Config Validation Failed"));
break;
case "config_validate::error":
Notice.error(t("Config Validation Failed"));
break;
@ -90,37 +69,83 @@ const Layout = () => {
case "config_validate::stderr_error":
Notice.error(msg);
break;
default:
break;
}
});
};
setTimeout(async () => {
portableFlag = await getPortableFlag();
await appWindow.unminimize();
await appWindow.show();
await appWindow.setFocus();
}, 50);
const Layout = () => {
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { language, start_page } = verge ?? {};
const navigate = useNavigate();
const location = useLocation();
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
// 监听窗口显示/隐藏事件
const setupListeners = async () => {
const unlisten1 = await listen("verge://hide-window", () => {
appWindow.hide();
});
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload;
handleNoticeMessage(status, msg, t, navigate);
},
[t, navigate],
);
const unlisten2 = await listen("verge://show-window", () => {
appWindow.show();
});
// 设置监听器
useEffect(() => {
const listeners = [
// 配置更新监听
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
// verge 配置更新监听
addListener("verge://refresh-verge-config", () =>
mutate("getVergeConfig"),
),
// 通知消息监听
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
];
// 设置窗口显示/隐藏监听
const setupWindowListeners = async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
return () => {
unlisten1();
unlisten2();
hideUnlisten();
showUnlisten();
};
};
setupListeners();
}, []);
// 初始化
setupCloseListener();
const cleanupWindow = setupWindowListeners();
// 清理函数
return () => {
// 清理主要监听器
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
// 清理窗口监听器
cleanupWindow.then((cleanup) => cleanup());
};
}, [handleNotice]);
// 语言和起始页设置
useEffect(() => {
if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language);
@ -129,7 +154,9 @@ const Layout = () => {
if (start_page) {
navigate(start_page);
}
}, [language, start_page]);
}, [language, start_page, navigate]);
if (!routersEles) return null;
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
@ -139,23 +166,18 @@ const Layout = () => {
elevation={0}
className={`${OS} layout`}
onContextMenu={(e) => {
// only prevent it on Windows
const validList = ["input", "textarea"];
const target = e.currentTarget;
if (
OS === "windows" &&
!(
validList.includes(target.tagName.toLowerCase()) ||
target.isContentEditable
)
!["input", "textarea"].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault();
}
}}
sx={[
({ palette }) => ({
bgcolor: palette.background.paper,
}),
({ palette }) => ({ bgcolor: palette.background.paper }),
{
borderRadius: "8px",
border: "2px solid var(--divider-color)",
@ -186,7 +208,7 @@ const Layout = () => {
/>
<LogoSvg fill={isDark ? "white" : "black"} />
</div>
{<UpdateButton className="the-newbtn" />}
<UpdateButton className="the-newbtn" />
</div>
<List className="the-menu">
@ -207,16 +229,14 @@ const Layout = () => {
</div>
<div className="layout__right">
{
<div className="the-bar">
<div
className="the-dragbar"
data-tauri-drag-region="true"
style={{ width: "100%" }}
></div>
/>
{OS !== "macos" && <LayoutControl />}
</div>
}
<TransitionGroup className="the-content">
<CSSTransition