feat: enhance profile management and proxy refresh with improved event listening and state updates

This commit is contained in:
wonfen
2025-06-17 11:38:53 +08:00
parent 4068e5ec9c
commit ac7307b1f7
9 changed files with 468 additions and 103 deletions

View File

@@ -12,39 +12,85 @@ use tokio::sync::Mutex;
// 添加全局互斥锁防止并发配置更新 // 添加全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
/// 获取配置文件列表 /// 获取配置文件避免锁竞争
#[tauri::command] #[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> { pub async fn get_profiles() -> CmdResult<IProfiles> {
let profiles_result = tokio::time::timeout( // 策略1: 尝试快速获取latest数据
Duration::from_secs(3), // 3秒超时 let latest_result = tokio::time::timeout(
tokio::task::spawn_blocking(move || Config::profiles().data().clone()), Duration::from_millis(500),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let latest = profiles.latest();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
}),
) )
.await; .await;
match profiles_result { match latest_result {
Ok(Ok(profiles)) => Ok(*profiles), Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => { Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
} }
Err(_) => { Err(_) => {
// 超时情况 logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败尝试获取data()
let data_result = tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let data = profiles.data();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
}),
)
.await;
match data_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!( logging!(
error, error,
Type::Cmd, Type::Cmd,
true, true,
"获取配置列表超时(3秒),可能存在锁竞争" "获取draft配置任务失败: {}",
join_err
); );
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(*profiles)
} }
Err(_) => { Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败返回空配置"); logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
}
}
// 策略3: fallback尝试重新创建配置
logging!(
warn,
Type::Cmd,
true,
"所有获取配置策略都失败尝试fallback"
);
match tokio::task::spawn_blocking(move || IProfiles::new()).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
}
Err(err) => {
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃
Ok(IProfiles { Ok(IProfiles {
current: None, current: None,
items: Some(vec![]), items: Some(vec![]),
@@ -52,8 +98,6 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
} }
} }
} }
}
}
/// 增强配置文件 /// 增强配置文件
#[tauri::command] #[tauri::command]
@@ -246,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Config::profiles().apply(); Config::profiles().apply();
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
// 强制刷新代理缓存确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await {
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
}
});
crate::process::AsyncHandler::spawn(|| async move { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() { if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "异步更新托盘提示失败: {}", e); log::warn!(target: "app", "异步更新托盘提示失败: {}", e);

View File

@@ -43,6 +43,28 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
Ok(*proxies) Ok(*proxies)
} }
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
log::debug!(target: "app", "强制刷新代理缓存");
let proxies = manager.get_refresh_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.proxies = Box::new(proxies.clone());
state.need_refresh = false;
state.last_refresh_time = Instant::now();
}
log::debug!(target: "app", "强制刷新代理缓存完成");
Ok(proxies)
}
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();

View File

@@ -151,6 +151,10 @@ impl NotificationSystem {
match window.emit(event_name_str, payload) { match window.emit(event_name_str, payload) {
Ok(_) => { Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst); system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {}", event_name_str);
}
} }
Err(e) => { Err(e) => {
log::warn!("Failed to emit event {}: {}", event_name_str, e); log::warn!("Failed to emit event {}: {}", event_name_str, e);
@@ -224,12 +228,27 @@ impl NotificationSystem {
} }
fn shutdown(&mut self) { fn shutdown(&mut self) {
log::info!("NotificationSystem shutdown initiated");
self.is_running = false; self.is_running = false;
self.sender = None;
if let Some(handle) = self.worker_handle.take() { // 先关闭发送端,让接收端知道不会再有新消息
let _ = handle.join(); if let Some(sender) = self.sender.take() {
drop(sender);
} }
// 设置超时避免无限等待
if let Some(handle) = self.worker_handle.take() {
match handle.join() {
Ok(_) => {
log::info!("NotificationSystem worker thread joined successfully");
}
Err(e) => {
log::error!("NotificationSystem worker thread join failed: {:?}", e);
}
}
}
log::info!("NotificationSystem shutdown completed");
} }
} }

