diff --git a/UPDATELOG.md b/UPDATELOG.md index 071d8ac1..9cd24f9d 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -21,6 +21,7 @@ - 修复 Linux WebKit 网络进程的崩溃 - 修复无法导入订阅 - 修复实际导入成功但显示导入失败的问题 +- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 - 修复删除订阅时未能实际删除相关文件 - 修复 macOS 连接界面显示异常 - 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 @@ -78,6 +79,7 @@ - 允许在 `界面设置` 修改 `悬浮跳转导航延迟` - 添加热键绑定错误的提示信息 - 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 +- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index dfbfed7c..4318a3e2 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -1,9 +1,10 @@ use super::{IClashTemp, IProfiles, IRuntime, IVerge}; use crate::{ + cmd, config::{PrfItem, profiles_append_item_safe}, constants::{files, timing}, - core::{CoreManager, handle, validate::CoreConfigValidator}, - enhance, logging, + core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, + enhance, logging, logging_error, utils::{Draft, dirs, help, logging::Type}, }; use anyhow::{Result, anyhow}; @@ -55,6 +56,20 @@ impl Config { pub async fn init_config() -> Result<()> { Self::ensure_default_profile_items().await?; + // init Tun mode + if !cmd::system::is_admin().unwrap_or_default() + && service::is_service_available().await.is_err() + { + let verge = Config::verge().await; + verge.draft_mut().enable_tun_mode = Some(false); + verge.apply(); + let _ = tray::Tray::global().update_tray_display().await; + + // 分离数据获取和异步调用避免Send问题 + let verge_data = Config::verge().await.latest_ref().clone(); + logging_error!(Type::Core, verge_data.save_file().await); + } + let validation_result = Self::generate_and_validate().await?; if let Some((msg_type, msg_content)) = validation_result { diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 1a397438..d9429c76 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -8,7 +8,6 @@ use anyhow::{Result, bail}; use parking_lot::Mutex; use smartstring::alias::String; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; -use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; /// Enum representing all available hotkey functions @@ -105,66 +104,53 @@ impl Hotkey { } /// Execute the function associated with a hotkey function enum - fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) { - let app_handle = app_handle.clone(); + fn execute_function(function: HotkeyFunction) { match function { HotkeyFunction::OpenOrCloseDashboard => { AsyncHandler::spawn(async move || { crate::feat::open_or_close_dashboard().await; - notify_event(app_handle, NotificationEvent::DashboardToggled).await; + notify_event(NotificationEvent::DashboardToggled).await; }); } HotkeyFunction::ClashModeRule => { AsyncHandler::spawn(async move || { feat::change_clash_mode("rule".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Rule" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await; }); } HotkeyFunction::ClashModeGlobal => { AsyncHandler::spawn(async move || { feat::change_clash_mode("global".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Global" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await; }); } HotkeyFunction::ClashModeDirect => { AsyncHandler::spawn(async move || { feat::change_clash_mode("direct".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Direct" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await; }); } HotkeyFunction::ToggleSystemProxy => { AsyncHandler::spawn(async move || { feat::toggle_system_proxy().await; - notify_event(app_handle, NotificationEvent::SystemProxyToggled).await; + notify_event(NotificationEvent::SystemProxyToggled).await; }); } HotkeyFunction::ToggleTunMode => { AsyncHandler::spawn(async move || { feat::toggle_tun_mode(None).await; - notify_event(app_handle, NotificationEvent::TunModeToggled).await; + notify_event(NotificationEvent::TunModeToggled).await; }); } HotkeyFunction::EntryLightweightMode => { AsyncHandler::spawn(async move || { entry_lightweight_mode().await; - notify_event(app_handle, NotificationEvent::LightweightModeEntered).await; + notify_event(NotificationEvent::LightweightModeEntered).await; }); } HotkeyFunction::Quit => { AsyncHandler::spawn(async move || { - notify_event(app_handle, NotificationEvent::AppQuit).await; + notify_event(NotificationEvent::AppQuit).await; feat::quit().await; }); } @@ -172,7 +158,7 @@ impl Hotkey { HotkeyFunction::Hide => { AsyncHandler::spawn(async move || { feat::hide().await; - notify_event(app_handle, NotificationEvent::AppHidden).await; + notify_event(NotificationEvent::AppHidden).await; }); } } @@ -224,14 +210,12 @@ impl Hotkey { let is_quit = matches!(function, HotkeyFunction::Quit); - manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| { + manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| { let hotkey_event_owned = *hotkey_event; let event_owned = event; let function_owned = function; let is_quit_owned = is_quit; - let app_handle_cloned = app_handle.clone(); - AsyncHandler::spawn(move || async move { if event_owned.state == ShortcutState::Pressed { logging!( @@ -242,11 +226,11 @@ impl Hotkey { ); if hotkey_event_owned.key == Code::KeyQ && is_quit_owned { - if let Some(window) = app_handle_cloned.get_webview_window("main") + if let Some(window) = handle::Handle::get_window() && window.is_focused().unwrap_or(false) { logging!(debug, Type::Hotkey, "Executing quit function"); - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } } else { logging!(debug, Type::Hotkey, "Executing function directly"); @@ -258,14 +242,14 @@ impl Hotkey { .unwrap_or(true); if is_enable_global_hotkey { - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } else { use crate::utils::window_manager::WindowManager; let is_visible = WindowManager::is_main_window_visible(); let is_focused = WindowManager::is_main_window_focused(); if is_focused && is_visible { - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } } } diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 1361a593..04ded18a 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1,5 +1,6 @@ use crate::{ config::Config, + core::tray, logging, logging_error, utils::{dirs, init::service_writer_config, logging::Type}, }; @@ -531,6 +532,7 @@ impl ServiceManager { return Err(anyhow::anyhow!("服务不可用: {}", reason)); } } + let _ = tray::Tray::global().update_tray_display().await; Ok(()) } } diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index a2b15ff6..ed9f21e8 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -5,6 +5,7 @@ use tauri_plugin_mihomo::models::Proxies; #[cfg(target_os = "macos")] pub mod speed_rate; use crate::config::PrfSelected; +use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; @@ -297,6 +298,9 @@ impl Tray { let verge = Config::verge().await.latest_ref().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); + let tun_mode_available = cmd::system::is_admin().unwrap_or_default() + || service::is_service_available().await.is_ok(); + println!("tun_mode_available: {}", tun_mode_available); let mode = { Config::clash() .await @@ -322,6 +326,7 @@ impl Tray { Some(mode.as_str()), *system_proxy, *tun_mode, + tun_mode_available, profile_uid_and_name, is_lightweight_mode, ) @@ -837,6 +842,7 @@ async fn create_tray_menu( mode: Option<&str>, system_proxy_enabled: bool, tun_mode_enabled: bool, + tun_mode_available: bool, profile_uid_and_name: Vec<(String, String)>, is_lightweight_mode: bool, ) -> Result> { @@ -980,7 +986,7 @@ async fn create_tray_menu( app_handle, MenuIds::TUN_MODE, &texts.tun_mode, - true, + tun_mode_available, tun_mode_enabled, hotkeys.get("toggle_tun_mode").map(|s| s.as_str()), )?; diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/utils/notification.rs index 0b0b7199..f737560e 100644 --- a/src-tauri/src/utils/notification.rs +++ b/src-tauri/src/utils/notification.rs @@ -1,6 +1,5 @@ -use crate::utils::i18n::t; +use crate::{core::handle, utils::i18n::t}; -use tauri::AppHandle; use tauri_plugin_notification::NotificationExt; pub enum NotificationEvent<'a> { @@ -16,8 +15,10 @@ pub enum NotificationEvent<'a> { AppHidden, } -fn notify(app: &AppHandle, title: &str, body: &str) { - app.notification() +fn notify(title: &str, body: &str) { + let app_handle = handle::Handle::app_handle(); + app_handle + .notification() .builder() .title(title) .body(body) @@ -25,49 +26,44 @@ fn notify(app: &AppHandle, title: &str, body: &str) { .ok(); } -pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) { +pub async fn notify_event<'a>(event: NotificationEvent<'a>) { match event { NotificationEvent::DashboardToggled => { notify( - &app, &t("DashboardToggledTitle").await, &t("DashboardToggledBody").await, ); } NotificationEvent::ClashModeChanged { mode } => { notify( - &app, &t("ClashModeChangedTitle").await, &t_with_args("ClashModeChangedBody", mode).await, ); } NotificationEvent::SystemProxyToggled => { notify( - &app, &t("SystemProxyToggledTitle").await, &t("SystemProxyToggledBody").await, ); } NotificationEvent::TunModeToggled => { notify( - &app, &t("TunModeToggledTitle").await, &t("TunModeToggledBody").await, ); } NotificationEvent::LightweightModeEntered => { notify( - &app, &t("LightweightModeEnteredTitle").await, &t("LightweightModeEnteredBody").await, ); } NotificationEvent::AppQuit => { - notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await); + notify(&t("AppQuitTitle").await, &t("AppQuitBody").await); } #[cfg(target_os = "macos")] NotificationEvent::AppHidden => { - notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await); + notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await); } } } diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index ccf3ec7b..5d9304bf 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -117,13 +117,8 @@ const ProxyControlSwitches = ({ const { uninstallServiceAndRestartCore } = useServiceUninstaller(); const { actualState: systemProxyActualState, toggleSystemProxy } = useSystemProxyState(); - const { - isServiceMode, - isTunModeAvailable, - mutateRunningMode, - mutateServiceOk, - mutateTunModeAvailable, - } = useSystemState(); + const { isServiceOk, isTunModeAvailable, mutateSystemState } = + useSystemState(); const sysproxyRef = useRef(null); const tunRef = useRef(null); @@ -148,9 +143,7 @@ const ProxyControlSwitches = ({ const onInstallService = useLockFn(async () => { try { await installServiceAndRestartCore(); - await mutateRunningMode(); - await mutateServiceOk(); - await mutateTunModeAvailable(); + await mutateSystemState(); } catch (err) { showNotice("error", (err as Error).message || String(err)); } @@ -158,11 +151,11 @@ const ProxyControlSwitches = ({ const onUninstallService = useLockFn(async () => { try { - await handleTunToggle(false); + if (verge?.enable_tun_mode) { + await handleTunToggle(false); + } await uninstallServiceAndRestartCore(); - await mutateRunningMode(); - await mutateServiceOk(); - await mutateTunModeAvailable(); + await mutateSystemState(); } catch (err) { showNotice("error", (err as Error).message || String(err)); } @@ -198,22 +191,22 @@ const ProxyControlSwitches = ({ extraIcons={ <> {!isTunModeAvailable && ( - + <> + + + )} - {!isTunModeAvailable && ( - - )} - {isServiceMode && ( + {isServiceOk && ( { + const [runningMode, isAdminMode, isServiceOk] = await Promise.all([ + getRunningMode(), + isAdmin(), + isServiceAvailable(), + ]); + return { runningMode, isAdminMode, isServiceOk } as SystemState; + }, { - suspense: false, - revalidateOnFocus: false, + suspense: true, + refreshInterval: 30000, + fallback: defaultSystemState, }, ); - const { - data: isServiceOk = false, - mutate: mutateServiceOk, - isLoading: isServiceLoading, - } = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, { - suspense: false, - revalidateOnFocus: false, - onSuccess: (data) => { - console.log("[useSystemState] 服务状态更新:", data); - }, - onError: (error) => { - console.error("[useSystemState] 服务状态检查失败:", error); - }, - // isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求 - }); + const isSidecarMode = systemState.runningMode === "Sidecar"; + const isServiceMode = systemState.runningMode === "Service"; + const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk; - const isLoading = - runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading); + const enable_tun_mode = verge?.enable_tun_mode; + useEffect(() => { + if (enable_tun_mode === undefined) return; - const { data: isTunModeAvailable = false, mutate: mutateTunModeAvailable } = - useSWR( - ["isTunModeAvailable", isAdminMode, isServiceOk], - () => isAdminMode || isServiceOk, - { - suspense: false, - revalidateOnFocus: false, - }, - ); + if ( + !disablingTunMode && + enable_tun_mode && + !isTunModeAvailable && + !isLoading + ) { + disablingTunMode = true; + patchVerge({ enable_tun_mode: false }) + .then(() => { + showNotice( + "info", + t("TUN Mode automatically disabled due to service unavailable"), + ); + }) + .catch((err) => { + console.error("[useVerge] 自动关闭TUN模式失败:", err); + showNotice("error", t("Failed to disable TUN Mode automatically")); + }) + .finally(() => { + const tid = setTimeout(() => { + // 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式 + disablingTunMode = false; + clearTimeout(tid); + }, 1000); + }); + } + }, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, t]); return { - runningMode, - isAdminMode, + runningMode: systemState.runningMode, + isAdminMode: systemState.isAdminMode, + isServiceOk: systemState.isServiceOk, isSidecarMode, isServiceMode, - isServiceOk, isTunModeAvailable, - mutateRunningMode, - mutateServiceOk, - mutateTunModeAvailable, + mutateSystemState, isLoading, }; } diff --git a/src/hooks/use-verge.ts b/src/hooks/use-verge.ts index 1e7d7afc..f6415ff8 100644 --- a/src/hooks/use-verge.ts +++ b/src/hooks/use-verge.ts @@ -1,16 +1,8 @@ -import { useCallback, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; import useSWR from "swr"; -import { useSystemState } from "@/hooks/use-system-state"; import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; export const useVerge = () => { - const { t } = useTranslation(); - const { isTunModeAvailable, isServiceMode, isLoading } = useSystemState(); - const disablingRef = useRef(false); - const { data: verge, mutate: mutateVerge } = useSWR( "getVergeConfig", async () => { @@ -24,53 +16,6 @@ export const useVerge = () => { mutateVerge(); }; - const { enable_tun_mode } = verge ?? {}; - - const mutateVergeRef = useRef(mutateVerge); - const tRef = useRef(t); - const enableTunRef = useRef(enable_tun_mode); - const isLoadingRef = useRef(isLoading); - const isServiceModeRef = useRef(isServiceMode); - - mutateVergeRef.current = mutateVerge; - tRef.current = t; - enableTunRef.current = enable_tun_mode; - isLoadingRef.current = isLoading; - isServiceModeRef.current = isServiceMode; - - const doDisable = useCallback(async () => { - try { - if (isServiceModeRef.current === true) return; - await patchVergeConfig({ enable_tun_mode: false }); - await mutateVergeRef.current?.(); - showNotice( - "info", - tRef.current( - "TUN Mode automatically disabled due to service unavailable", - ), - ); - } catch (err) { - console.error("[useVerge] 自动关闭TUN模式失败:", err); - showNotice( - "error", - tRef.current("Failed to disable TUN Mode automatically"), - ); - } finally { - disablingRef.current = false; - } - }, []); - - useEffect(() => { - if (isTunModeAvailable === true) return; - if (isLoadingRef.current === true) return; - if (enableTunRef.current !== true) return; - if (isServiceModeRef.current === true) return; - if (disablingRef.current) return; - - disablingRef.current = true; - void doDisable(); - }, [isTunModeAvailable, doDisable]); - return { verge, mutateVerge, diff --git a/src/hooks/useServiceInstaller.ts b/src/hooks/useServiceInstaller.ts index b074c678..2f216876 100644 --- a/src/hooks/useServiceInstaller.ts +++ b/src/hooks/useServiceInstaller.ts @@ -25,7 +25,7 @@ const executeWithErrorHandling = async ( }; export const useServiceInstaller = () => { - const { mutateRunningMode, mutateServiceOk } = useSystemState(); + const { mutateSystemState } = useSystemState(); const installServiceAndRestartCore = useCallback(async () => { await executeWithErrorHandling( @@ -34,9 +34,13 @@ export const useServiceInstaller = () => { "Service Installed Successfully", ); - await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); - await mutateRunningMode(); - await mutateServiceOk(); - }, [mutateRunningMode, mutateServiceOk]); + await executeWithErrorHandling( + () => restartCore(), + "Restarting Core...", + "Clash Core Restarted", + ); + + await mutateSystemState(); + }, [mutateSystemState]); return { installServiceAndRestartCore }; }; diff --git a/src/hooks/useServiceUninstaller.ts b/src/hooks/useServiceUninstaller.ts index bb2c450d..fbcd76a5 100644 --- a/src/hooks/useServiceUninstaller.ts +++ b/src/hooks/useServiceUninstaller.ts @@ -25,21 +25,26 @@ const executeWithErrorHandling = async ( }; export const useServiceUninstaller = () => { - const { mutateRunningMode, mutateServiceOk } = useSystemState(); + const { mutateSystemState } = useSystemState(); const uninstallServiceAndRestartCore = useCallback(async () => { - await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); - - await executeWithErrorHandling( - () => uninstallService(), - "Uninstalling Service...", - "Service Uninstalled Successfully", - ); - - await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); - await mutateRunningMode(); - await mutateServiceOk(); - }, [mutateRunningMode, mutateServiceOk]); + try { + await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); + await executeWithErrorHandling( + () => uninstallService(), + "Uninstalling Service...", + "Service Uninstalled Successfully", + ); + } catch (ignore) { + } finally { + await executeWithErrorHandling( + () => restartCore(), + "Restarting Core...", + "Clash Core Restarted", + ); + await mutateSystemState(); + } + }, [mutateSystemState]); return { uninstallServiceAndRestartCore }; };