refactor: clash-verge-service management (#4674)

* refactor: clash-verge-service management

* fix: correct service state checks in ProxyControlSwitches component
refactor: improve logging in service state update functions

* fix: add missing async handler for Windows and adjust logging import for macOS

* fix: streamline logging imports and add missing async handler for Windows

* refactor: remove unused useServiceStateSync hook and update imports in _layout

* refactor: remove unused useServiceStateSync import and clean up code in ProxyControlSwitches and _layout

* refactor: simplify service status checks and reduce wait time in useServiceInstaller hook

* refactor: remove unnecessary logging statements in service checks and IPC connection

* refactor: extract SwitchRow component for better code organization and readability

* refactor: enhance service state management and update related mutations in layout

* refactor: streamline core stopping logic and improve IPC connection logging

* refactor: consolidate service uninstallation logic and improve error handling

* fix: simplify conditional statements in CoreManager and service functions

* feat: add backoff dependency and implement retry strategy for IPC requests

* refactor: remove redundant Windows conditional and improve error handling in IPC tests

* test: improve error handling in IPC tests for message signing and verification

* fix: adjust IPC backoff retry parameters

* refactor: Remove service state tracking and related logic from service management

* feat: Enhance service status handling with logging and running mode updates

* fix: Improve service status handling with enhanced error logging

* fix: Ensure proper handling of service operations with error propagation

* refactor: Simplify service operation execution and enhance service status handling

* fix: Improve error message formatting in service operation execution and simplify service status retrieval

* refactor: Replace Cache with CacheProxy in multiple modules and update CacheEntry to be generic

* fix: Remove unnecessary success message from config validation

* refactor: Comment out logging statements in service version check and IPC request handling
This commit is contained in:
Tunglies
2025-09-17 22:59:02 +08:00
committed by GitHub
parent 6724f1ae35
commit c207516b47
20 changed files with 781 additions and 1386 deletions

View File

@@ -1,9 +1,13 @@
## v2.4.3
### 🚀 性能优化
- 重构并简化服务模式启动检测流程,消除重复检测
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- macOS intel Mihomo 兼容性
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组

15
src-tauri/Cargo.lock generated
View File

@@ -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",

View File

@@ -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]

View File

@@ -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<Value>,
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant,
}
pub struct Cache {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry>>>>,
pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
}
impl Cache {
impl<T> Cache<T> {
fn new() -> Self {
Cache {
map: DashMap::new(),
@@ -25,10 +28,11 @@ impl Cache {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
where
F: Fn() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Value> + Send + 'static,
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = T> + 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<Result<String>>;
pub type CacheProxy = Cache<Value>;
singleton!(Cache<Value>, PROXY_INSTANCE);
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);

View File

@@ -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<serde_json::Value> {
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
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<serde_json::Value> {
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
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
}

View File

@@ -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<serde_json::Value> {
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<serde_json::Value> {
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
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<serde_json::Value> {
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 {

View File

@@ -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<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), E>>,
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]

View File

@@ -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) => {

View File

@@ -203,9 +203,6 @@ pub struct IVerge {
/// 启用外部控制器
pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
#[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<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
impl From<IVerge> for IVergeResponse {
@@ -664,7 +658,6 @@ impl From<IVerge> 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,
}
}
}

View File

@@ -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,7 +761,6 @@ 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))
@@ -777,7 +774,6 @@ impl CoreManager {
);
}
}
});
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);

View File

