diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index be5f2c493..02f4ed269 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -5,7 +5,7 @@ use crate::{ service::{self}, }, ipc::IpcManager, - logging, logging_error, + logging, logging_error, singleton_lazy, utils::{ dirs, help::{self}, @@ -14,7 +14,6 @@ use crate::{ }; use anyhow::Result; use chrono::Local; -use once_cell::sync::OnceCell; use std::{ fmt, fs::{create_dir_all, File}, @@ -823,14 +822,19 @@ impl CoreManager { } } -impl CoreManager { - pub fn global() -> &'static CoreManager { - static CORE_MANAGER: OnceCell = OnceCell::new(); - CORE_MANAGER.get_or_init(|| CoreManager { +impl Default for CoreManager { + fn default() -> Self { + CoreManager { running: Arc::new(Mutex::new(RunningMode::NotRunning)), child_sidecar: Arc::new(Mutex::new(None)), - }) + } } +} + +// Use simplified singleton_lazy macro +singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default); + +impl CoreManager { // 当服务安装失败时的回退逻辑 async fn attempt_service_init(&self) -> Result<()> { if service::check_service_needs_reinstall().await { diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index d7e151876..5ded18859 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,4 +1,4 @@ -use once_cell::sync::OnceCell; +use crate::singleton; use parking_lot::RwLock; use std::{ sync::{ @@ -272,10 +272,12 @@ impl Default for Handle { } } +// Use singleton macro +singleton!(Handle, HANDLE); + impl Handle { - pub fn global() -> &'static Handle { - static HANDLE: OnceCell = OnceCell::new(); - HANDLE.get_or_init(Handle::default) + pub fn new() -> Self { + Self::default() } pub fn init(&self, app_handle: &AppHandle) { diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index fae8240d7..e79cc2dd4 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,28 +1,273 @@ use crate::utils::notification::{notify_event, NotificationEvent}; use crate::{ config::Config, core::handle, feat, logging, logging_error, - module::lightweight::entry_lightweight_mode, utils::logging::Type, + module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type, }; use anyhow::{bail, Result}; -use once_cell::sync::OnceCell; use parking_lot::Mutex; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; use tauri::Manager; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; +/// Enum representing all available hotkey functions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HotkeyFunction { + OpenOrCloseDashboard, + ClashModeRule, + ClashModeGlobal, + ClashModeDirect, + ToggleSystemProxy, + ToggleTunMode, + EntryLightweightMode, + Quit, + #[cfg(target_os = "macos")] + Hide, +} + +impl fmt::Display for HotkeyFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard", + HotkeyFunction::ClashModeRule => "clash_mode_rule", + HotkeyFunction::ClashModeGlobal => "clash_mode_global", + HotkeyFunction::ClashModeDirect => "clash_mode_direct", + HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy", + HotkeyFunction::ToggleTunMode => "toggle_tun_mode", + HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode", + HotkeyFunction::Quit => "quit", + #[cfg(target_os = "macos")] + HotkeyFunction::Hide => "hide", + }; + write!(f, "{s}") + } +} + +impl FromStr for HotkeyFunction { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.trim() { + "open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard), + "clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule), + "clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal), + "clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect), + "toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy), + "toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode), + "entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode), + "quit" => Ok(HotkeyFunction::Quit), + #[cfg(target_os = "macos")] + "hide" => Ok(HotkeyFunction::Hide), + _ => bail!("invalid hotkey function: {}", s), + } + } +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Enum representing predefined system hotkeys +pub enum SystemHotkey { + CmdQ, + CmdW, +} + +#[cfg(target_os = "macos")] +impl fmt::Display for SystemHotkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SystemHotkey::CmdQ => "CMD+Q", + SystemHotkey::CmdW => "CMD+W", + }; + write!(f, "{s}") + } +} + +#[cfg(target_os = "macos")] +impl SystemHotkey { + pub fn function(self) -> HotkeyFunction { + match self { + SystemHotkey::CmdQ => HotkeyFunction::Quit, + SystemHotkey::CmdW => HotkeyFunction::Hide, + } + } +} + pub struct Hotkey { current: Arc>>, } impl Hotkey { - pub fn global() -> &'static Hotkey { - static HOTKEY: OnceCell = OnceCell::new(); - - HOTKEY.get_or_init(|| Hotkey { + fn new() -> Self { + Self { current: Arc::new(Mutex::new(Vec::new())), - }) + } } + /// Execute the function associated with a hotkey function enum + fn execute_function(function: HotkeyFunction, app_handle: &tauri::AppHandle) { + match function { + HotkeyFunction::OpenOrCloseDashboard => { + logging!( + debug, + Type::Hotkey, + true, + "=== Hotkey Dashboard Window Operation Start ===" + ); + + logging!( + info, + Type::Hotkey, + true, + "Using unified WindowManager for hotkey operation (bypass debounce)" + ); + + crate::feat::open_or_close_dashboard_hotkey(); + + logging!( + debug, + Type::Hotkey, + "=== Hotkey Dashboard Window Operation End ===" + ); + notify_event(app_handle, NotificationEvent::DashboardToggled); + } + HotkeyFunction::ClashModeRule => { + feat::change_clash_mode("rule".into()); + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Rule" }, + ); + } + HotkeyFunction::ClashModeGlobal => { + feat::change_clash_mode("global".into()); + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Global" }, + ); + } + HotkeyFunction::ClashModeDirect => { + feat::change_clash_mode("direct".into()); + notify_event( + app_handle, + NotificationEvent::ClashModeChanged { mode: "Direct" }, + ); + } + HotkeyFunction::ToggleSystemProxy => { + feat::toggle_system_proxy(); + notify_event(app_handle, NotificationEvent::SystemProxyToggled); + } + HotkeyFunction::ToggleTunMode => { + feat::toggle_tun_mode(None); + notify_event(app_handle, NotificationEvent::TunModeToggled); + } + HotkeyFunction::EntryLightweightMode => { + entry_lightweight_mode(); + notify_event(app_handle, NotificationEvent::LightweightModeEntered); + } + HotkeyFunction::Quit => { + feat::quit(); + notify_event(app_handle, NotificationEvent::AppQuit); + } + #[cfg(target_os = "macos")] + HotkeyFunction::Hide => { + feat::hide(); + notify_event(app_handle, NotificationEvent::AppHidden); + } + } + } + + #[cfg(target_os = "macos")] + /// Register a system hotkey using enum + pub fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { + let hotkey_str = hotkey.to_string(); + let function = hotkey.function(); + self.register_hotkey_with_function(&hotkey_str, function) + } + + #[cfg(target_os = "macos")] + /// Unregister a system hotkey using enum + pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { + let hotkey_str = hotkey.to_string(); + self.unregister(&hotkey_str) + } + + /// Register a hotkey with function enum + pub fn register_hotkey_with_function( + &self, + hotkey: &str, + function: HotkeyFunction, + ) -> Result<()> { + let app_handle = handle::Handle::global().app_handle().unwrap(); + let manager = app_handle.global_shortcut(); + + logging!( + debug, + Type::Hotkey, + "Attempting to register hotkey: {} for function: {}", + hotkey, + function + ); + + if manager.is_registered(hotkey) { + logging!( + debug, + Type::Hotkey, + "Hotkey {} was already registered, unregistering first", + hotkey + ); + manager.unregister(hotkey)?; + } + + let app_handle_clone = app_handle.clone(); + let is_quit = matches!(function, HotkeyFunction::Quit); + + let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| { + if event.state == ShortcutState::Pressed { + logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event); + + if hotkey_event.key == Code::KeyQ && is_quit { + if let Some(window) = app_handle.get_webview_window("main") { + if window.is_focused().unwrap_or(false) { + logging!(debug, Type::Hotkey, "Executing quit function"); + Self::execute_function(function, &app_handle_clone); + } + } + } else { + logging!(debug, Type::Hotkey, "Executing function directly"); + + let is_enable_global_hotkey = Config::verge() + .latest_ref() + .enable_global_hotkey + .unwrap_or(true); + + if is_enable_global_hotkey { + Self::execute_function(function, &app_handle_clone); + } 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, &app_handle_clone); + } + } + } + } + }); + + logging!( + debug, + Type::Hotkey, + "Successfully registered hotkey {} for {}", + hotkey, + function + ); + Ok(()) + } +} + +// Use unified singleton macro +singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); + +impl Hotkey { pub fn init(&self) -> Result<()> { let verge = Config::verge(); let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true); @@ -112,173 +357,10 @@ impl Hotkey { Ok(()) } + /// Register a hotkey with string-based function (backward compatibility) pub fn register(&self, hotkey: &str, func: &str) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let manager = app_handle.global_shortcut(); - - logging!( - debug, - Type::Hotkey, - "Attempting to register hotkey: {} for function: {}", - hotkey, - func - ); - - if manager.is_registered(hotkey) { - logging!( - debug, - Type::Hotkey, - "Hotkey {} was already registered, unregistering first", - hotkey - ); - manager.unregister(hotkey)?; - } - - let app_handle_clone = app_handle.clone(); - let f: Box = match func.trim() { - "open_or_close_dashboard" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - logging!( - debug, - Type::Hotkey, - true, - "=== Hotkey Dashboard Window Operation Start ===" - ); - - logging!( - info, - Type::Hotkey, - true, - "Using unified WindowManager for hotkey operation (bypass debounce)" - ); - - crate::feat::open_or_close_dashboard_hotkey(); - - logging!( - debug, - Type::Hotkey, - "=== Hotkey Dashboard Window Operation End ===" - ); - notify_event(&app_handle, NotificationEvent::DashboardToggled); - }) - } - "clash_mode_rule" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("rule".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Rule" }, - ); - }) - } - "clash_mode_global" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("global".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Global" }, - ); - }) - } - "clash_mode_direct" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::change_clash_mode("direct".into()); - notify_event( - &app_handle, - NotificationEvent::ClashModeChanged { mode: "Direct" }, - ); - }) - } - "toggle_system_proxy" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::toggle_system_proxy(); - notify_event(&app_handle, NotificationEvent::SystemProxyToggled); - }) - } - "toggle_tun_mode" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::toggle_tun_mode(None); - notify_event(&app_handle, NotificationEvent::TunModeToggled); - }) - } - "entry_lightweight_mode" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - entry_lightweight_mode(); - notify_event(&app_handle, NotificationEvent::LightweightModeEntered); - }) - } - "quit" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::quit(); - notify_event(&app_handle, NotificationEvent::AppQuit); - }) - } - #[cfg(target_os = "macos")] - "hide" => { - let app_handle = app_handle_clone.clone(); - Box::new(move || { - feat::hide(); - notify_event(&app_handle, NotificationEvent::AppHidden); - }) - } - _ => { - logging!(error, Type::Hotkey, "Invalid function: {}", func); - bail!("invalid function \"{func}\""); - } - }; - - let is_quit = func.trim() == "quit"; - - let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| { - if event.state == ShortcutState::Pressed { - logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey); - - if hotkey.key == Code::KeyQ && is_quit { - if let Some(window) = app_handle.get_webview_window("main") { - if window.is_focused().unwrap_or(false) { - logging!(debug, Type::Hotkey, "Executing quit function"); - f(); - } - } - } else { - logging!(debug, Type::Hotkey, "Executing function directly"); - - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - - if is_enable_global_hotkey { - f(); - } 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 { - f(); - } - } - } - } - }); - - logging!( - debug, - Type::Hotkey, - "Successfully registered hotkey {} for {}", - hotkey, - func - ); - Ok(()) + let function = HotkeyFunction::from_str(func)?; + self.register_hotkey_with_function(hotkey, function) } pub fn unregister(&self, hotkey: &str) -> Result<()> { diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 758331fb6..5dec7b6ff 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -3,11 +3,10 @@ use crate::utils::autostart as startup_shortcut; use crate::{ config::{Config, IVerge}, core::{handle::Handle, EventDrivenProxyManager}, - logging, logging_error, + logging, logging_error, singleton_lazy, utils::logging::Type, }; use anyhow::Result; -use once_cell::sync::OnceCell; use std::sync::Arc; #[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; @@ -52,15 +51,19 @@ fn get_bypass() -> String { } } -impl Sysopt { - pub fn global() -> &'static Sysopt { - static SYSOPT: OnceCell = OnceCell::new(); - SYSOPT.get_or_init(|| Sysopt { +impl Default for Sysopt { + fn default() -> Self { + Sysopt { update_sysproxy: Arc::new(TokioMutex::new(false)), reset_sysproxy: Arc::new(TokioMutex::new(false)), - }) + } } +} +// Use simplified singleton_lazy macro +singleton_lazy!(Sysopt, SYSOPT, Sysopt::default); + +impl Sysopt { pub fn init_guard_sysproxy(&self) -> Result<()> { // 使用事件驱动代理管理器 let proxy_manager = EventDrivenProxyManager::global(); diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index e7b1c2f78..2cddf5796 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -8,6 +8,7 @@ use crate::{ config::Config, feat, logging, module::lightweight::is_in_lightweight_mode, + singleton_lazy, utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, Type, }; @@ -168,23 +169,19 @@ impl TrayState { } } -impl Tray { - pub fn global() -> &'static Tray { - static TRAY: OnceCell = OnceCell::new(); - - #[cfg(target_os = "macos")] - return TRAY.get_or_init(|| Tray { +impl Default for Tray { + fn default() -> Self { + Tray { last_menu_update: Mutex::new(None), menu_updating: AtomicBool::new(false), - }); - - #[cfg(not(target_os = "macos"))] - return TRAY.get_or_init(|| Tray { - last_menu_update: Mutex::new(None), - menu_updating: AtomicBool::new(false), - }); + } } +} +// Use simplified singleton_lazy macro +singleton_lazy!(Tray, TRAY, Tray::default); + +impl Tray { pub fn init(&self) -> Result<()> { Ok(()) } diff --git a/src-tauri/src/ipc/general.rs b/src-tauri/src/ipc/general.rs index 5b1312c0e..19ee27ea3 100644 --- a/src-tauri/src/ipc/general.rs +++ b/src-tauri/src/ipc/general.rs @@ -3,7 +3,6 @@ use kode_bridge::{ IpcHttpClient, LegacyResponse, }; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; -use std::sync::OnceLock; // 定义用于URL路径的编码集合,只编码真正必要的字符 const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS @@ -14,10 +13,7 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS .add(b'&') // 和号 .add(b'%'); // 百分号 -use crate::{ - logging, - utils::{dirs::ipc_path, logging::Type}, -}; +use crate::{logging, singleton_with_logging, utils::dirs::ipc_path}; // Helper function to create AnyError from string fn create_error(msg: impl Into) -> AnyError { @@ -28,28 +24,19 @@ pub struct IpcManager { ipc_path: String, } -static INSTANCE: OnceLock = OnceLock::new(); - impl IpcManager { - pub fn global() -> &'static IpcManager { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let instance = IpcManager { - ipc_path: ipc_path.to_string(), - }; - logging!( - info, - Type::Ipc, - true, - "IpcManager initialized with IPC path: {}", - instance.ipc_path - ); - instance - }) + fn new() -> Self { + let ipc_path_buf = ipc_path().unwrap(); + let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); + Self { + ipc_path: ipc_path.to_string(), + } } } +// Use singleton macro with logging +singleton_with_logging!(IpcManager, INSTANCE, "IpcManager"); + impl IpcManager { pub async fn request( &self, diff --git a/src-tauri/src/ipc/logs.rs b/src-tauri/src/ipc/logs.rs index a5cedc2d6..37f262832 100644 --- a/src-tauri/src/ipc/logs.rs +++ b/src-tauri/src/ipc/logs.rs @@ -1,14 +1,10 @@ -use kode_bridge::IpcStreamClient; use serde::{Deserialize, Serialize}; -use std::{ - collections::VecDeque, - sync::{Arc, OnceLock}, - time::Instant, -}; +use std::{collections::VecDeque, sync::Arc, time::Instant}; use tokio::{sync::RwLock, task::JoinHandle, time::Duration}; use crate::{ - logging, + ipc::monitor::MonitorData, + logging, singleton_with_logging, utils::{dirs::ipc_path, logging::Type}, }; @@ -66,6 +62,16 @@ impl Default for CurrentLogs { } } +impl MonitorData for CurrentLogs { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } +} + // Logs monitor with streaming support pub struct LogsMonitor { current: Arc>, @@ -73,17 +79,10 @@ pub struct LogsMonitor { current_monitoring_level: Arc>>, } -static INSTANCE: OnceLock = OnceLock::new(); +// Use singleton_with_logging macro +singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor"); impl LogsMonitor { - pub fn global() -> &'static LogsMonitor { - INSTANCE.get_or_init(|| { - let instance = LogsMonitor::new(); - logging!(info, Type::Ipc, true, "LogsMonitor initialized"); - instance - }) - } - fn new() -> Self { let current = Arc::new(RwLock::new(CurrentLogs::default())); @@ -135,9 +134,6 @@ impl LogsMonitor { } let monitor_current = self.current.clone(); - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let client = IpcStreamClient::new(ipc_path).unwrap(); // Update current level in data structure { @@ -147,6 +143,16 @@ impl LogsMonitor { let task = tokio::spawn(async move { loop { + // Get fresh IPC path and client for each connection attempt + let (_ipc_path_buf, client) = match Self::create_ipc_client().await { + Ok((path, client)) => (path, client), + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + }; + let url = if filter_level == "info" { "/logs".to_string() } else { @@ -170,35 +176,7 @@ impl LogsMonitor { .get(&url) .timeout(Duration::from_secs(30)) .process_lines(|line| { - if let Ok(log_data) = serde_json::from_str::(line.trim()) { - // Filter logs based on level if needed - let should_include = match filter_level.as_str() { - "all" => true, - level => log_data.log_type.to_lowercase() == level.to_lowercase(), - }; - - if should_include { - let log_item = LogItem::new(log_data.log_type, log_data.payload); - - tokio::spawn({ - let current = monitor_current.clone(); - async move { - let mut logs = current.write().await; - - // Add new log - logs.logs.push_back(log_item); - - // Keep only the last 1000 logs - if logs.logs.len() > 1000 { - logs.logs.pop_front(); - } - - logs.last_updated = Instant::now(); - } - }); - } - } - Ok(()) + Self::process_log_line(line, &filter_level, monitor_current.clone()) }) .await; @@ -222,6 +200,51 @@ impl LogsMonitor { ); } + async fn create_ipc_client() -> Result< + (std::path::PathBuf, kode_bridge::IpcStreamClient), + Box, + > { + use kode_bridge::IpcStreamClient; + + let ipc_path_buf = ipc_path()?; + let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?; + let client = IpcStreamClient::new(ipc_path)?; + Ok((ipc_path_buf, client)) + } + + fn process_log_line( + line: &str, + filter_level: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(log_data) = serde_json::from_str::(line.trim()) { + // Filter logs based on level if needed + let should_include = match filter_level { + "all" => true, + level => log_data.log_type.to_lowercase() == level.to_lowercase(), + }; + + if should_include { + let log_item = LogItem::new(log_data.log_type, log_data.payload); + + tokio::spawn(async move { + let mut logs = current.write().await; + + // Add new log + logs.logs.push_back(log_item); + + // Keep only the last 1000 logs + if logs.logs.len() > 1000 { + logs.logs.pop_front(); + } + + logs.mark_fresh(); + }); + } + } + Ok(()) + } + pub async fn current(&self) -> CurrentLogs { self.current.read().await.clone() } @@ -229,7 +252,7 @@ impl LogsMonitor { pub async fn clear_logs(&self) { let mut current = self.current.write().await; current.logs.clear(); - current.last_updated = Instant::now(); + current.mark_fresh(); // Also reset monitoring level when clearing logs { diff --git a/src-tauri/src/ipc/memory.rs b/src-tauri/src/ipc/memory.rs index c3cbdeef0..7fc7b2d8c 100644 --- a/src-tauri/src/ipc/memory.rs +++ b/src-tauri/src/ipc/memory.rs @@ -1,14 +1,11 @@ -use kode_bridge::IpcStreamClient; use serde::{Deserialize, Serialize}; -use std::{ - sync::{Arc, OnceLock}, - time::Instant, -}; +use std::{sync::Arc, time::Instant}; use tokio::{sync::RwLock, time::Duration}; use crate::{ - logging, - utils::{dirs::ipc_path, format::fmt_bytes, logging::Type}, + ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, + singleton_lazy_with_logging, + utils::format::fmt_bytes, }; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -34,70 +31,66 @@ impl Default for CurrentMemory { } } -// Minimal memory monitor -pub struct MemoryMonitor { - current: Arc>, +impl MonitorData for CurrentMemory { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } } -static INSTANCE: OnceLock = OnceLock::new(); +impl StreamingParser for CurrentMemory { + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(memory) = serde_json::from_str::(line.trim()) { + tokio::spawn(async move { + let mut current_guard = current.write().await; + current_guard.inuse = memory.inuse; + current_guard.oslimit = memory.oslimit; + current_guard.mark_fresh(); + }); + } + Ok(()) + } +} + +// Minimal memory monitor using the new architecture +pub struct MemoryMonitor { + monitor: IpcStreamMonitor, +} + +impl Default for MemoryMonitor { + fn default() -> Self { + MemoryMonitor { + monitor: IpcStreamMonitor::new( + "/memory".to_string(), + Duration::from_secs(10), + Duration::from_secs(2), + Duration::from_secs(10), + ), + } + } +} + +// Use simplified singleton_lazy_with_logging macro +singleton_lazy_with_logging!( + MemoryMonitor, + INSTANCE, + "MemoryMonitor", + MemoryMonitor::default +); impl MemoryMonitor { - pub fn global() -> &'static MemoryMonitor { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let client = IpcStreamClient::new(ipc_path).unwrap(); - - let instance = MemoryMonitor::new(client); - logging!( - info, - Type::Ipc, - true, - "MemoryMonitor initialized with IPC path: {}", - ipc_path - ); - instance - }) - } - - fn new(client: IpcStreamClient) -> Self { - let current = Arc::new(RwLock::new(CurrentMemory::default())); - let monitor_current = current.clone(); - - tokio::spawn(async move { - loop { - let _ = client - .get("/memory") - .timeout(Duration::from_secs(10)) - .process_lines(|line| { - if let Ok(memory) = serde_json::from_str::(line.trim()) { - tokio::spawn({ - let current = monitor_current.clone(); - async move { - *current.write().await = CurrentMemory { - inuse: memory.inuse, - oslimit: memory.oslimit, - last_updated: Instant::now(), - }; - } - }); - } - Ok(()) - }) - .await; - tokio::time::sleep(Duration::from_secs(2)).await; // Memory updates less frequently - } - }); - - Self { current } - } - pub async fn current(&self) -> CurrentMemory { - self.current.read().await.clone() + self.monitor.current().await } pub async fn is_fresh(&self) -> bool { - self.current.read().await.last_updated.elapsed() < Duration::from_secs(10) + self.monitor.is_fresh().await } } diff --git a/src-tauri/src/ipc/mod.rs b/src-tauri/src/ipc/mod.rs index c04d9d21a..8332cee31 100644 --- a/src-tauri/src/ipc/mod.rs +++ b/src-tauri/src/ipc/mod.rs @@ -1,6 +1,7 @@ pub mod general; pub mod logs; pub mod memory; +pub mod monitor; pub mod traffic; pub use general::IpcManager; diff --git a/src-tauri/src/ipc/monitor.rs b/src-tauri/src/ipc/monitor.rs new file mode 100644 index 000000000..261df1c97 --- /dev/null +++ b/src-tauri/src/ipc/monitor.rs @@ -0,0 +1,119 @@ +use kode_bridge::IpcStreamClient; +use std::sync::Arc; +use tokio::{sync::RwLock, time::Duration}; + +use crate::{ + logging, + utils::{dirs::ipc_path, logging::Type}, +}; + +/// Generic base structure for IPC monitoring data with freshness tracking +pub trait MonitorData: Clone + Send + Sync + 'static { + /// Update the last_updated timestamp to now + fn mark_fresh(&mut self); + + /// Check if data is fresh based on the given duration + fn is_fresh_within(&self, duration: Duration) -> bool; +} + +/// Trait for parsing streaming data and updating monitor state +pub trait StreamingParser: MonitorData { + /// Parse a line of streaming data and update the current state + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box>; +} + +/// Generic IPC stream monitor that handles the common streaming pattern +pub struct IpcStreamMonitor +where + T: MonitorData + StreamingParser + Default, +{ + current: Arc>, + #[allow(dead_code)] + endpoint: String, + #[allow(dead_code)] + timeout: Duration, + #[allow(dead_code)] + retry_interval: Duration, + freshness_duration: Duration, +} + +impl IpcStreamMonitor +where + T: MonitorData + StreamingParser + Default, +{ + pub fn new( + endpoint: String, + timeout: Duration, + retry_interval: Duration, + freshness_duration: Duration, + ) -> Self { + let current = Arc::new(RwLock::new(T::default())); + let monitor_current = current.clone(); + let endpoint_clone = endpoint.clone(); + + // Start the monitoring task + tokio::spawn(async move { + Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await; + }); + + Self { + current, + endpoint, + timeout, + retry_interval, + freshness_duration, + } + } + + pub async fn current(&self) -> T { + self.current.read().await.clone() + } + + pub async fn is_fresh(&self) -> bool { + self.current + .read() + .await + .is_fresh_within(self.freshness_duration) + } + + /// The core streaming task that can be specialized per monitor type + async fn streaming_task( + current: Arc>, + endpoint: String, + timeout: Duration, + retry_interval: Duration, + ) { + loop { + let ipc_path_buf = match ipc_path() { + Ok(path) => path, + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); + tokio::time::sleep(retry_interval).await; + continue; + } + }; + + let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); + + let client = match IpcStreamClient::new(ipc_path) { + Ok(client) => client, + Err(e) => { + logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); + tokio::time::sleep(retry_interval).await; + continue; + } + }; + + let _ = client + .get(&endpoint) + .timeout(timeout) + .process_lines(|line| T::parse_and_update(line, current.clone())) + .await; + + tokio::time::sleep(retry_interval).await; + } + } +} diff --git a/src-tauri/src/ipc/traffic.rs b/src-tauri/src/ipc/traffic.rs index 5c971d89a..a7a329a1b 100644 --- a/src-tauri/src/ipc/traffic.rs +++ b/src-tauri/src/ipc/traffic.rs @@ -1,14 +1,11 @@ -use kode_bridge::IpcStreamClient; use serde::{Deserialize, Serialize}; -use std::{ - sync::{Arc, OnceLock}, - time::Instant, -}; +use std::{sync::Arc, time::Instant}; use tokio::{sync::RwLock, time::Duration}; use crate::{ - logging, - utils::{dirs::ipc_path, format::fmt_bytes, logging::Type}, + ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, + singleton_lazy_with_logging, + utils::format::fmt_bytes, }; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -38,84 +35,101 @@ impl Default for CurrentTraffic { } } -// Minimal traffic monitor -pub struct TrafficMonitor { - current: Arc>, +impl MonitorData for CurrentTraffic { + fn mark_fresh(&mut self) { + self.last_updated = Instant::now(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.last_updated.elapsed() < duration + } } -static INSTANCE: OnceLock = OnceLock::new(); +// Traffic monitoring state for calculating rates +#[derive(Debug, Clone, Default)] +pub struct TrafficMonitorState { + pub current: CurrentTraffic, + pub last_traffic: Option, +} + +impl MonitorData for TrafficMonitorState { + fn mark_fresh(&mut self) { + self.current.mark_fresh(); + } + + fn is_fresh_within(&self, duration: Duration) -> bool { + self.current.is_fresh_within(duration) + } +} + +impl StreamingParser for TrafficMonitorState { + fn parse_and_update( + line: &str, + current: Arc>, + ) -> Result<(), Box> { + if let Ok(traffic) = serde_json::from_str::(line.trim()) { + tokio::spawn(async move { + let mut state_guard = current.write().await; + + let (up_rate, down_rate) = state_guard + .last_traffic + .as_ref() + .map(|l| { + ( + traffic.up.saturating_sub(l.up), + traffic.down.saturating_sub(l.down), + ) + }) + .unwrap_or((0, 0)); + + state_guard.current = CurrentTraffic { + up_rate, + down_rate, + total_up: traffic.up, + total_down: traffic.down, + last_updated: Instant::now(), + }; + + state_guard.last_traffic = Some(traffic); + }); + } + Ok(()) + } +} + +// Minimal traffic monitor using the new architecture +pub struct TrafficMonitor { + monitor: IpcStreamMonitor, +} + +impl Default for TrafficMonitor { + fn default() -> Self { + TrafficMonitor { + monitor: IpcStreamMonitor::new( + "/traffic".to_string(), + Duration::from_secs(10), + Duration::from_secs(1), + Duration::from_secs(5), + ), + } + } +} + +// Use simplified singleton_lazy_with_logging macro +singleton_lazy_with_logging!( + TrafficMonitor, + INSTANCE, + "TrafficMonitor", + TrafficMonitor::default +); impl TrafficMonitor { - pub fn global() -> &'static TrafficMonitor { - INSTANCE.get_or_init(|| { - let ipc_path_buf = ipc_path().unwrap(); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let client = IpcStreamClient::new(ipc_path).unwrap(); - - let instance = TrafficMonitor::new(client); - logging!( - info, - Type::Ipc, - true, - "TrafficMonitor initialized with IPC path: {}", - ipc_path - ); - instance - }) - } - - fn new(client: IpcStreamClient) -> Self { - let current = Arc::new(RwLock::new(CurrentTraffic::default())); - let monitor_current = current.clone(); - - tokio::spawn(async move { - let mut last: Option = None; - loop { - let _ = client - .get("/traffic") - .timeout(Duration::from_secs(10)) - .process_lines(|line| { - if let Ok(traffic) = serde_json::from_str::(line.trim()) { - let (up_rate, down_rate) = last - .as_ref() - .map(|l| { - ( - traffic.up.saturating_sub(l.up), - traffic.down.saturating_sub(l.down), - ) - }) - .unwrap_or((0, 0)); - - tokio::spawn({ - let current = monitor_current.clone(); - async move { - *current.write().await = CurrentTraffic { - up_rate, - down_rate, - total_up: traffic.up, - total_down: traffic.down, - last_updated: Instant::now(), - }; - } - }); - last = Some(traffic); - } - Ok(()) - }) - .await; - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - - Self { current } - } - pub async fn current(&self) -> CurrentTraffic { - self.current.read().await.clone() + self.monitor.current().await.current } pub async fn is_fresh(&self) -> bool { - self.current.read().await.last_updated.elapsed() < Duration::from_secs(5) + self.monitor.is_fresh().await } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7417cee8..e3d95b5c1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,7 +15,6 @@ use crate::{ }; use config::Config; use parking_lot::Mutex; -use std::sync::Once; use tauri::AppHandle; #[cfg(target_os = "macos")] use tauri::Manager; @@ -27,31 +26,34 @@ use utils::logging::Type; /// A global singleton handle to the application. pub struct AppHandleManager { - inner: Mutex>, - init: Once, + handle: Mutex>, } impl AppHandleManager { - /// Get the global instance of the app handle manager. - pub fn global() -> &'static Self { - static INSTANCE: AppHandleManager = AppHandleManager { - inner: Mutex::new(None), - init: Once::new(), - }; - &INSTANCE + /// Create a new AppHandleManager instance + fn new() -> Self { + Self { + handle: Mutex::new(None), + } } /// Initialize the app handle manager with an app handle. pub fn init(&self, handle: AppHandle) { - self.init.call_once(|| { - let mut app_handle = self.inner.lock(); + let mut app_handle = self.handle.lock(); + if app_handle.is_none() { *app_handle = Some(handle); - }); + logging!( + info, + Type::Setup, + true, + "AppHandleManager initialized with handle" + ); + } } /// Get the app handle if it has been initialized. pub fn get(&self) -> Option { - self.inner.lock().clone() + self.handle.lock().clone() } /// Get the app handle, panics if it hasn't been initialized. @@ -59,168 +61,228 @@ impl AppHandleManager { self.get().expect("AppHandle not initialized") } + /// Check if the app handle has been initialized. + pub fn is_initialized(&self) -> bool { + self.handle.lock().is_some() + } + + #[cfg(target_os = "macos")] + pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> { + let app_handle = self.handle.lock(); + if let Some(app_handle) = app_handle.as_ref() { + app_handle + .set_activation_policy(policy) + .map_err(|e| e.to_string()) + } else { + Err("AppHandle not initialized".to_string()) + } + } + pub fn set_activation_policy_regular(&self) { #[cfg(target_os = "macos")] { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular); + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) { + logging!( + warn, + Type::Setup, + true, + "Failed to set regular activation policy: {}", + e + ); + } } } pub fn set_activation_policy_accessory(&self) { #[cfg(target_os = "macos")] { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory); + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) { + logging!( + warn, + Type::Setup, + true, + "Failed to set accessory activation policy: {}", + e + ); + } } } pub fn set_activation_policy_prohibited(&self) { #[cfg(target_os = "macos")] { - let app_handle = self.inner.lock(); - let app_handle = app_handle.as_ref().unwrap(); - let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited); - } - } -} - -pub fn run() { - utils::network::NetworkManager::global().init(); - - let _ = utils::dirs::init_portable_flag(); - - // 异步单例检测 - AsyncHandler::spawn(move || async move { - logging!(info, Type::Setup, true, "开始检查单例实例..."); - match timeout(Duration::from_secs(3), server::check_singleton()).await { - Ok(result) => { - if result.is_err() { - logging!(info, Type::Setup, true, "检测到已有应用实例运行"); - if let Some(app_handle) = AppHandleManager::global().get() { - app_handle.exit(0); - } else { - std::process::exit(0); - } - } else { - logging!(info, Type::Setup, true, "未检测到其他应用实例"); - } - } - Err(_) => { + if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) { logging!( warn, Type::Setup, true, - "单例检查超时,假定没有其他实例运行" + "Failed to set prohibited activation policy: {}", + e ); } } - }); + } +} - #[cfg(target_os = "linux")] - std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); +// Use unified singleton macro +singleton_with_logging!(AppHandleManager, INSTANCE, "AppHandleManager"); - #[cfg(debug_assertions)] - let devtools = tauri_plugin_devtools::init(); +/// Application initialization helper functions +mod app_init { + use super::*; - #[allow(unused_mut)] - let mut builder = tauri::Builder::default() - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_deep_link::init()) - .manage(Mutex::new(state::lightweight::LightWeightState::default())) - .setup(|app| { - logging!(info, Type::Setup, true, "开始应用初始化..."); - let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); - #[cfg(target_os = "macos")] - { - auto_start_plugin_builder = auto_start_plugin_builder - .macos_launcher(MacosLauncher::LaunchAgent) - .app_name(app.config().identifier.clone()); - } - let _ = app.handle().plugin(auto_start_plugin_builder.build()); - - #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] - { - use tauri_plugin_deep_link::DeepLinkExt; - logging!(info, Type::Setup, true, "注册深层链接..."); - logging_error!(Type::System, true, app.deep_link().register_all()); - } - - app.deep_link().on_open_url(|event| { - AsyncHandler::spawn(move || { - let url = event.urls().first().map(|u| u.to_string()); - async move { - if let Some(url) = url { - logging_error!(Type::Setup, true, resolve_scheme(url).await); + /// Initialize singleton monitoring for other instances + pub fn init_singleton_check() { + AsyncHandler::spawn(move || async move { + logging!(info, Type::Setup, true, "开始检查单例实例..."); + match timeout(Duration::from_secs(3), server::check_singleton()).await { + Ok(result) => { + if result.is_err() { + logging!(info, Type::Setup, true, "检测到已有应用实例运行"); + if let Some(app_handle) = AppHandleManager::global().get() { + app_handle.exit(0); + } else { + std::process::exit(0); } + } else { + logging!(info, Type::Setup, true, "未检测到其他应用实例"); } - }); - }); + } + Err(_) => { + logging!( + warn, + Type::Setup, + true, + "单例检查超时,假定没有其他实例运行" + ); + } + } + }); + } - // 窗口管理 - logging!(info, Type::Setup, true, "初始化窗口状态管理..."); - let window_state_plugin = tauri_plugin_window_state::Builder::new() - .with_filename("window_state.json") - .with_state_flags(tauri_plugin_window_state::StateFlags::default()) - .build(); - let _ = app.handle().plugin(window_state_plugin); + /// Setup plugins for the Tauri builder + pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder { + let mut builder = builder + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_deep_link::init()); - // 异步处理 - let app_handle = app.handle().clone(); - AsyncHandler::spawn(move || async move { - logging!(info, Type::Setup, true, "异步执行应用设置..."); - match timeout( - Duration::from_secs(30), - resolve::resolve_setup_async(&app_handle), - ) - .await - { - Ok(_) => { - logging!(info, Type::Setup, true, "应用设置成功完成"); - } - Err(_) => { - logging!( - error, - Type::Setup, - true, - "应用设置超时(30秒),继续执行后续流程" - ); + #[cfg(debug_assertions)] + { + builder = builder.plugin(tauri_plugin_devtools::init()); + } + + builder.manage(std::sync::Mutex::new( + state::lightweight::LightWeightState::default(), + )) + } + + /// Setup deep link handling + pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box> { + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] + { + logging!(info, Type::Setup, true, "注册深层链接..."); + app.deep_link().register_all()?; + } + + app.deep_link().on_open_url(|event| { + AsyncHandler::spawn(move || { + let url = event.urls().first().map(|u| u.to_string()); + async move { + if let Some(url) = url { + if let Err(e) = resolve_scheme(url).await { + logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e); + } } } }); + }); - logging!(info, Type::Setup, true, "执行主要设置操作..."); + Ok(()) + } - logging!(info, Type::Setup, true, "初始化AppHandleManager..."); - AppHandleManager::global().init(app.handle().clone()); + /// Setup autostart plugin + pub fn setup_autostart(app: &tauri::App) -> Result<(), Box> { + #[cfg(target_os = "macos")] + let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); + #[cfg(not(target_os = "macos"))] + let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); - logging!(info, Type::Setup, true, "初始化核心句柄..."); - core::handle::Handle::global().init(app.handle()); + #[cfg(target_os = "macos")] + { + auto_start_plugin_builder = auto_start_plugin_builder + .macos_launcher(MacosLauncher::LaunchAgent) + .app_name(app.config().identifier.clone()); + } + app.handle().plugin(auto_start_plugin_builder.build())?; + Ok(()) + } - logging!(info, Type::Setup, true, "初始化配置..."); - if let Err(e) = utils::init::init_config() { - logging!(error, Type::Setup, true, "初始化配置失败: {}", e); + /// Setup window state management + pub fn setup_window_state(app: &tauri::App) -> Result<(), Box> { + logging!(info, Type::Setup, true, "初始化窗口状态管理..."); + let window_state_plugin = tauri_plugin_window_state::Builder::new() + .with_filename("window_state.json") + .with_state_flags(tauri_plugin_window_state::StateFlags::default()) + .build(); + app.handle().plugin(window_state_plugin)?; + Ok(()) + } + + /// Initialize core components asynchronously + pub fn init_core_async(app_handle: tauri::AppHandle) { + AsyncHandler::spawn(move || async move { + logging!(info, Type::Setup, true, "异步执行应用设置..."); + match timeout( + Duration::from_secs(30), + resolve::resolve_setup_async(&app_handle), + ) + .await + { + Ok(_) => { + logging!(info, Type::Setup, true, "应用设置成功完成"); + } + Err(_) => { + logging!( + error, + Type::Setup, + true, + "应用设置超时(30秒),继续执行后续流程" + ); + } } + }); + } - logging!(info, Type::Setup, true, "初始化资源..."); - if let Err(e) = utils::init::init_resources() { - logging!(error, Type::Setup, true, "初始化资源失败: {}", e); - } + /// Initialize core components synchronously + pub fn init_core_sync(app_handle: &tauri::AppHandle) -> Result<(), Box> { + logging!(info, Type::Setup, true, "初始化AppHandleManager..."); + AppHandleManager::global().init(app_handle.clone()); - logging!(info, Type::Setup, true, "初始化完成,继续执行"); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - // common + logging!(info, Type::Setup, true, "初始化核心句柄..."); + core::handle::Handle::global().init(app_handle); + + logging!(info, Type::Setup, true, "初始化配置..."); + utils::init::init_config()?; + + logging!(info, Type::Setup, true, "初始化资源..."); + utils::init::init_resources()?; + + logging!(info, Type::Setup, true, "核心组件初始化完成"); + Ok(()) + } + + /// Generate all command handlers for the application + pub fn generate_handlers( + ) -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + tauri::generate_handler![ + // Common commands cmd::get_sys_proxy, cmd::get_auto_proxy, cmd::open_app_dir, @@ -231,11 +293,11 @@ pub fn run() { cmd::get_network_interfaces, cmd::get_system_hostname, cmd::restart_app, - // 内核管理 + // Core management cmd::start_core, cmd::stop_core, cmd::restart_core, - // 启动命令 + // Application lifecycle cmd::notify_ui_ready, cmd::update_ui_stage, cmd::reset_ui_ready_state, @@ -243,16 +305,16 @@ pub fn run() { cmd::get_app_uptime, cmd::get_auto_launch_status, cmd::is_admin, - // 添加轻量模式相关命令 + // Lightweight mode cmd::entry_lightweight_mode, cmd::exit_lightweight_mode, - // service 管理 + // Service management cmd::install_service, cmd::uninstall_service, cmd::reinstall_service, cmd::repair_service, cmd::is_service_available, - // clash + // Clash core commands cmd::get_clash_info, cmd::patch_clash_config, cmd::patch_clash_mode, @@ -289,6 +351,7 @@ pub fn run() { cmd::get_group_proxy_delays, cmd::is_clash_debug_enabled, cmd::clash_gc, + // Logging and monitoring cmd::get_clash_logs, cmd::start_logs_monitoring, cmd::clear_logs, @@ -299,7 +362,7 @@ pub fn run() { cmd::get_system_monitor_overview, cmd::start_traffic_service, cmd::stop_traffic_service, - // verge + // Verge configuration cmd::get_verge_config, cmd::patch_verge_config, cmd::test_delay, @@ -309,7 +372,7 @@ pub fn run() { cmd::open_devtools, cmd::exit_app, cmd::get_network_interfaces_info, - // profile + // Profile management cmd::get_profiles, cmd::enhance_profiles, cmd::patch_profiles_config, @@ -323,67 +386,254 @@ pub fn run() { cmd::read_profile_file, cmd::save_profile_file, cmd::get_next_update_time, - // script validation + // Script validation cmd::script_validate_notice, cmd::validate_script_file, - // clash api + // Clash API cmd::clash_api_get_proxy_delay, - // backup + // Backup and WebDAV cmd::create_webdav_backup, cmd::save_webdav_config, cmd::list_webdav_backup, cmd::delete_webdav_backup, cmd::restore_webdav_backup, - // export diagnostic info for issue reporting + // Diagnostics and system info cmd::export_diagnostic_info, - // get system info for display cmd::get_system_info, - // media unlock checker + // Media unlock checker cmd::get_unlock_items, cmd::check_media_unlock, - // light-weight model - cmd::entry_lightweight_mode, - ]); + ] + } +} - #[cfg(debug_assertions)] - { - builder = builder.plugin(devtools); - } - - // Macos Application Menu - #[cfg(target_os = "macos")] - { - // Temporary Achived due to cannot CMD+C/V/A +pub fn run() { + // Setup singleton check + app_init::init_singleton_check(); + + // Initialize network manager + utils::network::NetworkManager::global().init(); + + // Initialize portable flag + let _ = utils::dirs::init_portable_flag(); + + // Set Linux environment variable + #[cfg(target_os = "linux")] + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + + // Create and configure the Tauri builder + let builder = app_init::setup_plugins(tauri::Builder::default()) + .setup(|app| { + logging!(info, Type::Setup, true, "开始应用初始化..."); + + // Setup autostart plugin + if let Err(e) = app_init::setup_autostart(app) { + logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e); + } + + // Setup deep links + if let Err(e) = app_init::setup_deep_links(app) { + logging!( + error, + Type::Setup, + true, + "Failed to setup deep links: {}", + e + ); + } + + // Setup window state management + if let Err(e) = app_init::setup_window_state(app) { + logging!( + error, + Type::Setup, + true, + "Failed to setup window state: {}", + e + ); + } + + // Initialize core components asynchronously + app_init::init_core_async(app.handle().clone()); + + logging!(info, Type::Setup, true, "执行主要设置操作..."); + + // Initialize core components synchronously + if let Err(e) = app_init::init_core_sync(app.handle()) { + logging!( + error, + Type::Setup, + true, + "Failed to initialize core components: {}", + e + ); + return Err(e); + } + + logging!(info, Type::Setup, true, "初始化完成,继续执行"); + Ok(()) + }) + .invoke_handler(app_init::generate_handlers()); + + /// Event handling helper functions + mod event_handlers { + use super::*; + + /// Handle application ready/resumed events + pub fn handle_ready_resumed(app_handle: &tauri::AppHandle) { + logging!(info, Type::System, true, "应用就绪或恢复"); + AppHandleManager::global().init(app_handle.clone()); + + #[cfg(target_os = "macos")] + { + if let Some(window) = app_handle.get_webview_window("main") { + logging!(info, Type::Window, true, "设置macOS窗口标题"); + let _ = window.set_title("Clash Verge"); + } + } + } + + /// Handle application reopen events (macOS) + #[cfg(target_os = "macos")] + pub fn handle_reopen(app_handle: &tauri::AppHandle, has_visible_windows: bool) { + if !has_visible_windows { + AppHandleManager::global().set_activation_policy_regular(); + } + AppHandleManager::global().init(app_handle.clone()); + } + + /// Handle window close requests + pub fn handle_window_close(api: &tauri::WindowEvent) { + #[cfg(target_os = "macos")] + AppHandleManager::global().set_activation_policy_accessory(); + + if core::handle::Handle::global().is_exiting() { + return; + } + + log::info!(target: "app", "closing window..."); + if let tauri::WindowEvent::CloseRequested { api, .. } = api { + api.prevent_close(); + if let Some(window) = core::handle::Handle::global().get_window() { + let _ = window.hide(); + } else { + logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); + } + } + } + + /// Handle window focus events + pub fn handle_window_focus(focused: bool) { + let is_enable_global_hotkey = Config::verge() + .latest_ref() + .enable_global_hotkey + .unwrap_or(true); + + if focused { + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = + hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdQ) + { + logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e); + } + if let Err(e) = + hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdW) + { + logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e); + } + } + + if !is_enable_global_hotkey { + if let Err(e) = hotkey::Hotkey::global().init() { + logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e); + } + } + return; + } + + // Handle unfocused state + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+Q: {}", + e + ); + } + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+W: {}", + e + ); + } + } + + if !is_enable_global_hotkey { + if let Err(e) = hotkey::Hotkey::global().reset() { + logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e); + } + } + } + + /// Handle window destroyed events + pub fn handle_window_destroyed() { + #[cfg(target_os = "macos")] + { + use crate::core::hotkey::SystemHotkey; + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+Q on destroy: {}", + e + ); + } + if let Err(e) = + hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW) + { + logging!( + error, + Type::Hotkey, + true, + "Failed to unregister CMD+W on destroy: {}", + e + ); + } + } + } } + // Build the application let app = builder .build(tauri::generate_context!()) .expect("error while running tauri application"); app.run(|app_handle, e| match e { tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { - logging!(info, Type::System, true, "应用就绪或恢复"); - AppHandleManager::global().init(app_handle.clone()); - #[cfg(target_os = "macos")] - { - if let Some(window) = AppHandleManager::global() - .get_handle() - .get_webview_window("main") - { - logging!(info, Type::Window, true, "设置macOS窗口标题"); - let _ = window.set_title("Clash Verge"); - } - } + event_handlers::handle_ready_resumed(app_handle); } #[cfg(target_os = "macos")] tauri::RunEvent::Reopen { has_visible_windows, .. } => { - if !has_visible_windows { - AppHandleManager::global().set_activation_policy_regular(); - } - AppHandleManager::global().init(app_handle.clone()); + event_handlers::handle_reopen(app_handle, has_visible_windows); } tauri::RunEvent::ExitRequested { api, code, .. } => { if code.is_none() { @@ -391,7 +641,7 @@ pub fn run() { } } tauri::RunEvent::Exit => { - // avoid duplicate cleanup + // Avoid duplicate cleanup if core::handle::Handle::global().is_exiting() { return; } @@ -400,82 +650,14 @@ pub fn run() { tauri::RunEvent::WindowEvent { label, event, .. } => { if label == "main" { match event { - tauri::WindowEvent::CloseRequested { api, .. } => { - #[cfg(target_os = "macos")] - AppHandleManager::global().set_activation_policy_accessory(); - if core::handle::Handle::global().is_exiting() { - return; - } - log::info!(target: "app", "closing window..."); - api.prevent_close(); - if let Some(window) = core::handle::Handle::global().get_window() { - let _ = window.hide(); - } else { - logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); - } + tauri::WindowEvent::CloseRequested { .. } => { + event_handlers::handle_window_close(&event); } - tauri::WindowEvent::Focused(true) => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().register("CMD+Q", "quit") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().register("CMD+W", "hide") - ); - } - { - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - if !is_enable_global_hotkey { - logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init()) - } - } - } - tauri::WindowEvent::Focused(false) => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+Q") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+W") - ); - } - { - let is_enable_global_hotkey = Config::verge() - .latest_ref() - .enable_global_hotkey - .unwrap_or(true); - if !is_enable_global_hotkey { - logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset()) - } - } + tauri::WindowEvent::Focused(focused) => { + event_handlers::handle_window_focus(focused); } tauri::WindowEvent::Destroyed => { - #[cfg(target_os = "macos")] - { - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+Q") - ); - logging_error!( - Type::Hotkey, - true, - hotkey::Hotkey::global().unregister("CMD+W") - ); - } + event_handlers::handle_window_destroyed(); } _ => {} } diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index ff9ae361c..00c159d8b 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -22,14 +22,23 @@ const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; // 添加退出轻量模式的锁,防止并发调用 static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); -fn with_lightweight_status(f: F) -> R +fn with_lightweight_status(f: F) -> Option where F: FnOnce(&mut LightWeightState) -> R, { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let state = app_handle.state::>(); - let mut guard = state.lock(); - f(&mut guard) + if let Some(app_handle) = handle::Handle::global().app_handle() { + // Try to get state, but don't panic if it's not managed yet + if let Some(state) = app_handle.try_state::>() { + let mut guard = state.lock(); + Some(f(&mut guard)) + } else { + // State not managed yet, return None + None + } + } else { + // App handle not available yet + None + } } pub fn run_once_auto_lightweight() { @@ -62,7 +71,17 @@ pub fn run_once_auto_lightweight() { pub fn auto_lightweight_mode_init() { if let Some(app_handle) = handle::Handle::global().app_handle() { - let _ = app_handle.state::>(); + // Check if state is available before accessing it + if app_handle.try_state::>().is_none() { + logging!( + warn, + Type::Lightweight, + true, + "LightWeightState 尚未初始化,跳过自动轻量模式初始化" + ); + return; + } + let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false); let enable_auto = { Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false); @@ -87,18 +106,20 @@ pub fn auto_lightweight_mode_init() { // 检查是否处于轻量模式 pub fn is_in_lightweight_mode() -> bool { - with_lightweight_status(|state| state.is_lightweight) + with_lightweight_status(|state| state.is_lightweight).unwrap_or(false) } // 设置轻量模式状态 pub fn set_lightweight_mode(value: bool) { - with_lightweight_status(|state| { + if with_lightweight_status(|state| { state.set_lightweight_mode(value); - }); - - // 触发托盘更新 - if let Err(e) = Tray::global().update_part() { - log::warn!("Failed to update tray: {e}"); + }) + .is_some() + { + // 只有在状态可用时才触发托盘更新 + if let Err(e) = Tray::global().update_part() { + log::warn!("Failed to update tray: {e}"); + } } } diff --git a/src-tauri/src/state/proxy.rs b/src-tauri/src/state/proxy.rs index 56733fda8..777eacda1 100644 --- a/src-tauri/src/state/proxy.rs +++ b/src-tauri/src/state/proxy.rs @@ -1,24 +1,24 @@ +use crate::singleton; +use dashmap::DashMap; +use serde_json::Value; +use std::sync::Arc; use std::time::{Duration, Instant}; +use tokio::sync::OnceCell; + pub struct CacheEntry { pub value: Arc, pub expires_at: Instant, } -use dashmap::DashMap; -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::OnceCell; pub struct ProxyRequestCache { pub map: DashMap>>, } impl ProxyRequestCache { - pub fn global() -> &'static Self { - static INSTANCE: once_cell::sync::OnceCell = - once_cell::sync::OnceCell::new(); - INSTANCE.get_or_init(|| ProxyRequestCache { + fn new() -> Self { + ProxyRequestCache { map: DashMap::new(), - }) + } } pub fn make_key(prefix: &str, id: &str) -> String { @@ -63,3 +63,6 @@ impl ProxyRequestCache { Arc::clone(&cell.get().unwrap().value) } } + +// Use singleton macro +singleton!(ProxyRequestCache, INSTANCE); diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index e00e3bd62..74b895bdb 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -9,5 +9,6 @@ pub mod network; pub mod notification; pub mod resolve; pub mod server; +pub mod singleton; pub mod tmpl; pub mod window_manager; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index d72284d80..dfa5fcefb 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use lazy_static::lazy_static; use parking_lot::Mutex; use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response}; use std::{ @@ -8,7 +7,7 @@ use std::{ }; use tokio::runtime::{Builder, Runtime}; -use crate::{config::Config, logging, utils::logging::Type}; +use crate::{config::Config, logging, singleton_lazy, utils::logging::Type}; // HTTP2 相关 const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024; @@ -32,9 +31,8 @@ pub struct NetworkManager { connection_error_count: Arc>, } -lazy_static! { - static ref NETWORK_MANAGER: NetworkManager = NetworkManager::new(); -} +// Use singleton_lazy macro to replace lazy_static! +singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new); impl NetworkManager { fn new() -> Self { @@ -58,10 +56,6 @@ impl NetworkManager { } } - pub fn global() -> &'static Self { - &NETWORK_MANAGER - } - /// 初始化网络客户端 pub fn init(&self) { self.init.call_once(|| { @@ -79,7 +73,7 @@ impl NetworkManager { .build() .expect("Failed to build no_proxy client"); - let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock(); + let mut no_proxy_guard = NetworkManager::global().no_proxy_client.lock(); *no_proxy_guard = Some(no_proxy_client); logging!(info, Type::Network, true, "网络管理器初始化完成"); diff --git a/src-tauri/src/utils/singleton.rs b/src-tauri/src/utils/singleton.rs new file mode 100644 index 000000000..2a18edc98 --- /dev/null +++ b/src-tauri/src/utils/singleton.rs @@ -0,0 +1,124 @@ +/// Macro to generate singleton pattern for structs +/// +/// Usage: +/// ```rust,ignore +/// use crate::utils::singleton::singleton; +/// +/// struct MyStruct { +/// value: i32, +/// } +/// impl MyStruct { +/// fn new() -> Self { +/// MyStruct { value: 0 } +/// } +/// } +/// singleton!(MyStruct, INSTANCE); +/// ``` +#[macro_export] +macro_rules! singleton { + ($struct_name:ty, $instance_name:ident) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| Self::new()) + } + } + }; + + ($struct_name:ty, $instance_name:ident, $init_expr:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| $init_expr) + } + } + }; +} + +/// Macro for singleton pattern with logging +#[macro_export] +macro_rules! singleton_with_logging { + ($struct_name:ty, $instance_name:ident, $struct_name_str:literal) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| { + let instance = Self::new(); + $crate::logging!( + info, + $crate::utils::logging::Type::Setup, + true, + concat!($struct_name_str, " initialized") + ); + instance + }) + } + } + }; +} + +/// Macro for singleton pattern with lazy initialization using a closure +/// This replaces patterns like lazy_static! or complex OnceLock initialization +#[macro_export] +macro_rules! singleton_lazy { + ($struct_name:ty, $instance_name:ident, $init_closure:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init($init_closure) + } + } + }; +} + +/// Macro for singleton pattern with lazy initialization and logging +#[macro_export] +macro_rules! singleton_lazy_with_logging { + ($struct_name:ty, $instance_name:ident, $struct_name_str:literal, $init_closure:expr) => { + static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); + + impl $struct_name { + pub fn global() -> &'static $struct_name { + $instance_name.get_or_init(|| { + let instance = $init_closure(); + $crate::logging!( + info, + $crate::utils::logging::Type::Setup, + true, + concat!($struct_name_str, " initialized") + ); + instance + }) + } + } + }; +} + +#[cfg(test)] +mod tests { + struct TestStruct { + value: i32, + } + + impl TestStruct { + fn new() -> Self { + Self { value: 42 } + } + } + + singleton!(TestStruct, TEST_INSTANCE); + + #[test] + fn test_singleton_macro() { + let instance1 = TestStruct::global(); + let instance2 = TestStruct::global(); + + assert_eq!(instance1.value, 42); + assert_eq!(instance2.value, 42); + assert!(std::ptr::eq(instance1, instance2)); + } +}