View File

@@ -255,6 +255,7 @@ pub fn run() {
cmd::invoke_uwp_tool, cmd::invoke_uwp_tool,
cmd::copy_clash_env, cmd::copy_clash_env,
cmd::get_proxies, cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies, cmd::get_providers_proxies,
cmd::save_dns_config, cmd::save_dns_config,
cmd::apply_dns_config, cmd::apply_dns_config,

View File

@@ -10,11 +10,32 @@ export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR( const { data: profiles, mutate: mutateProfiles } = useSWR(
"getProfiles", "getProfiles",
getProfiles, getProfiles,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 2000,
errorRetryCount: 2,
errorRetryInterval: 1000,
},
); );
const patchProfiles = async (value: Partial<IProfilesConfig>) => { const patchProfiles = async (value: Partial<IProfilesConfig>) => {
// 立即更新本地状态
if (value.current && profiles) {
const optimisticUpdate = {
...profiles,
current: value.current,
};
mutateProfiles(optimisticUpdate, false); // 不重新验证
}
try {
await patchProfilesConfig(value); await patchProfilesConfig(value);
mutateProfiles(); mutateProfiles();
} catch (error) {
mutateProfiles();
throw error;
}
}; };
const patchCurrent = async (value: Partial<IProfileItem>) => { const patchCurrent = async (value: Partial<IProfileItem>) => {
@@ -26,40 +47,90 @@ export const useProfiles = () => {
// 根据selected的节点选择 // 根据selected的节点选择
const activateSelected = async () => { const activateSelected = async () => {
const proxiesData = await getProxies(); try {
const profileData = await getProfiles(); console.log("[ActivateSelected] 开始处理代理选择");
if (!profileData || !proxiesData) return; const [proxiesData, profileData] = await Promise.all([
getProxies(),
getProfiles(),
]);
if (!profileData || !proxiesData) {
console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理");
return;
}
const current = profileData.items?.find( const current = profileData.items?.find(
(e) => e && e.uid === profileData.current, (e) => e && e.uid === profileData.current,
); );
if (!current) return; if (!current) {
console.log("[ActivateSelected] 未找到当前profile配置");
return;
}
// init selected array // 检查是否有saved的代理选择
const { selected = [] } = current; const { selected = [] } = current;
if (selected.length === 0) {
console.log("[ActivateSelected] 当前profile无保存的代理选择跳过");
return;
}
console.log(
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
);
const selectedMap = Object.fromEntries( const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]), selected.map((each) => [each.name!, each.now!]),
); );
let hasChange = false; let hasChange = false;
const newSelected: typeof selected = []; const newSelected: typeof selected = [];
const { global, groups } = proxiesData; const { global, groups } = proxiesData;
// 处理所有代理组
[global, ...groups].forEach(({ type, name, now }) => { [global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") return; if (!now || type !== "Selector") {
if (selectedMap[name] != null && selectedMap[name] !== now) { if (selectedMap[name] != null) {
hasChange = true; newSelected.push({ name, now: now || selectedMap[name] });
updateProxy(name, selectedMap[name]);
} }
newSelected.push({ name, now: selectedMap[name] }); return;
}
const targetProxy = selectedMap[name];
if (targetProxy != null && targetProxy !== now) {
console.log(
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`,
);
hasChange = true;
updateProxy(name, targetProxy);
}
newSelected.push({ name, now: targetProxy || now });
}); });
if (hasChange) { if (!hasChange) {
patchProfile(profileData.current!, { selected: newSelected }); console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新");
return;
}
console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`);
try {
await patchProfile(profileData.current!, { selected: newSelected });
console.log("[ActivateSelected] 代理选择配置保存成功");
setTimeout(() => {
mutate("getProxies", getProxies()); mutate("getProxies", getProxies());
}, 100);
} catch (error: any) {
console.error(
"[ActivateSelected] 保存代理选择配置失败:",
error.message,
);
}
} catch (error: any) {
console.error("[ActivateSelected] 处理代理选择失败:", error.message);
} }
}; };

View File

@@ -169,7 +169,13 @@ const Layout = () => {
const handleNotice = useCallback( const handleNotice = useCallback(
(payload: [string, string]) => { (payload: [string, string]) => {
const [status, msg] = payload; const [status, msg] = payload;
setTimeout(() => {
try {
handleNoticeMessage(status, msg, t, navigate); handleNoticeMessage(status, msg, t, navigate);
} catch (error) {
console.error("[Layout] 处理通知消息失败:", error);
}
}, 0);
}, },
[t, navigate], [t, navigate],
); );
@@ -220,12 +226,35 @@ const Layout = () => {
const cleanupWindow = setupWindowListeners(); const cleanupWindow = setupWindowListeners();
return () => { return () => {
setTimeout(() => {
listeners.forEach((listener) => { listeners.forEach((listener) => {
if (typeof listener.then === "function") { if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten()); listener
.then((unlisten) => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error);
});
} }
}); });
cleanupWindow.then((cleanup) => cleanup());
cleanupWindow
.then((cleanup) => {
try {
cleanup();
} catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error);
});
}, 0);
}; };
}, [handleNotice]); }, [handleNotice]);