@@ -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<String>, // 上次错误信息
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<ResponseBody>,
}
#[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<JsonResponse> {
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::<ResponseBody>(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::<JsonResponse>(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<String> {
async fn check_service_version() -> Result<String> {
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!({});
// logging!(debug, Type::Service, true, "发送GetVersion请求");
let response = send_ipc_request(IpcCommand::GetVersion, payload).await?;
match send_ipc_request(IpcCommand::GetVersion, payload).await {
Ok(response) => {
/* logging!(
debug,
Type::Service,
true,
"收到GetVersion响应: success={}, error={:?}",
response.success,
response.error
); */
let data = response
.data
.ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?;
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(nested_data) = data.get("data") {
if let Some(version) = nested_data.get("version")
&& let Some(version_str) = version.as_str()
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_str);
return Ok(version_str.to_string());
}
logging!(
error,
Type::Service,
true,
"嵌套数据中没有version字段: {:?}",
nested_data
);
} else {
// 兼容旧格式
match serde_json::from_value::<VersionResponse>(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)
// logging!(info, Type::Service, true, "获取到服务版本: {}", version);
return Ok(version.to_string());
}
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请求");
// 使用IPC通信
match send_ipc_request(IpcCommand::StartClash, payload).await {
Ok(response) => {
/* logging!(
info,
Type::Service,
true,
"收到StartClash响应: success={}, error={:?}",
response.success,
response.error
); */
let response = send_ipc_request(IpcCommand::StartClash, payload)
.await
.context("无法连接到Clash Verge Service")?;
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 Some(code) = data.get("code").and_then(|c| c.as_u64())
&& code != 0
{
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)
}
}
}
// 以服务启动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);
reinstall_service().await?;
}
log::info!(target: "app", "服务重装完成,尝试启动核心");
logging!(info, Type::Service, true, "服务已运行且版本匹配,直接使用");
start_with_existing_service(config_file).await
} else {
log::warn!(target: "app", "服务不可用且无法重装");
bail!("Service is not available and cannot be reinstalled at this time")
}
}
/// 通过服务停止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, "开始检查服务是否正在运行");
match check_ipc_service_status().await {
Ok(resp) => {
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
logging!(info, Type::Service, true, "服务正在运行");
check_service_version().await?;
Ok(())
} else {
logging!(
warn,
}
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,
"服务未正常运行: code={}, msg={}",
resp.code,
resp.msg
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!(error, Type::Service, true, "检查服务运行状态失败: {}", err);
Err(err)
logging!(warn, Type::Service, true, "服务不可用,检查安装状态");
ServiceStatus::Unavailable(err.to_string())
}
}
}
/// 根据服务状态执行相应操作
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<Mutex<ServiceManager>> =
Lazy::new(|| Mutex::new(ServiceManager::default()));

View File

@@ -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<bool> {
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<IpcResponse> {
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,
let operation = || async {
match send_ipc_request_internal(command.clone(), payload.clone()).await {
Ok(response) => Ok(response),
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return 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<IpcResponse> {
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));
match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
// logging!(
// info,
// Type::Service,
// true,
// "IPC请求成功: 命令={}, 成功={}",
// command_type,
// response.success
// );
Ok(response)
}
};
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();
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<IpcResponse> {
#[cfg(target_os = "windows")]
{
send_ipc_request_windows(command, payload).await
}
#[cfg(target_family = "unix")]
{
send_ipc_request_unix(command, payload).await
}
}
// IPC连接管理-win
#[cfg(target_os = "windows")]
async fn send_ipc_request_windows(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
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));
}
if let Err(e) = pipe.write_all(request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
if let Err(e) = pipe.flush() {
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
}
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 response_len = u32::from_be_bytes(response_len_bytes) as usize;
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 = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
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));
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
pipe.write_all(&len_bytes).await?;
pipe.write_all(request_bytes).await?;
pipe.flush().await?;
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 mut response_bytes = vec![0u8; response_len];
pipe.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!("服务响应签名验证失败");
}
}
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)
}
// 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<IpcResponse> {
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 = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
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))?;
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
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);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
#[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);
}
}
}
}

View File

