From ac7307b1f7577d606d62509592c6619a968c61f2 Mon Sep 17 00:00:00 2001 From: wonfen Date: Tue, 17 Jun 2025 11:38:53 +0800 Subject: [PATCH] feat: enhance profile management and proxy refresh with improved event listening and state updates --- src-tauri/src/cmd/profile.rs | 103 +++++++++++++++------ src-tauri/src/cmd/proxy.rs | 22 +++++ src-tauri/src/core/handle.rs | 25 +++++- src-tauri/src/lib.rs | 1 + src/hooks/use-profiles.ts | 135 +++++++++++++++++++++------- src/pages/_layout.tsx | 43 +++++++-- src/pages/profiles.tsx | 106 +++++++++++++++------- src/providers/app-data-provider.tsx | 130 ++++++++++++++++++++++++++- src/services/cmds.ts | 6 ++ 9 files changed, 468 insertions(+), 103 deletions(-) diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index f1ecab6a3..56f729e74 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -12,45 +12,89 @@ use tokio::sync::Mutex; // 添加全局互斥锁防止并发配置更新 static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); -/// 获取配置文件列表 +/// 获取配置文件避免锁竞争 #[tauri::command] pub async fn get_profiles() -> CmdResult { - let profiles_result = tokio::time::timeout( - Duration::from_secs(3), // 3秒超时 - tokio::task::spawn_blocking(move || Config::profiles().data().clone()), + // 策略1: 尝试快速获取latest数据 + let latest_result = tokio::time::timeout( + 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; - match profiles_result { - Ok(Ok(profiles)) => Ok(*profiles), + match latest_result { + Ok(Ok(profiles)) => { + logging!(info, Type::Cmd, false, "快速获取配置列表成功"); + return Ok(profiles); + } Ok(Err(join_err)) => { - logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); - Ok(IProfiles { - current: None, - items: Some(vec![]), - }) + logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_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!( error, Type::Cmd, 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(_) => { - logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置"); - Ok(IProfiles { - current: None, - items: Some(vec![]), - }) - } - } + } + Err(_) => { + 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 { + current: None, + items: Some(vec![]), + }) } } } @@ -246,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { Config::profiles().apply(); 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 { if let Err(e) = Tray::global().update_tooltip() { log::warn!(target: "app", "异步更新托盘提示失败: {}", e); diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index c6b58a035..48be7c2ce 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -43,6 +43,28 @@ pub async fn get_proxies() -> CmdResult { Ok(*proxies) } +/// 强制刷新代理缓存用于profile切换 +#[tauri::command] +pub async fn force_refresh_proxies() -> CmdResult { + let manager = MihomoManager::global(); + let app_handle = handle::Handle::global().app_handle().unwrap(); + let cmd_proxy_state = app_handle.state::>(); + + 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] pub async fn get_providers_proxies() -> CmdResult { let app_handle = handle::Handle::global().app_handle().unwrap(); diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 0ab9faee8..a3c4a74e1 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -151,6 +151,10 @@ impl NotificationSystem { match window.emit(event_name_str, payload) { Ok(_) => { 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) => { log::warn!("Failed to emit event {}: {}", event_name_str, e); @@ -224,12 +228,27 @@ impl NotificationSystem { } fn shutdown(&mut self) { + log::info!("NotificationSystem shutdown initiated"); 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"); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 245ad23c7..5836cb696 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -255,6 +255,7 @@ pub fn run() { cmd::invoke_uwp_tool, cmd::copy_clash_env, cmd::get_proxies, + cmd::force_refresh_proxies, cmd::get_providers_proxies, cmd::save_dns_config, cmd::apply_dns_config, diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index d8f7f04a7..f94aceb94 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -10,11 +10,32 @@ export const useProfiles = () => { const { data: profiles, mutate: mutateProfiles } = useSWR( "getProfiles", getProfiles, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 2000, + errorRetryCount: 2, + errorRetryInterval: 1000, + }, ); const patchProfiles = async (value: Partial) => { - await patchProfilesConfig(value); - mutateProfiles(); + // 立即更新本地状态 + if (value.current && profiles) { + const optimisticUpdate = { + ...profiles, + current: value.current, + }; + mutateProfiles(optimisticUpdate, false); // 不重新验证 + } + + try { + await patchProfilesConfig(value); + mutateProfiles(); + } catch (error) { + mutateProfiles(); + throw error; + } }; const patchCurrent = async (value: Partial) => { @@ -26,40 +47,90 @@ export const useProfiles = () => { // 根据selected的节点选择 const activateSelected = async () => { - const proxiesData = await getProxies(); - const profileData = await getProfiles(); + try { + console.log("[ActivateSelected] 开始处理代理选择"); - if (!profileData || !proxiesData) return; + const [proxiesData, profileData] = await Promise.all([ + getProxies(), + getProfiles(), + ]); - const current = profileData.items?.find( - (e) => e && e.uid === profileData.current, - ); - - if (!current) return; - - // init selected array - const { selected = [] } = current; - const selectedMap = Object.fromEntries( - selected.map((each) => [each.name!, each.now!]), - ); - - let hasChange = false; - - const newSelected: typeof selected = []; - const { global, groups } = proxiesData; - - [global, ...groups].forEach(({ type, name, now }) => { - if (!now || type !== "Selector") return; - if (selectedMap[name] != null && selectedMap[name] !== now) { - hasChange = true; - updateProxy(name, selectedMap[name]); + if (!profileData || !proxiesData) { + console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理"); + return; } - newSelected.push({ name, now: selectedMap[name] }); - }); - if (hasChange) { - patchProfile(profileData.current!, { selected: newSelected }); - mutate("getProxies", getProxies()); + const current = profileData.items?.find( + (e) => e && e.uid === profileData.current, + ); + + if (!current) { + console.log("[ActivateSelected] 未找到当前profile配置"); + return; + } + + // 检查是否有saved的代理选择 + const { selected = [] } = current; + if (selected.length === 0) { + console.log("[ActivateSelected] 当前profile无保存的代理选择,跳过"); + return; + } + + console.log( + `[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`, + ); + + const selectedMap = Object.fromEntries( + selected.map((each) => [each.name!, each.now!]), + ); + + let hasChange = false; + const newSelected: typeof selected = []; + const { global, groups } = proxiesData; + + // 处理所有代理组 + [global, ...groups].forEach(({ type, name, now }) => { + if (!now || type !== "Selector") { + if (selectedMap[name] != null) { + newSelected.push({ name, now: 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) { + console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新"); + return; + } + + console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`); + + try { + await patchProfile(profileData.current!, { selected: newSelected }); + console.log("[ActivateSelected] 代理选择配置保存成功"); + + setTimeout(() => { + mutate("getProxies", getProxies()); + }, 100); + } catch (error: any) { + console.error( + "[ActivateSelected] 保存代理选择配置失败:", + error.message, + ); + } + } catch (error: any) { + console.error("[ActivateSelected] 处理代理选择失败:", error.message); } }; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index eca75fa18..d578f2355 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -169,7 +169,13 @@ const Layout = () => { const handleNotice = useCallback( (payload: [string, string]) => { const [status, msg] = payload; - handleNoticeMessage(status, msg, t, navigate); + setTimeout(() => { + try { + handleNoticeMessage(status, msg, t, navigate); + } catch (error) { + console.error("[Layout] 处理通知消息失败:", error); + } + }, 0); }, [t, navigate], ); @@ -220,12 +226,35 @@ const Layout = () => { const cleanupWindow = setupWindowListeners(); return () => { - listeners.forEach((listener) => { - if (typeof listener.then === "function") { - listener.then((unlisten) => unlisten()); - } - }); - cleanupWindow.then((cleanup) => cleanup()); + setTimeout(() => { + listeners.forEach((listener) => { + if (typeof listener.then === "function") { + listener + .then((unlisten) => { + try { + unlisten(); + } catch (error) { + console.error("[Layout] 清理事件监听器失败:", error); + } + }) + .catch((error) => { + console.error("[Layout] 获取unlisten函数失败:", error); + }); + } + }); + + cleanupWindow + .then((cleanup) => { + try { + cleanup(); + } catch (error) { + console.error("[Layout] 清理窗口监听器失败:", error); + } + }) + .catch((error) => { + console.error("[Layout] 获取cleanup函数失败:", error); + }); + }, 0); }; }, [handleNotice]); diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index f5a8501e8..6bbcd7d51 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -190,27 +190,53 @@ const ProfilePage = () => { } }; - const activateProfile = async (profile: string, notifySuccess: boolean) => { - // 避免大多数情况下loading态闪烁 - const reset = setTimeout(() => { - setActivatings((prev) => [...prev, profile]); - }, 100); - - try { - const success = await patchProfiles({ current: profile }); - await mutateLogs(); - closeAllConnections(); - await activateSelected(); - if (notifySuccess && success) { - showNotice("success", t("Profile Switched"), 1000); + const activateProfile = useLockFn( + async (profile: string, notifySuccess: boolean) => { + if (profiles.current === profile && !notifySuccess) { + console.log( + `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`, + ); + return; } - } catch (err: any) { - showNotice("error", err?.message || err.toString(), 4000); - } finally { - clearTimeout(reset); - setActivatings([]); - } - }; + + // 避免大多数情况下loading态闪烁 + const reset = setTimeout(() => { + setActivatings((prev) => [...prev, profile]); + }, 100); + + try { + console.log(`[Profile] 开始切换到: ${profile}`); + + const success = await patchProfiles({ current: profile }); + await mutateLogs(); + closeAllConnections(); + + if (notifySuccess && success) { + 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) { + console.warn("Failed to activate selected proxies:", err); + } + }, 50); + } catch (err: any) { + console.error(`[Profile] 切换失败:`, err); + showNotice("error", err?.message || err.toString(), 4000); + clearTimeout(reset); + setActivatings([]); + } + }, + ); const onSelect = useLockFn(async (current: string, force: boolean) => { if (!force && current === profiles.current) return; await activateProfile(current, true); @@ -300,31 +326,45 @@ const ProfilePage = () => { // 监听后端配置变更 useEffect(() => { let unlistenPromise: Promise<() => void> | undefined; - let timeoutId: ReturnType | undefined; + let lastProfileId: string | null = null; + let lastUpdateTime = 0; + const debounceDelay = 200; const setupListener = async () => { unlistenPromise = listen("profile-changed", (event) => { - console.log("Profile changed event received:", event.payload); - if (timeoutId) { - clearTimeout(timeoutId); + const newProfileId = event.payload; + const now = Date.now(); + + console.log(`[Profile] 收到配置变更事件: ${newProfileId}`); + + if ( + lastProfileId === newProfileId && + now - lastUpdateTime < debounceDelay + ) { + console.log(`[Profile] 重复事件被防抖,跳过`); + return; } - timeoutId = setTimeout(() => { - mutateProfiles(); - timeoutId = undefined; - }, 300); + lastProfileId = newProfileId; + lastUpdateTime = now; + + console.log(`[Profile] 执行配置数据刷新`); + + // 使用异步调度避免阻塞事件处理 + setTimeout(() => { + mutateProfiles().catch((error) => { + console.error("[Profile] 配置数据刷新失败:", error); + }); + }, 0); }); }; setupListener(); return () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - unlistenPromise?.then((unlisten) => unlisten()); + unlistenPromise?.then((unlisten) => unlisten()).catch(console.error); }; - }, [mutateProfiles, t]); + }, [mutateProfiles]); return ( { + let profileUnlisten: Promise<() => void> | undefined; + let lastProfileId: string | null = null; + let lastUpdateTime = 0; + const refreshThrottle = 500; + + const setupEventListeners = async () => { + try { + // 监听profile切换事件 + profileUnlisten = listen("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( "getClashConfig", getClashConfig, diff --git a/src/services/cmds.ts b/src/services/cmds.ts index c847c464c..5f2cbc877 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -220,6 +220,12 @@ export async function cmdGetProxyDelay( } } +/// 用于profile切换等场景 +export async function forceRefreshProxies() { + console.log("[API] 强制刷新代理缓存"); + return invoke("force_refresh_proxies"); +} + export async function cmdTestDelay(url: string) { return invoke("test_delay", { url }); }