View File

@@ -190,27 +190,53 @@ const ProfilePage = () => {
} }
}; };
const activateProfile = async (profile: string, notifySuccess: boolean) => { const activateProfile = useLockFn(
async (profile: string, notifySuccess: boolean) => {
if (profiles.current === profile && !notifySuccess) {
console.log(
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
);
return;
}
// 避免大多数情况下loading态闪烁 // 避免大多数情况下loading态闪烁
const reset = setTimeout(() => { const reset = setTimeout(() => {
setActivatings((prev) => [...prev, profile]); setActivatings((prev) => [...prev, profile]);
}, 100); }, 100);
try { try {
console.log(`[Profile] 开始切换到: ${profile}`);
const success = await patchProfiles({ current: profile }); const success = await patchProfiles({ current: profile });
await mutateLogs(); await mutateLogs();
closeAllConnections(); closeAllConnections();
await activateSelected();
if (notifySuccess && success) { if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000); showNotice("success", t("Profile Switched"), 1000);
} }
// 立即清除loading状态
clearTimeout(reset);
setActivatings([]);
console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`);
setTimeout(async () => {
try {
await activateSelected();
console.log(`[Profile] 后台处理完成`);
} catch (err: any) { } catch (err: any) {
console.warn("Failed to activate selected proxies:", err);
}
}, 50);
} catch (err: any) {
console.error(`[Profile] 切换失败:`, err);
showNotice("error", err?.message || err.toString(), 4000); showNotice("error", err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset); clearTimeout(reset);
setActivatings([]); setActivatings([]);
} }
}; },
);
const onSelect = useLockFn(async (current: string, force: boolean) => { const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return; if (!force && current === profiles.current) return;
await activateProfile(current, true); await activateProfile(current, true);
@@ -300,31 +326,45 @@ const ProfilePage = () => {
// 监听后端配置变更 // 监听后端配置变更
useEffect(() => { useEffect(() => {
let unlistenPromise: Promise<() => void> | undefined; let unlistenPromise: Promise<() => void> | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined; let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const debounceDelay = 200;
const setupListener = async () => { const setupListener = async () => {
unlistenPromise = listen<string>("profile-changed", (event) => { unlistenPromise = listen<string>("profile-changed", (event) => {
console.log("Profile changed event received:", event.payload); const newProfileId = event.payload;
if (timeoutId) { const now = Date.now();
clearTimeout(timeoutId);
console.log(`[Profile] 收到配置变更事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < debounceDelay
) {
console.log(`[Profile] 重复事件被防抖,跳过`);
return;
} }
timeoutId = setTimeout(() => { lastProfileId = newProfileId;
mutateProfiles(); lastUpdateTime = now;
timeoutId = undefined;
}, 300); console.log(`[Profile] 执行配置数据刷新`);
// 使用异步调度避免阻塞事件处理
setTimeout(() => {
mutateProfiles().catch((error) => {
console.error("[Profile] 配置数据刷新失败:", error);
});
}, 0);
}); });
}; };
setupListener(); setupListener();
return () => { return () => {
if (timeoutId) { unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
clearTimeout(timeoutId);
}
unlistenPromise?.then((unlisten) => unlisten());
}; };
}, [mutateProfiles, t]); }, [mutateProfiles]);
return ( return (
<BasePage <BasePage

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo, useEffect } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { import {
@@ -8,10 +8,16 @@ import {
getProxyProviders, getProxyProviders,
getRuleProviders, getRuleProviders,
} from "@/services/api"; } from "@/services/api";
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds"; import {
getSystemProxy,
getRunningMode,
getAppUptime,
forceRefreshProxies,
} from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { createAuthSockette } from "@/utils/websocket"; import { createAuthSockette } from "@/utils/websocket";
import { useVisibility } from "@/hooks/use-visibility"; import { useVisibility } from "@/hooks/use-visibility";
import { listen } from "@tauri-apps/api/event";
// 定义AppDataContext类型 - 使用宽松类型 // 定义AppDataContext类型 - 使用宽松类型
interface AppDataContextType { interface AppDataContextType {
@@ -64,6 +70,126 @@ export const AppDataProvider = ({
}, },
); );
// 监听profile和clash配置变更事件
useEffect(() => {
let profileUnlisten: Promise<() => void> | undefined;
let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const refreshThrottle = 500;
const setupEventListeners = async () => {
try {
// 监听profile切换事件
profileUnlisten = listen<string>("profile-changed", (event) => {
const newProfileId = event.payload;
const now = Date.now();
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < refreshThrottle
) {
console.log("[AppDataProvider] 重复事件被防抖,跳过");
return;
}
lastProfileId = newProfileId;
lastUpdateTime = now;
setTimeout(async () => {
try {
console.log("[AppDataProvider] 强制刷新代理缓存");
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
console.log("[AppDataProvider] 刷新前端代理数据");
await refreshProxy();
console.log("[AppDataProvider] Profile切换的代理数据刷新完成");
} catch (error) {
console.error("[AppDataProvider] 强制刷新代理缓存失败:", error);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] 普通刷新也失败:", e),
);
}
}, 0);
});
// 监听Clash配置刷新事件(enhance操作等)
const handleRefreshClash = () => {
const now = Date.now();
console.log("[AppDataProvider] Clash配置刷新事件");
if (now - lastUpdateTime > refreshThrottle) {
lastUpdateTime = now;
setTimeout(async () => {
try {
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
// 添加超时保护
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
await refreshProxy();
} catch (error) {
console.error(
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
error,
);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
);
}
}, 0);
}
};
window.addEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
return () => {
window.removeEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
};
} catch (error) {
console.error("[AppDataProvider] 事件监听器设置失败:", error);
return () => {};
}
};
const cleanupPromise = setupEventListeners();
return () => {
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
cleanupPromise.then((cleanup) => cleanup());
};
}, [refreshProxy]);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR( const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig", "getClashConfig",
getClashConfig, getClashConfig,

View File

@@ -220,6 +220,12 @@ export async function cmdGetProxyDelay(
} }
} }
/// 用于profile切换等场景
export async function forceRefreshProxies() {
console.log("[API] 强制刷新代理缓存");
return invoke<any>("force_refresh_proxies");
}
export async function cmdTestDelay(url: string) { export async function cmdTestDelay(url: string) {
return invoke<number>("test_delay", { url }); return invoke<number>("test_delay", { url });
} }