@@ -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");

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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<void>;
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 (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 1,
pr: 2,
borderRadius: 1.5,
bgcolor: highlight
? alpha(theme.palette.success.main, 0.07)
: "transparent",
opacity: disabled ? 0.6 : 1,
transition: "background-color 0.3s",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
{active ? (
<PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
) : (
<PauseCircleOutlineRounded sx={{ color: "text.disabled", mr: 1 }} />
)}
<Typography
variant="subtitle1"
sx={{ fontWeight: 500, fontSize: "15px" }}
>
{label}
</Typography>
<TooltipIcon
title={infoTitle}
icon={SettingsRounded}
onClick={onInfoClick}
sx={{ ml: 1 }}
/>
{extraIcons}
</Box>
<GuardState
value={active}
valueProps="checked"
onCatch={onError}
onFormat={(_, v) => v}
onGuard={onToggle}
>
<Switch edge="end" disabled={disabled} />
</GuardState>
</Box>
);
};
const ProxyControlSwitches = ({
label,
onError,
@@ -39,159 +112,94 @@ 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<DialogRef>(null);
const tunRef = useRef<DialogRef>(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<HTMLInputElement>,
value: boolean,
) => value;
const onChangeData = (patch: Partial<IVergeConfig>) => {
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 (
<Box sx={{ width: "100%" }}>
{/* 仅显示当前选中的开关 */}
<Box sx={{ width: "100%", pr: noRightPadding ? 1 : 2 }}>
{isSystemProxyMode && (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 1,
pr: noRightPadding ? 1 : 2,
borderRadius: 1.5,
bgcolor: enable_system_proxy
? alpha(theme.palette.success.main, 0.07)
: "transparent",
transition: "background-color 0.3s",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
{systemProxyActualState ? (
<PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
) : (
<PauseCircleOutlineRounded
sx={{ color: "text.disabled", mr: 1 }}
<SwitchRow
label={t("System Proxy")}
active={systemProxyActualState}
infoTitle={t("System Proxy Info")}
onInfoClick={() => sysproxyRef.current?.open()}
onToggle={(value) => toggleSystemProxy(value)}
onError={onError}
highlight={enable_system_proxy}
/>
)}
<Typography
variant="subtitle1"
sx={{ fontWeight: 500, fontSize: "15px" }}
>
{t("System Proxy")}
</Typography>
<TooltipIcon
title={t("System Proxy Info")}
icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()}
sx={{ ml: 1 }}
/>
</Box>
<GuardState
value={systemProxyActualState}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onGuard={(e) => toggleSystemProxy(e)}
>
<Switch edge="end" />
</GuardState>
</Box>
)}
{isTunMode && (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 1,
pr: noRightPadding ? 1 : 2,
borderRadius: 1.5,
bgcolor: enable_tun_mode
? alpha(theme.palette.success.main, 0.07)
: "transparent",
opacity: !isTunAvailable ? 0.6 : 1,
transition: "background-color 0.3s",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
{enable_tun_mode ? (
<PlayCircleOutlineRounded sx={{ color: "success.main", mr: 1 }} />
) : (
<PauseCircleOutlineRounded
sx={{ color: "text.disabled", mr: 1 }}
/>
)}
<Typography
variant="subtitle1"
sx={{ fontWeight: 500, fontSize: "15px" }}
>
{t("Tun Mode")}
</Typography>
<TooltipIcon
title={t("Tun Mode Info")}
icon={SettingsRounded}
onClick={() => tunRef.current?.open()}
sx={{ ml: 1 }}
/>
{!isTunAvailable && (
<SwitchRow
label={t("Tun Mode")}
active={!!enable_tun_mode}
infoTitle={t("Tun Mode Info")}
onInfoClick={() => tunRef.current?.open()}
onToggle={handleTunToggle}
onError={onError}
disabled={!isServiceMode}
highlight={!!enable_tun_mode}
extraIcons={
<>
{!isServiceMode && (
<TooltipIcon
title={t("TUN requires Service Mode or Admin Mode")}
icon={WarningRounded}
sx={{ color: "warning.main", ml: 1 }}
/>
)}
{!isTunAvailable && (
{!isServiceMode ? (
<TooltipIcon
title={t("Install Service")}
icon={BuildRounded}
@@ -199,9 +207,7 @@ const ProxyControlSwitches = ({
onClick={onInstallService}
sx={{ ml: 1 }}
/>
)}
{isServiceMode && (
) : (
<TooltipIcon
title={t("Uninstall Service")}
icon={DeleteForeverRounded}
@@ -210,44 +216,11 @@ const ProxyControlSwitches = ({
sx={{ ml: 1 }}
/>
)}
</Box>
<GuardState
value={enable_tun_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(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")),
);
</>
}
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 });
}}
>
<Switch edge="end" disabled={!isTunAvailable} />
</GuardState>
</Box>
/>
)}
{/* 引用对话框组件 */}
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
</Box>

View File

@@ -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,
};
}

View File

@@ -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 () => {
const executeWithErrorHandling = async (
operation: () => Promise<void>,
loadingMessage: string,
successMessage?: string,
) => {
try {
showNotice("info", t("Installing Service..."));
await installService();
showNotice("success", t("Service Installed Successfully"));
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;
}
};
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,
}),
export const useServiceInstaller = () => {
const installServiceAndRestartCore = useCallback(async () => {
await executeWithErrorHandling(
() => installService(),
"Installing Service...",
"Service Installed Successfully",
);
}
} 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;
}
});
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
}, []);
return { installServiceAndRestartCore };
}
};

View File

@@ -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<void>,
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 };
};

View File

@@ -208,6 +208,9 @@ const Layout = () => {
mutate("getVergeConfig");
mutate("getSystemProxy");
mutate("getAutotemProxy");
// 运行模式变更时也需要刷新相关状态
mutate("getRunningMode");
mutate("isServiceAvailable");
}),
addListener("verge://notice-message", ({ payload }) =>