diff --git a/UPDATELOG.md b/UPDATELOG.md index b733f66af..52f91945a 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -1,9 +1,13 @@ ## v2.4.3 +### 🚀 性能优化 + +- 重构并简化服务模式启动检测流程,消除重复检测 + ### 🐞 修复问题 +- 优化服务模式重装逻辑,避免不必要的重复检查 - 修复轻量模式退出无响应的问题 -- macOS intel Mihomo 兼容性 - macOS Tun/系统代理 模式下图标大小不统一 - 托盘节点切换不再显示隐藏组 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2e7ac723d..7927d4f42 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -556,6 +556,20 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.16", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -1086,6 +1100,7 @@ version = "2.4.3" dependencies = [ "aes-gcm", "anyhow", + "backoff", "base64 0.22.1", "boa_engine", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d202e4afd..d94cb21bd 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -82,6 +82,7 @@ isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding", "parking_lot", ] } +backoff = { version = "0.4.0", features = ["tokio"] } [target.'cfg(windows)'.dependencies] diff --git a/src-tauri/src/cache/mod.rs b/src-tauri/src/cache/mod.rs index 3b8109e39..cea404c14 100644 --- a/src-tauri/src/cache/mod.rs +++ b/src-tauri/src/cache/mod.rs @@ -1,20 +1,23 @@ use crate::singleton; +use anyhow::Result; 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 const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250); + +pub struct CacheEntry { + pub value: Arc, pub expires_at: Instant, } -pub struct Cache { - pub map: DashMap>>>, +pub struct Cache { + pub map: DashMap>>>>, } -impl Cache { +impl Cache { fn new() -> Self { Cache { map: DashMap::new(), @@ -25,10 +28,11 @@ impl Cache { format!("{prefix}:{id}") } - pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc + pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc where - F: Fn() -> Fut + Send + 'static, - Fut: std::future::Future + Send + 'static, + F: Fn() -> Fut + Send + Sync + 'static, + Fut: std::future::Future + Send + 'static, + T: Send + Sync + 'static, { loop { let now = Instant::now(); @@ -79,6 +83,10 @@ impl Cache { } } + // pub fn clean_key(&self, key: &str) { + // self.map.remove(key); + // } + // TODO pub fn clean_default_keys(&self) { // logging!(info, Type::Cache, "Cleaning proxies keys"); @@ -96,5 +104,8 @@ impl Cache { } } -// Use singleton macro -singleton!(Cache, INSTANCE); +pub type CacheService = Cache>; +pub type CacheProxy = Cache; + +singleton!(Cache, PROXY_INSTANCE); +singleton!(Cache>, SERVICE_INSTANCE); diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 01f23deb2..0081f9171 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::{ - cache::Cache, + cache::CacheProxy, config::Config, core::{CoreManager, handle}, }; @@ -317,8 +317,8 @@ pub async fn get_clash_version() -> CmdResult { #[tauri::command] pub async fn get_clash_config() -> CmdResult { let manager = IpcManager::global(); - let cache = Cache::global(); - let key = Cache::make_key("clash_config", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("clash_config", "default"); let value = cache .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async { manager.get_config().await.unwrap_or_else(|e| { @@ -333,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult { /// 强制刷新Clash配置缓存 #[tauri::command] pub async fn force_refresh_clash_config() -> CmdResult { - let cache = Cache::global(); - let key = Cache::make_key("clash_config", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("clash_config", "default"); cache.map.remove(&key); get_clash_config().await } diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index f29bcc3e6..387da3ad7 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -2,7 +2,7 @@ use tauri::Emitter; use super::CmdResult; use crate::{ - cache::Cache, + cache::CacheProxy, core::{handle::Handle, tray::Tray}, ipc::IpcManager, logging, @@ -15,8 +15,8 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60); #[tauri::command] pub async fn get_proxies() -> CmdResult { - let cache = Cache::global(); - let key = Cache::make_key("proxies", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); let value = cache .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { let manager = IpcManager::global(); @@ -32,16 +32,16 @@ pub async fn get_proxies() -> CmdResult { /// 强制刷新代理缓存用于profile切换 #[tauri::command] pub async fn force_refresh_proxies() -> CmdResult { - let cache = Cache::global(); - let key = Cache::make_key("proxies", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); cache.map.remove(&key); get_proxies().await } #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { - let cache = Cache::global(); - let key = Cache::make_key("providers", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("providers", "default"); let value = cache .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { let manager = IpcManager::global(); @@ -85,8 +85,8 @@ pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<() proxy ); - let cache = Cache::global(); - let key = Cache::make_key("proxies", "default"); + let cache = CacheProxy::global(); + let key = CacheProxy::make_key("proxies", "default"); cache.map.remove(&key); if let Err(e) = Tray::global().update_menu().await { diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 872573259..efb4070de 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -1,45 +1,47 @@ use super::CmdResult; use crate::{ - core::{CoreManager, service}, + core::{ + CoreManager, + service::{self, SERVICE_MANAGER, ServiceStatus}, + }, utils::i18n::t, }; -use anyhow::Result; -async fn execute_service_operation_sync(service_op: F, op_type: &str) -> CmdResult -where - F: FnOnce() -> Fut, - Fut: std::future::Future>, - E: ToString + std::fmt::Debug, -{ - if let Err(e) = service_op().await { - let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string()); +async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult { + if let Err(e) = SERVICE_MANAGER + .lock() + .await + .handle_service_status(&status) + .await + { + let emsg = format!("{} Service failed: {}", op_type, e); return Err(t(emsg.as_str()).await); } if CoreManager::global().restart_core().await.is_err() { - let emsg = format!("{} {} failed", "Restart", "Core"); - return Err(t(emsg.as_str()).await); + let emsg = "Restart Core failed"; + return Err(t(emsg).await); } Ok(()) } #[tauri::command] pub async fn install_service() -> CmdResult { - execute_service_operation_sync(service::install_service, "Install").await + execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await } #[tauri::command] pub async fn uninstall_service() -> CmdResult { - execute_service_operation_sync(service::uninstall_service, "Uninstall").await + execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await } #[tauri::command] pub async fn reinstall_service() -> CmdResult { - execute_service_operation_sync(service::reinstall_service, "Reinstall").await + execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await } #[tauri::command] pub async fn repair_service() -> CmdResult { - execute_service_operation_sync(service::force_reinstall_service, "Repair").await + execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await } #[tauri::command] diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index 6dcd206b0..9b065b88b 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -101,7 +101,9 @@ impl Config { Some(("config_validate::boot_error", error_msg)) } else { logging!(info, Type::Config, true, "配置验证成功"); - Some(("config_validate::success", String::new())) + // 前端没有必要知道验证成功的消息,也没有事件驱动 + // Some(("config_validate::success", String::new())) + None } } Err(err) => { diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 05f54087f..cda71e722 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -203,9 +203,6 @@ pub struct IVerge { /// 启用外部控制器 pub enable_external_controller: Option, - - /// 服务状态跟踪 - pub service_state: Option, } #[derive(Default, Debug, Clone, Deserialize, Serialize)] @@ -407,7 +404,6 @@ impl IVerge { auto_light_weight_minutes: Some(10), enable_dns_settings: Some(false), home_cards: None, - service_state: None, enable_external_controller: Some(false), ..Self::default() } @@ -495,7 +491,6 @@ impl IVerge { patch!(auto_light_weight_minutes); patch!(enable_dns_settings); patch!(home_cards); - patch!(service_state); patch!(enable_external_controller); } @@ -592,7 +587,6 @@ pub struct IVergeResponse { pub home_cards: Option, pub enable_hover_jump_navigator: Option, pub enable_external_controller: Option, - pub service_state: Option, } impl From for IVergeResponse { @@ -664,7 +658,6 @@ impl From for IVergeResponse { home_cards: verge.home_cards, enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_external_controller: verge.enable_external_controller, - service_state: verge.service_state, } } } diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 6df7805f9..f8c9b7f05 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -1,13 +1,13 @@ +#[cfg(target_os = "windows")] +use crate::AsyncHandler; use crate::{ config::*, core::{ handle, - service::{self}, + service::{self, SERVICE_MANAGER, ServiceStatus}, }, ipc::IpcManager, - logging, logging_error, - process::AsyncHandler, - singleton_lazy, + logging, logging_error, singleton_lazy, utils::{ dirs, help::{self}, @@ -384,8 +384,6 @@ impl CoreManager { return Ok((true, String::new())); } - logging!(info, Type::Config, true, "开始更新配置"); - // 1. 先生成新的配置内容 logging!(info, Type::Config, true, "生成新的配置内容"); Config::generate().await?; @@ -393,9 +391,8 @@ impl CoreManager { // 2. 验证配置 match self.validate_config().await { Ok((true, _)) => { - logging!(info, Type::Config, true, "配置验证通过"); // 4. 验证通过后,生成正式的运行时配置 - logging!(info, Type::Config, true, "生成运行时配置"); + logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置"); let run_path = Config::generate_file(ConfigType::Run).await?; logging_error!(Type::Config, true, self.put_configs_force(run_path).await); Ok((true, "something".into())) @@ -734,7 +731,8 @@ impl CoreManager { } async fn start_core_by_sidecar(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Running core by sidecar"); + logging!(info, Type::Core, true, "Running core by sidecar"); + let config_file = &Config::generate_file(ConfigType::Run).await?; let app_handle = handle::Handle::global() .app_handle() @@ -763,21 +761,19 @@ impl CoreManager { ]) .spawn()?; - AsyncHandler::spawn(move || async move { - while let Some(event) = rx.recv().await { - if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event - && let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) - { - logging!( - error, - Type::Core, - true, - "[Sidecar] Failed to write stdout to file: {}", - e - ); - } + while let Some(event) = rx.recv().await { + if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event + && let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) + { + logging!( + error, + Type::Core, + true, + "[Sidecar] Failed to write stdout to file: {}", + e + ); } - }); + } let pid = child.pid(); logging!( @@ -792,7 +788,7 @@ impl CoreManager { Ok(()) } fn stop_core_by_sidecar(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Stopping core by sidecar"); + logging!(info, Type::Core, true, "Stopping core by sidecar"); if let Some(child) = self.child_sidecar.lock().take() { let pid = child.pid(); @@ -812,14 +808,14 @@ impl CoreManager { impl CoreManager { async fn start_core_by_service(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Running core by service"); + logging!(info, Type::Core, true, "Running core by service"); let config_file = &Config::generate_file(ConfigType::Run).await?; service::run_core_by_service(config_file).await?; self.set_running_mode(RunningMode::Service); Ok(()) } async fn stop_core_by_service(&self) -> Result<()> { - logging!(trace, Type::Core, true, "Stopping core by service"); + logging!(info, Type::Core, true, "Stopping core by service"); service::stop_core_by_service().await?; self.set_running_mode(RunningMode::NotRunning); Ok(()) @@ -835,58 +831,9 @@ impl Default for CoreManager { } } -// 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 { - logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装"); - if let Err(e) = service::reinstall_service().await { - logging!( - warn, - Type::Core, - true, - "服务重装失败 during attempt_service_init: {}", - e - ); - return Err(e); - } - // 如果重装成功,还需要尝试启动服务 - logging!(info, Type::Core, true, "服务重装成功,尝试启动服务"); - } - - if let Err(e) = self.start_core_by_service().await { - logging!( - warn, - Type::Core, - true, - "通过服务启动核心失败 during attempt_service_init: {}", - e - ); - // 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置 - let mut state = service::ServiceState::get().await; - if !state.prefer_sidecar { - state.prefer_sidecar = true; - state.last_error = Some(format!("通过服务启动核心失败: {e}")); - if let Err(save_err) = state.save().await { - logging!( - error, - Type::Core, - true, - "保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}", - save_err - ); - } - } - return Err(e); - } - Ok(()) - } - pub async fn init(&self) -> Result<()> { - logging!(trace, Type::Core, "Initializing core"); + logging!(info, Type::Core, "Initializing core"); // 应用启动时先清理任何遗留的 mihomo 进程 if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { @@ -899,155 +846,11 @@ impl CoreManager { ); } - let mut core_started_successfully = false; + // 使用简化的启动流程 + logging!(info, Type::Core, true, "开始核心初始化"); + self.start_core().await?; - if service::is_service_available().await.is_ok() { - logging!( - info, - Type::Core, - true, - "服务当前可用或看似可用,尝试通过服务模式启动/重装" - ); - match self.attempt_service_init().await { - Ok(_) => { - logging!(info, Type::Core, true, "服务模式成功启动核心"); - core_started_successfully = true; - } - Err(_err) => { - logging!( - warn, - Type::Core, - true, - "服务模式启动或重装失败。将尝试Sidecar模式回退。" - ); - } - } - } else { - logging!( - info, - Type::Core, - true, - "服务初始不可用 (is_service_available 调用失败)" - ); - } - - if !core_started_successfully { - logging!( - info, - Type::Core, - true, - "核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑" - ); - - let service_state = service::ServiceState::get().await; - - if service_state.prefer_sidecar { - logging!( - info, - Type::Core, - true, - "用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动" - ); - self.start_core_by_sidecar().await?; - // 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束 - // 后续的 Tray::global().subscribe_traffic().await 仍然会执行 - } else { - let has_service_install_record = service_state.last_install_time > 0; - if !has_service_install_record { - logging!( - info, - Type::Core, - true, - "无服务安装记录 (首次运行或状态重置),尝试安装服务" - ); - match service::install_service().await { - Ok(_) => { - logging!(info, Type::Core, true, "服务安装成功(首次尝试)"); - let mut new_state = service::ServiceState::default(); - new_state.record_install(); - new_state.prefer_sidecar = false; - new_state.save().await?; - - if service::is_service_available().await.is_ok() { - logging!(info, Type::Core, true, "新安装的服务可用,尝试启动"); - if self.start_core_by_service().await.is_ok() { - logging!(info, Type::Core, true, "新安装的服务启动成功"); - } else { - logging!( - warn, - Type::Core, - true, - "新安装的服务启动失败,回退到Sidecar模式" - ); - let mut final_state = service::ServiceState::get().await; - final_state.prefer_sidecar = true; - final_state.last_error = - Some("Newly installed service failed to start".to_string()); - final_state.save().await?; - self.start_core_by_sidecar().await?; - } - } else { - logging!( - warn, - Type::Core, - true, - "服务安装成功但未能连接/立即可用,回退到Sidecar模式" - ); - let mut final_state = service::ServiceState::get().await; - final_state.prefer_sidecar = true; - final_state.last_error = Some( - "Newly installed service not immediately available/connectable" - .to_string(), - ); - final_state.save().await?; - self.start_core_by_sidecar().await?; - } - } - Err(err) => { - logging!(warn, Type::Core, true, "服务首次安装失败: {}", err); - let new_state = service::ServiceState { - last_error: Some(err.to_string()), - prefer_sidecar: true, - ..Default::default() - }; - new_state.save().await?; - self.start_core_by_sidecar().await?; - } - } - } else { - // 有安装记录,服务未成功启动,且初始不偏好sidecar - // 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar), - // 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。 - logging!( - info, - Type::Core, - true, - "有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式" - ); - let mut final_state = service::ServiceState::get().await; - if !final_state.prefer_sidecar { - logging!( - warn, - Type::Core, - true, - "prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true" - ); - final_state.prefer_sidecar = true; - final_state.last_error = - Some(final_state.last_error.unwrap_or_else(|| { - "Service startup failed or unavailable before sidecar fallback" - .to_string() - })); - final_state.save().await?; - } - self.start_core_by_sidecar().await?; - } - } - } - - logging!(trace, Type::Core, "Initied core logic completed"); - // #[cfg(target_os = "macos")] - // logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); + logging!(info, Type::Core, true, "核心初始化完成"); Ok(()) } @@ -1061,31 +864,32 @@ impl CoreManager { (*guard).clone() } + pub async fn prestart_core(&self) -> Result<()> { + SERVICE_MANAGER.lock().await.refresh().await?; + match SERVICE_MANAGER.lock().await.current() { + ServiceStatus::Ready => { + self.set_running_mode(RunningMode::Service); + } + _ => { + self.set_running_mode(RunningMode::Sidecar); + } + } + Ok(()) + } + /// 启动核心 pub async fn start_core(&self) -> Result<()> { - if service::is_service_available().await.is_ok() { - if service::check_service_needs_reinstall().await { - service::reinstall_service().await?; + self.prestart_core().await?; + + match self.get_running_mode() { + RunningMode::Service => { + logging_error!(Type::Core, true, self.start_core_by_service().await); + } + RunningMode::NotRunning | RunningMode::Sidecar => { + logging_error!(Type::Core, true, self.start_core_by_sidecar().await); } - logging!(info, Type::Core, true, "服务可用,使用服务模式启动"); - self.start_core_by_service().await?; - return Ok(()); }; - // 服务不可用,检查用户偏好 - let service_state = service::ServiceState::get().await; - if service_state.prefer_sidecar { - logging!( - info, - Type::Core, - true, - "服务不可用,根据用户偏好使用Sidecar模式" - ); - self.start_core_by_sidecar().await?; - } else { - logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式"); - self.start_core_by_sidecar().await?; - } Ok(()) } @@ -1100,6 +904,7 @@ impl CoreManager { /// 重启内核 pub async fn restart_core(&self) -> Result<()> { + logging!(info, Type::Core, true, "Restarting core"); self.stop_core().await?; self.start_core().await?; Ok(()) @@ -1141,3 +946,6 @@ impl CoreManager { Ok(()) } } + +// Use simplified singleton_lazy macro +singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default); diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 9d7a47bad..51344bedc 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1,95 +1,27 @@ use crate::{ + cache::{CacheService, SHORT_TERM_TTL}, config::Config, core::service_ipc::{IpcCommand, send_ipc_request}, - logging, + logging, logging_error, utils::{dirs, logging::Type}, }; use anyhow::{Context, Result, bail}; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use std::{ - env::current_exe, - path::PathBuf, - process::Command as StdCommand, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::{env::current_exe, path::PathBuf, process::Command as StdCommand}; +use tokio::sync::Mutex; const REQUIRED_SERVICE_VERSION: &str = "1.1.2"; // 定义所需的服务版本号 -// 限制重装时间和次数的常量 -const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期 -const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次 -const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数 - -#[derive(Debug, Deserialize, Serialize, Clone, Default)] -pub struct ServiceState { - pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒) - pub install_count: u32, // 24小时内安装次数 - pub last_check_time: u64, // 上次检查时间 - pub last_error: Option, // 上次错误信息 - pub prefer_sidecar: bool, // 用户是否偏好sidecar模式,如拒绝安装服务或安装失败 -} - -impl ServiceState { - // 获取当前的服务状态 - pub async fn get() -> Self { - if let Some(state) = Config::verge().await.latest_ref().service_state.clone() { - return state; - } - Self::default() - } - - // 保存服务状态 - pub async fn save(&self) -> Result<()> { - let config = Config::verge().await; - let mut latest = config.latest_ref().clone(); - latest.service_state = Some(self.clone()); - *config.draft_mut() = latest; - config.apply(); - - // 先获取数据,再异步保存,避免跨await持有锁 - let verge_data = config.latest_ref().clone(); - drop(config); // 显式释放锁 - - verge_data.save_file().await - } - - // 更新安装信息 - pub fn record_install(&mut self) { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - // 检查是否需要重置计数器(24小时已过) - if now - self.last_install_time > ONE_DAY_SECS { - self.install_count = 0; - } - - self.last_install_time = now; - self.install_count += 1; - } - - // 检查是否可以重新安装 - pub fn can_reinstall(&self) -> bool { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - // 如果在冷却期内,不允许重装 - if now - self.last_install_time < REINSTALL_COOLDOWN_SECS { - return false; - } - - // 如果24小时内安装次数过多,也不允许 - if now - self.last_install_time < ONE_DAY_SECS - && self.install_count >= MAX_REINSTALLS_PER_DAY - { - return false; - } - - true - } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ServiceStatus { + Ready, + NeedsReinstall, + InstallRequired, + UninstallRequired, + ReinstallRequired, + ForceReinstallRequired, + Unavailable(String), } // 保留核心数据结构,但将HTTP特定的结构体合并为通用结构体 @@ -101,12 +33,6 @@ pub struct ResponseBody { pub log_file: String, } -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct VersionResponse { - pub service: String, - pub version: String, -} - // 保留通用的响应结构体,用于IPC通信后的数据解析 #[derive(Debug, Deserialize, Serialize, Clone)] pub struct JsonResponse { @@ -115,8 +41,12 @@ pub struct JsonResponse { pub data: Option, } +#[derive(Clone)] +pub struct ServiceManager(ServiceStatus); + +#[allow(clippy::unused_async)] #[cfg(target_os = "windows")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { logging!(info, Type::Service, true, "uninstall service"); use deelevate::{PrivilegeLevel, Token}; @@ -149,8 +79,9 @@ pub async fn uninstall_service() -> Result<()> { Ok(()) } +#[allow(clippy::unused_async)] #[cfg(target_os = "windows")] -pub async fn install_service() -> Result<()> { +async fn install_service() -> Result<()> { logging!(info, Type::Service, true, "install service"); use deelevate::{PrivilegeLevel, Token}; @@ -184,23 +115,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "windows")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get().await; - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -214,26 +131,16 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save().await?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save().await?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } #[allow(clippy::unused_async)] #[cfg(target_os = "linux")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { logging!(info, Type::Service, true, "uninstall service"); use users::get_effective_uid; @@ -273,7 +180,8 @@ pub async fn uninstall_service() -> Result<()> { } #[cfg(target_os = "linux")] -pub async fn install_service() -> Result<()> { +#[allow(clippy::unused_async)] +async fn install_service() -> Result<()> { logging!(info, Type::Service, true, "install service"); use users::get_effective_uid; @@ -313,23 +221,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "linux")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get().await; - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -343,25 +237,15 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save().await?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save().await?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } #[cfg(target_os = "macos")] -pub async fn uninstall_service() -> Result<()> { +async fn uninstall_service() -> Result<()> { use crate::utils::i18n::t; logging!(info, Type::Service, true, "uninstall service"); @@ -397,7 +281,7 @@ pub async fn uninstall_service() -> Result<()> { } #[cfg(target_os = "macos")] -pub async fn install_service() -> Result<()> { +async fn install_service() -> Result<()> { use crate::utils::i18n::t; logging!(info, Type::Service, true, "install service"); @@ -433,23 +317,9 @@ pub async fn install_service() -> Result<()> { } #[cfg(target_os = "macos")] -pub async fn reinstall_service() -> Result<()> { +async fn reinstall_service() -> Result<()> { logging!(info, Type::Service, true, "reinstall service"); - // 获取当前服务状态 - let mut service_state = ServiceState::get().await; - - // 检查是否允许重装 - if !service_state.can_reinstall() { - logging!( - warn, - Type::Service, - true, - "service reinstall rejected: cooldown period or max attempts reached" - ); - bail!("Service reinstallation is rate limited. Please try again later."); - } - // 先卸载服务 if let Err(err) = uninstall_service().await { logging!( @@ -463,440 +333,114 @@ pub async fn reinstall_service() -> Result<()> { // 再安装服务 match install_service().await { - Ok(_) => { - // 记录安装信息并保存 - service_state.record_install(); - service_state.last_error = None; - service_state.save().await?; - Ok(()) - } + Ok(_) => Ok(()), Err(err) => { - let error = format!("failed to install service: {err}"); - service_state.last_error = Some(error.clone()); - service_state.prefer_sidecar = true; - service_state.save().await?; - bail!(error) + bail!(format!("failed to install service: {err}")) } } } -/// 检查服务状态 - 使用IPC通信 -pub async fn check_ipc_service_status() -> Result { - logging!(info, Type::Service, true, "开始检查服务状态 (IPC)"); - - // 使用IPC通信 - let payload = serde_json::json!({}); - // logging!(debug, Type::Service, true, "发送GetClash请求"); - - match send_ipc_request(IpcCommand::GetClash, payload).await { - Ok(response) => { - /* logging!( - debug, - Type::Service, - true, - "收到GetClash响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string()); - logging!(error, Type::Service, true, "服务响应错误: {}", err_msg); - bail!(err_msg); - } - - match response.data { - Some(data) => { - // 检查嵌套结构 - if let (Some(code), Some(msg)) = (data.get("code"), data.get("msg")) { - let code_value = code.as_u64().unwrap_or(0); - let msg_value = msg.as_str().unwrap_or("ok").to_string(); - - // 提取嵌套的data字段并解析为ResponseBody - let response_body = if let Some(nested_data) = data.get("data") { - match serde_json::from_value::(nested_data.clone()) { - Ok(body) => Some(body), - Err(e) => { - logging!( - warn, - Type::Service, - true, - "解析嵌套的ResponseBody失败: {}; 尝试其他方式", - e - ); - None - } - } - } else { - None - }; - - let json_response = JsonResponse { - code: code_value, - msg: msg_value, - data: response_body, - }; - - logging!( - info, - Type::Service, - true, - "服务检测成功: code={}, msg={}, data存在={}", - json_response.code, - json_response.msg, - json_response.data.is_some() - ); - Ok(json_response) - } else { - // 尝试直接解析 - match serde_json::from_value::(data.clone()) { - Ok(json_response) => { - logging!( - info, - Type::Service, - true, - "服务检测成功: code={}, msg={}", - json_response.code, - json_response.msg - ); - Ok(json_response) - } - Err(e) => { - logging!( - error, - Type::Service, - true, - "解析服务响应失败: {}; 原始数据: {:?}", - e, - data - ); - bail!("无法解析服务响应数据: {}", e) - } - } - } - } - None => { - logging!(error, Type::Service, true, "服务响应中没有数据"); - bail!("服务响应中没有数据") - } - } - } - Err(e) => { - logging!(error, Type::Service, true, "IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } - } +/// 强制重装服务(UI修复按钮) +pub async fn force_reinstall_service() -> Result<()> { + logging!(info, Type::Service, true, "用户请求强制重装服务"); + reinstall_service().await.map_err(|err| { + logging!(error, Type::Service, true, "强制重装服务失败: {}", err); + err + }) } /// 检查服务版本 - 使用IPC通信 -pub async fn check_service_version() -> Result { - logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); +async fn check_service_version() -> Result { + let cache = CacheService::global(); + let key = CacheService::make_key("service", "version"); + let version_arc = cache + .get_or_fetch(key, SHORT_TERM_TTL, || async { + logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); + let payload = serde_json::json!({}); + let response = send_ipc_request(IpcCommand::GetVersion, payload).await?; - let payload = serde_json::json!({}); - // logging!(debug, Type::Service, true, "发送GetVersion请求"); + let data = response + .data + .ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?; - match send_ipc_request(IpcCommand::GetVersion, payload).await { - Ok(response) => { - /* logging!( - debug, - Type::Service, - true, - "收到GetVersion响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response - .error - .unwrap_or_else(|| "获取服务版本失败".to_string()); - logging!(error, Type::Service, true, "获取版本错误: {}", err_msg); - bail!(err_msg); + if let Some(nested_data) = data.get("data") + && let Some(version) = nested_data.get("version").and_then(|v| v.as_str()) + { + // logging!(info, Type::Service, true, "获取到服务版本: {}", version); + return Ok(version.to_string()); } - match response.data { - Some(data) => { - if let Some(nested_data) = data.get("data") { - if let Some(version) = nested_data.get("version") - && let Some(version_str) = version.as_str() - { - logging!(info, Type::Service, true, "获取到服务版本: {}", version_str); - return Ok(version_str.to_string()); - } - logging!( - error, - Type::Service, - true, - "嵌套数据中没有version字段: {:?}", - nested_data - ); - } else { - // 兼容旧格式 - match serde_json::from_value::(data.clone()) { - Ok(version_response) => { - logging!( - info, - Type::Service, - true, - "获取到服务版本: {}", - version_response.version - ); - return Ok(version_response.version); - } - Err(e) => { - logging!( - error, - Type::Service, - true, - "解析版本响应失败: {}; 原始数据: {:?}", - e, - data - ); - bail!("无法解析服务版本数据: {}", e) - } - } - } - bail!("响应中未找到有效的版本信息") - } - None => { - logging!(error, Type::Service, true, "版本响应中没有数据"); - bail!("服务版本响应中没有数据") - } - } - } - Err(e) => { - logging!(error, Type::Service, true, "IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } + Ok("unknown".to_string()) + }) + .await; + + match version_arc.as_ref() { + Ok(v) => Ok(v.clone()), + Err(e) => Err(anyhow::Error::msg(e.to_string())), } } /// 检查服务是否需要重装 pub async fn check_service_needs_reinstall() -> bool { - logging!(info, Type::Service, true, "开始检查服务是否需要重装"); - - let service_state = ServiceState::get().await; - - if !service_state.can_reinstall() { - log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数"); - return false; - } - - // 检查版本和可用性 match check_service_version().await { - Ok(version) => { - log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}"); - /* logging!( - info, - Type::Service, - true, - "服务版本检测:当前={}, 要求={}", - version, - REQUIRED_SERVICE_VERSION - ); */ - - let needs_reinstall = version != REQUIRED_SERVICE_VERSION; - if needs_reinstall { - log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}"); - logging!(warn, Type::Service, true, "服务版本不匹配,需要重装"); - - // log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes()); - // log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes()); - } else { - log::info!(target: "app", "服务版本匹配,无需重装"); - // logging!(info, Type::Service, true, "服务版本匹配,无需重装"); - } - - needs_reinstall - } - Err(err) => { - logging!(error, Type::Service, true, "检查服务版本失败: {}", err); - - // 检查服务是否可用 - match is_service_available().await { - Ok(()) => { - log::info!(target: "app", "服务正在运行但版本检查失败: {err}"); - /* logging!( - info, - Type::Service, - true, - "服务正在运行但版本检查失败: {}", - err - ); */ - false - } - _ => { - log::info!(target: "app", "服务不可用或未运行,需要重装"); - // logging!(info, Type::Service, true, "服务不可用或未运行,需要重装"); - true - } - } - } + Ok(version) => version != REQUIRED_SERVICE_VERSION, + Err(_) => false, } } /// 尝试使用服务启动core pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> { - log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)"); - // logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); + logging!(info, Type::Service, true, "尝试使用现有服务启动核心"); - let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); + let verge_config = Config::verge().await; + let clash_core = verge_config.latest_ref().get_valid_clash_core(); + drop(verge_config); let bin_ext = if cfg!(windows) { ".exe" } else { "" }; - let clash_bin = format!("{clash_core}{bin_ext}"); - let bin_path = current_exe()?.with_file_name(clash_bin); - let bin_path = dirs::path_to_str(&bin_path)?; + let bin_path = current_exe()?.with_file_name(format!("{clash_core}{bin_ext}")); - let config_dir = dirs::app_home_dir()?; - let config_dir = dirs::path_to_str(&config_dir)?; - - let log_path = dirs::service_log_file()?; - let log_path = dirs::path_to_str(&log_path)?; - - let config_file = dirs::path_to_str(config_file)?; - - // 构建启动参数 let payload = serde_json::json!({ "core_type": clash_core, - "bin_path": bin_path, - "config_dir": config_dir, - "config_file": config_file, - "log_file": log_path, + "bin_path": dirs::path_to_str(&bin_path)?, + "config_dir": dirs::path_to_str(&dirs::app_home_dir()?)?, + "config_file": dirs::path_to_str(config_file)?, + "log_file": dirs::path_to_str(&dirs::service_log_file()?)?, }); - // log::info!(target:"app", "启动服务参数: {:?}", payload); - // logging!(info, Type::Service, true, "发送StartClash请求"); + let response = send_ipc_request(IpcCommand::StartClash, payload) + .await + .context("无法连接到Clash Verge Service")?; - // 使用IPC通信 - match send_ipc_request(IpcCommand::StartClash, payload).await { - Ok(response) => { - /* logging!( - info, - Type::Service, - true, - "收到StartClash响应: success={}, error={:?}", - response.success, - response.error - ); */ - - if !response.success { - let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string()); - logging!(error, Type::Service, true, "启动核心失败: {}", err_msg); - bail!(err_msg); - } - - // 添加对嵌套JSON结构的处理 - if let Some(data) = &response.data - && let Some(code) = data.get("code") - { - let code_value = code.as_u64().unwrap_or(1); - let msg = data - .get("msg") - .and_then(|m| m.as_str()) - .unwrap_or("未知错误"); - - if code_value != 0 { - logging!( - error, - Type::Service, - true, - "启动核心返回错误: code={}, msg={}", - code_value, - msg - ); - bail!("启动核心失败: {}", msg); - } - } - - logging!(info, Type::Service, true, "服务成功启动核心"); - Ok(()) - } - Err(e) => { - logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e); - bail!("无法连接到Clash Verge Service: {}", e) - } + if !response.success { + let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string()); + bail!(err_msg); } + + if let Some(data) = &response.data + && let Some(code) = data.get("code").and_then(|c| c.as_u64()) + && code != 0 + { + let msg = data + .get("msg") + .and_then(|m| m.as_str()) + .unwrap_or("未知错误"); + bail!("启动核心失败: {}", msg); + } + + logging!(info, Type::Service, true, "服务成功启动核心"); + Ok(()) } // 以服务启动core pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { - log::info!(target: "app", "正在尝试通过服务启动核心"); + logging!(info, Type::Service, true, "正在尝试通过服务启动核心"); - // 先检查服务版本,不受冷却期限制 - let version_check = match check_service_version().await { - Ok(version) => { - log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}"); - - if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() { - log::warn!(target: "app", "服务版本不匹配,需要重装"); - false - } else { - log::info!(target: "app", "服务版本匹配"); - true - } - } - Err(err) => { - log::warn!(target: "app", "无法获取服务版本: {err}"); - false - } - }; - - if version_check && is_service_available().await.is_ok() { - log::info!(target: "app", "服务已在运行且版本匹配,尝试使用"); - return start_with_existing_service(config_file).await; - } - - if !version_check { - log::info!(target: "app", "服务版本不匹配,尝试重装"); - - let service_state = ServiceState::get().await; - if !service_state.can_reinstall() { - log::warn!(target: "app", "由于限制无法重装服务"); - if let Ok(()) = start_with_existing_service(config_file).await { - log::info!(target: "app", "尽管版本不匹配,但成功启动了服务"); - return Ok(()); - } - bail!("服务版本不匹配且无法重装,启动失败"); - } - - log::info!(target: "app", "开始重装服务"); - if let Err(err) = reinstall_service().await { - log::warn!(target: "app", "服务重装失败: {err}"); - bail!("Failed to reinstall service: {}", err); - } - - log::info!(target: "app", "服务重装成功,尝试启动"); - return start_with_existing_service(config_file).await; - } - - // 检查服务状态 - match check_ipc_service_status().await { - Ok(_) => { - log::info!(target: "app", "服务可用但未运行核心,尝试启动"); - if let Ok(()) = start_with_existing_service(config_file).await { - return Ok(()); - } - } - Err(err) => { - log::warn!(target: "app", "服务检查失败: {err}"); - } - } - - // 服务不可用或启动失败,检查是否需要重装 if check_service_needs_reinstall().await { - log::info!(target: "app", "服务需要重装"); - - if let Err(err) = reinstall_service().await { - log::warn!(target: "app", "服务重装失败: {err}"); - bail!("Failed to reinstall service: {}", err); - } - - log::info!(target: "app", "服务重装完成,尝试启动核心"); - start_with_existing_service(config_file).await - } else { - log::warn!(target: "app", "服务不可用且无法重装"); - bail!("Service is not available and cannot be reinstalled at this time") + reinstall_service().await?; } + + logging!(info, Type::Service, true, "服务已运行且版本匹配,直接使用"); + start_with_existing_service(config_file).await } /// 通过服务停止core @@ -909,7 +453,9 @@ pub(super) async fn stop_core_by_service() -> Result<()> { .context("无法连接到Clash Verge Service")?; if !response.success { - bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string())); + let err_msg = response.error.unwrap_or_else(|| "停止核心失败".to_string()); + logging!(error, Type::Service, true, "停止核心失败: {}", err_msg); + bail!(err_msg); } if let Some(data) = &response.data @@ -934,54 +480,105 @@ pub(super) async fn stop_core_by_service() -> Result<()> { } } + logging!(info, Type::Service, true, "服务成功停止核心"); Ok(()) } /// 检查服务是否正在运行 pub async fn is_service_available() -> Result<()> { - logging!(info, Type::Service, true, "开始检查服务是否正在运行"); + check_service_version().await?; + Ok(()) +} - match check_ipc_service_status().await { - Ok(resp) => { - if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() { - logging!(info, Type::Service, true, "服务正在运行"); - Ok(()) - } else { - logging!( - warn, - Type::Service, - true, - "服务未正常运行: code={}, msg={}", - resp.code, - resp.msg - ); - Ok(()) +impl ServiceManager { + pub fn default() -> Self { + Self(ServiceStatus::Unavailable("Need Checks".into())) + } + + pub fn current(&self) -> ServiceStatus { + self.0.clone() + } + + pub async fn refresh(&mut self) -> Result<()> { + let status = self.check_service_comprehensive().await; + logging_error!( + Type::Service, + true, + self.handle_service_status(&status).await + ); + self.0 = status; + Ok(()) + } + + /// 综合服务状态检查(一次性完成所有检查) + pub async fn check_service_comprehensive(&self) -> ServiceStatus { + match is_service_available().await { + Ok(_) => { + logging!(info, Type::Service, true, "服务当前可用,检查是否需要重装"); + if check_service_needs_reinstall().await { + logging!(info, Type::Service, true, "服务需要重装且允许重装"); + ServiceStatus::NeedsReinstall + } else { + ServiceStatus::Ready + } + } + Err(err) => { + logging!(warn, Type::Service, true, "服务不可用,检查安装状态"); + ServiceStatus::Unavailable(err.to_string()) } } - Err(err) => { - logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err); - Err(err) + } + + /// 根据服务状态执行相应操作 + pub async fn handle_service_status(&mut self, status: &ServiceStatus) -> Result<()> { + match status { + ServiceStatus::Ready => { + logging!(info, Type::Service, true, "服务就绪,直接启动"); + Ok(()) + } + ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => { + logging!(info, Type::Service, true, "服务需要重装,执行重装流程"); + reinstall_service().await?; + self.0 = ServiceStatus::Ready; + Ok(()) + } + ServiceStatus::ForceReinstallRequired => { + logging!( + info, + Type::Service, + true, + "服务需要强制重装,执行强制重装流程" + ); + force_reinstall_service().await?; + self.0 = ServiceStatus::Ready; + Ok(()) + } + ServiceStatus::InstallRequired => { + logging!(info, Type::Service, true, "需要安装服务,执行安装流程"); + install_service().await?; + self.0 = ServiceStatus::Ready; + Ok(()) + } + ServiceStatus::UninstallRequired => { + logging!(info, Type::Service, true, "服务需要卸载,执行卸载流程"); + uninstall_service().await?; + self.0 = ServiceStatus::Unavailable("Service Uninstalled".into()); + Ok(()) + } + ServiceStatus::Unavailable(reason) => { + logging!( + info, + Type::Service, + true, + "服务不可用: {},将使用Sidecar模式", + reason + ); + self.0 = ServiceStatus::Unavailable(reason.clone()); + Err(anyhow::anyhow!("服务不可用: {}", reason)) + } } } } -/// 强制重装服务(UI修复按钮) -pub async fn force_reinstall_service() -> Result<()> { - log::info!(target: "app", "用户请求强制重装服务"); - - let service_state = ServiceState::default(); - service_state.save().await?; - - log::info!(target: "app", "已重置服务状态,开始执行重装"); - - match reinstall_service().await { - Ok(()) => { - log::info!(target: "app", "服务重装成功"); - Ok(()) - } - Err(err) => { - log::error!(target: "app", "强制重装服务失败: {err}"); - bail!("强制重装服务失败: {}", err) - } - } -} +pub static SERVICE_MANAGER: Lazy> = + Lazy::new(|| Mutex::new(ServiceManager::default())); diff --git a/src-tauri/src/core/service_ipc.rs b/src-tauri/src/core/service_ipc.rs index bdf50f562..c32b9de42 100644 --- a/src-tauri/src/core/service_ipc.rs +++ b/src-tauri/src/core/service_ipc.rs @@ -1,11 +1,15 @@ -#[cfg(target_os = "windows")] -use crate::process::AsyncHandler; use crate::{logging, utils::logging::Type}; use anyhow::{Context, Result, bail}; +use backoff::{Error as BackoffError, ExponentialBackoff}; use hmac::{Hmac, Mac}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(unix)] +use tokio::net::UnixStream; +#[cfg(windows)] +use tokio::net::windows::named_pipe::ClientOptions; const IPC_SOCKET_NAME: &str = if cfg!(windows) { r"\\.\pipe\clash-verge-service" @@ -112,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result { Ok(expected_signature == response.signature) } -// IPC连接管理-win -#[cfg(target_os = "windows")] +fn create_backoff_strategy() -> ExponentialBackoff { + ExponentialBackoff { + initial_interval: Duration::from_millis(50), + max_interval: Duration::from_secs(1), + max_elapsed_time: Some(Duration::from_secs(3)), + multiplier: 1.5, + ..Default::default() + } +} + pub async fn send_ipc_request( command: IpcCommand, payload: serde_json::Value, ) -> Result { - use std::{ - ffi::CString, - fs::File, - io::{Read, Write}, - os::windows::io::{FromRawHandle, RawHandle}, - ptr, - }; - use winapi::um::{ - fileapi::{CreateFileA, OPEN_EXISTING}, - handleapi::INVALID_HANDLE_VALUE, - winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}, - }; - - logging!(info, Type::Service, true, "正在连接服务 (Windows)..."); - let command_type = format!("{command:?}"); - let request = match create_signed_request(command, payload) { - Ok(req) => req, - Err(e) => { - logging!(error, Type::Service, true, "创建签名请求失败: {}", e); - return Err(e); + let operation = || async { + match send_ipc_request_internal(command.clone(), payload.clone()).await { + Ok(response) => Ok(response), + Err(e) => { + logging!( + warn, + Type::Service, + true, + "IPC请求失败,准备重试: 命令={}, 错误={}", + command_type, + e + ); + Err(BackoffError::transient(e)) + } } }; - let request_json = serde_json::to_string(&request)?; - - let result = AsyncHandler::spawn_blocking(move || -> Result { - let c_pipe_name = match CString::new(IPC_SOCKET_NAME) { - Ok(name) => name, - Err(e) => { - logging!(error, Type::Service, true, "创建CString失败: {}", e); - return Err(anyhow::anyhow!("创建CString失败: {}", e)); - } - }; - - let handle = unsafe { - CreateFileA( - c_pipe_name.as_ptr(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - ptr::null_mut(), - OPEN_EXISTING, - 0, - ptr::null_mut(), - ) - }; - - if handle == INVALID_HANDLE_VALUE { - let error = std::io::Error::last_os_error(); + match backoff::future::retry(create_backoff_strategy(), operation).await { + Ok(response) => { + // logging!( + // info, + // Type::Service, + // true, + // "IPC请求成功: 命令={}, 成功={}", + // command_type, + // response.success + // ); + Ok(response) + } + Err(e) => { logging!( error, Type::Service, true, - "连接到服务命名管道失败: {}", - error + "IPC请求最终失败,重试已耗尽: 命令={}, 错误={}", + command_type, + e ); - return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error)); + Err(anyhow::anyhow!("IPC请求重试失败: {}", e)) } + } +} - let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) }; - logging!(info, Type::Service, true, "服务连接成功 (Windows)"); +// 内部IPC请求实现(不带重试) +async fn send_ipc_request_internal( + command: IpcCommand, + payload: serde_json::Value, +) -> Result { + #[cfg(target_os = "windows")] + { + send_ipc_request_windows(command, payload).await + } + #[cfg(target_family = "unix")] + { + send_ipc_request_unix(command, payload).await + } +} - let request_bytes = request_json.as_bytes(); - let len_bytes = (request_bytes.len() as u32).to_be_bytes(); +// IPC连接管理-win +#[cfg(target_os = "windows")] +async fn send_ipc_request_windows( + command: IpcCommand, + payload: serde_json::Value, +) -> Result { + let request = create_signed_request(command, payload)?; + let request_json = serde_json::to_string(&request)?; + let request_bytes = request_json.as_bytes(); + let len_bytes = (request_bytes.len() as u32).to_be_bytes(); - if let Err(e) = pipe.write_all(&len_bytes) { - logging!(error, Type::Service, true, "写入请求长度失败: {}", e); - return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); + let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) { + Ok(p) => p, + Err(e) => { + logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e); + return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e)); } + }; - if let Err(e) = pipe.write_all(request_bytes) { - logging!(error, Type::Service, true, "写入请求内容失败: {}", e); - return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); - } + logging!(info, Type::Service, true, "服务连接成功 (Windows)"); - if let Err(e) = pipe.flush() { - logging!(error, Type::Service, true, "刷新管道失败: {}", e); - return Err(anyhow::anyhow!("刷新管道失败: {}", e)); - } + pipe.write_all(&len_bytes).await?; + pipe.write_all(request_bytes).await?; + pipe.flush().await?; - let mut response_len_bytes = [0u8; 4]; - if let Err(e) = pipe.read_exact(&mut response_len_bytes) { - logging!(error, Type::Service, true, "读取响应长度失败: {}", e); - return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); - } + let mut response_len_bytes = [0u8; 4]; + pipe.read_exact(&mut response_len_bytes).await?; + let response_len = u32::from_be_bytes(response_len_bytes) as usize; - let response_len = u32::from_be_bytes(response_len_bytes) as usize; + let mut response_bytes = vec![0u8; response_len]; + pipe.read_exact(&mut response_bytes).await?; - let mut response_bytes = vec![0u8; response_len]; - if let Err(e) = pipe.read_exact(&mut response_bytes) { - logging!(error, Type::Service, true, "读取响应内容失败: {}", e); - return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); - } + let response: IpcResponse = serde_json::from_slice(&response_bytes) + .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; - let response: IpcResponse = match serde_json::from_slice::(&response_bytes) { - Ok(r) => r, - Err(e) => { - logging!(error, Type::Service, true, "服务响应解析失败: {}", e); - return Err(anyhow::anyhow!("解析响应失败: {}", e)); - } - }; + if !verify_response_signature(&response)? { + logging!(error, Type::Service, true, "服务响应签名验证失败"); + bail!("服务响应签名验证失败"); + } - match verify_response_signature(&response) { - Ok(valid) => { - if !valid { - logging!(error, Type::Service, true, "服务响应签名验证失败"); - bail!("服务响应签名验证失败"); - } - } - Err(e) => { - logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); - return Err(e); - } - } - - logging!( - info, - Type::Service, - true, - "IPC请求完成: 命令={}, 成功={}", - command_type, - response.success - ); - Ok(response) - }) - .await??; - - Ok(result) + Ok(response) } // IPC连接管理-unix #[cfg(target_family = "unix")] -pub async fn send_ipc_request( +async fn send_ipc_request_unix( command: IpcCommand, payload: serde_json::Value, ) -> Result { - use std::os::unix::net::UnixStream; - - logging!(info, Type::Service, true, "正在连接服务 (Unix)..."); - - let command_type = format!("{command:?}"); - - let request = match create_signed_request(command, payload) { - Ok(req) => req, - Err(e) => { - logging!(error, Type::Service, true, "创建签名请求失败: {}", e); - return Err(e); - } - }; - + let request = create_signed_request(command, payload)?; let request_json = serde_json::to_string(&request)?; - let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) { - Ok(s) => { - logging!(info, Type::Service, true, "服务连接成功 (Unix)"); - s - } + let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await { + Ok(s) => s, Err(e) => { logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e); return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e)); @@ -285,58 +253,97 @@ pub async fn send_ipc_request( let request_bytes = request_json.as_bytes(); let len_bytes = (request_bytes.len() as u32).to_be_bytes(); - if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) { - logging!(error, Type::Service, true, "写入请求长度失败: {}", e); - return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); - } - - if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) { - logging!(error, Type::Service, true, "写入请求内容失败: {}", e); - return Err(anyhow::anyhow!("写入请求内容失败: {}", e)); - } + stream.write_all(&len_bytes).await?; + stream.write_all(request_bytes).await?; + stream.flush().await?; + // 读取响应长度 let mut response_len_bytes = [0u8; 4]; - if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) { - logging!(error, Type::Service, true, "读取响应长度失败: {}", e); - return Err(anyhow::anyhow!("读取响应长度失败: {}", e)); - } - + stream.read_exact(&mut response_len_bytes).await?; let response_len = u32::from_be_bytes(response_len_bytes) as usize; let mut response_bytes = vec![0u8; response_len]; - if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) { - logging!(error, Type::Service, true, "读取响应内容失败: {}", e); - return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); + stream.read_exact(&mut response_bytes).await?; + + let response: IpcResponse = serde_json::from_slice(&response_bytes) + .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; + + if !verify_response_signature(&response)? { + logging!(error, Type::Service, true, "服务响应签名验证失败"); + bail!("服务响应签名验证失败"); } - let response: IpcResponse = match serde_json::from_slice::(&response_bytes) { - Ok(r) => r, - Err(e) => { - logging!(error, Type::Service, true, "服务响应解析失败: {}", e,); - return Err(anyhow::anyhow!("解析响应失败: {}", e)); - } - }; - - match verify_response_signature(&response) { - Ok(valid) => { - if !valid { - logging!(error, Type::Service, true, "服务响应签名验证失败"); - bail!("服务响应签名验证失败"); - } - } - Err(e) => { - logging!(error, Type::Service, true, "验证响应签名时出错: {}", e); - return Err(e); - } - } - - logging!( - info, - Type::Service, - true, - "IPC请求完成: 命令={}, 成功={}", - command_type, - response.success - ); Ok(response) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_signed_request() { + let command = IpcCommand::GetVersion; + let payload = serde_json::json!({"test": "data"}); + + let result = create_signed_request(command, payload); + assert!(result.is_ok()); + + if let Ok(request) = result { + assert!(!request.id.is_empty()); + assert!(!request.signature.is_empty()); + assert_eq!(request.command, IpcCommand::GetVersion); + } + } + + #[test] + fn test_sign_and_verify_message() { + let test_message = "test message for signing"; + + let signature_result = sign_message(test_message); + assert!(signature_result.is_ok()); + + if let Ok(signature) = signature_result { + assert!(!signature.is_empty()); + + // 测试相同消息产生相同签名 + if let Ok(signature2) = sign_message(test_message) { + assert_eq!(signature, signature2); + } + } + } + + #[test] + fn test_verify_response_signature() { + let response = IpcResponse { + id: "test-id".to_string(), + success: true, + data: Some(serde_json::json!({"result": "success"})), + error: None, + signature: String::new(), + }; + + // 创建正确的签名 + let verification_response = IpcResponse { + id: response.id.clone(), + success: response.success, + data: response.data.clone(), + error: response.error.clone(), + signature: String::new(), + }; + + if let Ok(message) = serde_json::to_string(&verification_response) + && let Ok(correct_signature) = sign_message(&message) + { + let signed_response = IpcResponse { + signature: correct_signature, + ..response + }; + + let verification_result = verify_response_signature(&signed_response); + assert!(verification_result.is_ok()); + if let Ok(is_valid) = verification_result { + assert!(is_valid); + } + } + } +} diff --git a/src-tauri/src/ipc/general.rs b/src-tauri/src/ipc/general.rs index e4bdebf21..1386ac1ec 100644 --- a/src-tauri/src/ipc/general.rs +++ b/src-tauri/src/ipc/general.rs @@ -30,7 +30,8 @@ pub struct IpcManager { } impl IpcManager { - fn new() -> Self { + pub fn new() -> Self { + logging!(info, Type::Ipc, true, "Creating new IpcManager instance"); let ipc_path_buf = ipc_path().unwrap_or_else(|e| { logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path @@ -51,9 +52,6 @@ impl IpcManager { } } -// Use singleton macro with logging -singleton_with_logging!(IpcManager, INSTANCE, "IpcManager"); - impl IpcManager { pub async fn request( &self, @@ -367,3 +365,6 @@ impl IpcManager { // 日志相关功能已迁移到 logs.rs 模块,使用流式处理 } + +// Use singleton macro with logging +singleton_with_logging!(IpcManager, INSTANCE, "IpcManager"); diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 941e1d845..8eefe6fe0 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -1,5 +1,5 @@ use crate::{ - cache::Cache, + cache::CacheProxy, config::Config, core::{handle, timer::Timer, tray::Tray}, log_err, logging, @@ -183,7 +183,7 @@ pub async fn entry_lightweight_mode() -> bool { // 回到 In set_state(LightweightState::In); - Cache::global().clean_default_keys(); + CacheProxy::global().clean_default_keys(); true } diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 99364e57b..6118b934e 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -3,7 +3,9 @@ use tauri::AppHandle; use crate::{ config::Config, - core::{CoreManager, Timer, handle, hotkey::Hotkey, sysopt, tray::Tray}, + core::{ + CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray, + }, logging, logging_error, module::lightweight::auto_lightweight_mode_init, process::AsyncHandler, @@ -38,6 +40,8 @@ pub fn resolve_setup_async() { ); AsyncHandler::spawn(|| async { + init_service_manager().await; + futures::join!( init_work_config(), init_resources(), @@ -185,6 +189,15 @@ pub(super) async fn init_verge_config() { logging_error!(Type::Setup, true, Config::init_config().await); } +pub(super) async fn init_service_manager() { + logging!(info, Type::Setup, true, "Initializing service manager..."); + logging_error!( + Type::Setup, + true, + SERVICE_MANAGER.lock().await.refresh().await + ); +} + pub(super) async fn init_core_manager() { logging!(info, Type::Setup, true, "Initializing core manager..."); logging_error!(Type::Setup, true, CoreManager::global().init().await); diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index b8926aa11..e2f4839b7 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import React, { useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { SettingsRounded, @@ -19,8 +19,8 @@ import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { useSystemState } from "@/hooks/use-system-state"; import { showNotice } from "@/services/noticeService"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; -import { uninstallService, restartCore, stopCore } from "@/services/cmds"; import { useLockFn } from "ahooks"; +import { useServiceUninstaller } from "@/hooks/useServiceUninstaller"; interface ProxySwitchProps { label?: string; @@ -28,10 +28,83 @@ interface ProxySwitchProps { noRightPadding?: boolean; } +interface SwitchRowProps { + label: string; + active: boolean; + disabled?: boolean; + infoTitle: string; + onInfoClick?: () => void; + extraIcons?: React.ReactNode; + onToggle: (value: boolean) => Promise; + onError?: (err: Error) => void; + highlight?: boolean; +} + /** - * 可复用的代理控制开关组件 - * 包含 Tun Mode 和 System Proxy 的开关功能 + * 抽取的子组件:统一的开关 UI */ +const SwitchRow = ({ + label, + active, + disabled, + infoTitle, + onInfoClick, + extraIcons, + onToggle, + onError, + highlight, +}: SwitchRowProps) => { + const theme = useTheme(); + return ( + + + {active ? ( + + ) : ( + + )} + + {label} + + + {extraIcons} + + + v} + onGuard={onToggle} + > + + + + ); +}; + const ProxyControlSwitches = ({ label, onError, @@ -39,215 +112,115 @@ const ProxyControlSwitches = ({ }: ProxySwitchProps) => { const { t } = useTranslation(); const { verge, mutateVerge, patchVerge } = useVerge(); - const theme = useTheme(); const { installServiceAndRestartCore } = useServiceInstaller(); - + const { uninstallServiceAndRestartCore } = useServiceUninstaller(); const { actualState: systemProxyActualState, toggleSystemProxy } = useSystemProxyState(); - - const { isAdminMode, isServiceMode, mutateRunningMode } = useSystemState(); - - const isTunAvailable = isServiceMode || isAdminMode; + const { + isServiceMode, + isTunModeAvailable, + mutateRunningMode, + mutateServiceOk, + } = useSystemState(); const sysproxyRef = useRef(null); const tunRef = useRef(null); const { enable_tun_mode, enable_system_proxy } = verge ?? {}; - // 确定当前显示哪个开关 - const isSystemProxyMode = label === t("System Proxy") || !label; - const isTunMode = label === t("Tun Mode"); + const showErrorNotice = useCallback( + (msg: string) => showNotice("error", t(msg)), + [t], + ); - const onSwitchFormat = ( - _e: React.ChangeEvent, - value: boolean, - ) => value; - const onChangeData = (patch: Partial) => { - mutateVerge({ ...verge, ...patch }, false); + const handleTunToggle = async (value: boolean) => { + if (!isTunModeAvailable) { + const msg = "TUN requires Service Mode or Admin Mode"; + showErrorNotice(msg); + throw new Error(t(msg)); + } + mutateVerge({ ...verge, enable_tun_mode: value }, false); + await patchVerge({ enable_tun_mode: value }); }; - // 安装系统服务 - const onInstallService = installServiceAndRestartCore; - - // 卸载系统服务 - const onUninstallService = useLockFn(async () => { + const onInstallService = useLockFn(async () => { try { - showNotice("info", t("Stopping Core...")); - await stopCore(); - showNotice("info", t("Uninstalling Service...")); - await uninstallService(); - showNotice("success", t("Service Uninstalled Successfully")); - showNotice("info", t("Restarting Core...")); - await restartCore(); + await installServiceAndRestartCore(); await mutateRunningMode(); - } catch (err: unknown) { - showNotice("error", (err as Error).message || err?.toString()); - try { - showNotice("info", t("Try running core as Sidecar...")); - await restartCore(); - await mutateRunningMode(); - } catch (e: unknown) { - showNotice("error", (e as Error)?.message || e?.toString()); - } + await mutateServiceOk(); + } catch (err) { + showNotice("error", (err as Error).message || String(err)); } }); + const onUninstallService = useLockFn(async () => { + try { + await uninstallServiceAndRestartCore(); + await mutateRunningMode(); + await mutateServiceOk(); + } catch (err) { + showNotice("error", (err as Error).message || String(err)); + } + }); + + const isSystemProxyMode = label === t("System Proxy") || !label; + const isTunMode = label === t("Tun Mode"); + return ( - - {/* 仅显示当前选中的开关 */} + {isSystemProxyMode && ( - - - {systemProxyActualState ? ( - - ) : ( - - )} - - - {t("System Proxy")} - - sysproxyRef.current?.open()} - sx={{ ml: 1 }} - /> - - - toggleSystemProxy(e)} - > - - - + sysproxyRef.current?.open()} + onToggle={(value) => toggleSystemProxy(value)} + onError={onError} + highlight={enable_system_proxy} + /> )} {isTunMode && ( - - - {enable_tun_mode ? ( - - ) : ( - - )} - - - {t("Tun Mode")} - - tunRef.current?.open()} - sx={{ ml: 1 }} - /> - - {!isTunAvailable && ( - - )} - - {!isTunAvailable && ( - - )} - - {isServiceMode && ( - - )} - - - { - if (!isTunAvailable) { - showNotice( - "error", - t("TUN requires Service Mode or Admin Mode"), - ); - return Promise.reject( - new Error(t("TUN requires Service Mode or Admin Mode")), - ); - } - onChangeData({ enable_tun_mode: e }); - }} - onGuard={(e) => { - if (!isTunAvailable) { - showNotice( - "error", - t("TUN requires Service Mode or Admin Mode"), - ); - return Promise.reject( - new Error(t("TUN requires Service Mode or Admin Mode")), - ); - } - return patchVerge({ enable_tun_mode: e }); - }} - > - - - + tunRef.current?.open()} + onToggle={handleTunToggle} + onError={onError} + disabled={!isServiceMode} + highlight={!!enable_tun_mode} + extraIcons={ + <> + {!isServiceMode && ( + + )} + {!isServiceMode ? ( + + ) : ( + + )} + + } + /> )} - {/* 引用对话框组件 */} diff --git a/src/hooks/use-system-state.ts b/src/hooks/use-system-state.ts index b8ed5c4af..056028c73 100644 --- a/src/hooks/use-system-state.ts +++ b/src/hooks/use-system-state.ts @@ -15,6 +15,8 @@ export function useSystemState() { revalidateOnFocus: false, }, ); + const isSidecarMode = runningMode === "Sidecar"; + const isServiceMode = runningMode === "Service"; // 获取管理员状态 const { data: isAdminMode = false } = useSWR("isAdmin", isAdmin, { @@ -22,24 +24,32 @@ export function useSystemState() { revalidateOnFocus: false, }); - // 获取系统服务状态 - const isServiceMode = runningMode === "Service"; - const { data: isServiceOk = false } = useSWR( + const { data: isServiceOk = false, mutate: mutateServiceOk } = useSWR( "isServiceAvailable", isServiceAvailable, { suspense: false, revalidateOnFocus: false, - isPaused: () => !isServiceMode, // 仅在 Service 模式下请求 + onSuccess: (data) => { + console.log("[useSystemState] 服务状态更新:", data); + }, + onError: (error) => { + console.error("[useSystemState] 服务状态检查失败:", error); + }, + isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求 }, ); + const isTunModeAvailable = isAdminMode || isServiceOk; + return { runningMode, isAdminMode, - isSidecarMode: runningMode === "Sidecar", - isServiceMode: runningMode === "Service", + isSidecarMode, + isServiceMode, isServiceOk, + isTunModeAvailable: isTunModeAvailable, mutateRunningMode, + mutateServiceOk, }; } diff --git a/src/hooks/useServiceInstaller.ts b/src/hooks/useServiceInstaller.ts index 6dd5ba8c9..12e04e16c 100644 --- a/src/hooks/useServiceInstaller.ts +++ b/src/hooks/useServiceInstaller.ts @@ -1,121 +1,35 @@ -import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; +import { installService, restartCore } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -import { - installService, - isServiceAvailable, - restartCore, -} from "@/services/cmds"; -import { useSystemState } from "@/hooks/use-system-state"; -import { mutate } from "swr"; +import { t } from "i18next"; +import { useCallback } from "react"; -export function useServiceInstaller() { - const { t } = useTranslation(); - const { mutateRunningMode } = useSystemState(); - - const installServiceAndRestartCore = useLockFn(async () => { - try { - showNotice("info", t("Installing Service...")); - await installService(); - showNotice("success", t("Service Installed Successfully")); - - showNotice("info", t("Waiting for service to be ready...")); - let serviceReady = false; - for (let i = 0; i < 5; i++) { - try { - // 等待1秒再检查 - await new Promise((resolve) => setTimeout(resolve, 1000)); - const isAvailable = await isServiceAvailable(); - if (isAvailable) { - serviceReady = true; - mutate("isServiceAvailable", true, false); - break; - } - // 最后一次尝试不显示重试信息 - if (i < 4) { - showNotice( - "info", - t("Service not ready, retrying attempt {count}/{total}...", { - count: i + 1, - total: 5, - }), - ); - } - } catch (error) { - console.error(t("Error checking service status:"), error); - if (i < 4) { - showNotice( - "error", - t( - "Failed to check service status, retrying attempt {count}/{total}...", - { - count: i + 1, - total: 5, - }, - ), - ); - } - } - } - - if (!serviceReady) { - showNotice( - "info", - t( - "Service did not become ready after attempts. Proceeding with core restart.", - ), - ); - } - - showNotice("info", t("Restarting Core...")); - await restartCore(); - - // 核心重启后,再次确认并更新相关状态 - await mutateRunningMode(); - const finalServiceStatus = await isServiceAvailable(); - mutate("isServiceAvailable", finalServiceStatus, false); - - if (serviceReady && finalServiceStatus) { - showNotice("success", t("Service is ready and core restarted")); - } else if (finalServiceStatus) { - showNotice("success", t("Core restarted. Service is now available.")); - } else if (serviceReady) { - showNotice( - "info", - t( - "Service was ready, but core restart might have issues or service became unavailable. Please check.", - ), - ); - } else { - showNotice( - "error", - t( - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.", - ), - ); - } - return finalServiceStatus; - } catch (err: any) { - showNotice("error", err.message || err.toString()); - // 尝试性回退或最终操作 - try { - showNotice("info", t("Attempting to restart core as a fallback...")); - await restartCore(); - await mutateRunningMode(); - await isServiceAvailable().then((status) => - mutate("isServiceAvailable", status, false), - ); - } catch (recoveryError: any) { - showNotice( - "error", - t("Fallback core restart also failed: {message}", { - message: recoveryError.message, - }), - ); - } - return false; +const executeWithErrorHandling = async ( + operation: () => Promise, + loadingMessage: string, + successMessage?: string, +) => { + try { + showNotice("info", t(loadingMessage)); + await operation(); + if (successMessage) { + showNotice("success", t(successMessage)); } - }); + } catch (err) { + const msg = (err as Error)?.message || String(err); + showNotice("error", msg); + throw err; + } +}; +export const useServiceInstaller = () => { + const installServiceAndRestartCore = useCallback(async () => { + await executeWithErrorHandling( + () => installService(), + "Installing Service...", + "Service Installed Successfully", + ); + + await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + }, []); return { installServiceAndRestartCore }; -} +}; diff --git a/src/hooks/useServiceUninstaller.ts b/src/hooks/useServiceUninstaller.ts new file mode 100644 index 000000000..f3341eb78 --- /dev/null +++ b/src/hooks/useServiceUninstaller.ts @@ -0,0 +1,41 @@ +import { restartCore, stopCore, uninstallService } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; +import { t } from "i18next"; +import { useSystemState } from "./use-system-state"; +import { useCallback } from "react"; + +const executeWithErrorHandling = async ( + operation: () => Promise, + loadingMessage: string, + successMessage?: string, +) => { + try { + showNotice("info", t(loadingMessage)); + await operation(); + if (successMessage) { + showNotice("success", t(successMessage)); + } + } catch (err) { + const msg = (err as Error)?.message || String(err); + showNotice("error", msg); + throw err; + } +}; + +export const useServiceUninstaller = () => { + const { mutateRunningMode, mutateServiceOk } = useSystemState(); + + const uninstallServiceAndRestartCore = useCallback(async () => { + await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); + + await executeWithErrorHandling( + () => uninstallService(), + "Uninstalling Service...", + "Service Uninstalled Successfully", + ); + + await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + }, [mutateRunningMode, mutateServiceOk]); + + return { uninstallServiceAndRestartCore }; +}; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index b9545518e..22e34287a 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -208,6 +208,9 @@ const Layout = () => { mutate("getVergeConfig"); mutate("getSystemProxy"); mutate("getAutotemProxy"); + // 运行模式变更时也需要刷新相关状态 + mutate("getRunningMode"); + mutate("isServiceAvailable"); }), addListener("verge://notice-message", ({ payload }) =>