Tunglies b1e2940db6
refactor: optimize singleton macro usage with Default trait implementations (#4279)
* refactor: implement DRY principle improvements across backend

Major DRY violations identified and addressed:

1. **IPC Stream Monitor Pattern**:
   - Created `utils/ipc_monitor.rs` with generic `IpcStreamMonitor` trait
   - Added `IpcMonitorManager` for common async task management patterns
   - Eliminates duplication across traffic.rs, memory.rs, and logs.rs

2. **Singleton Pattern Duplication**:
   - Created `utils/singleton.rs` with `singleton\!` and `singleton_with_logging\!` macros
   - Replaces 16+ duplicate singleton implementations across codebase
   - Provides consistent, tested patterns for global instances

3. **macOS Activation Policy Refactoring**:
   - Consolidated 3 duplicate methods into single parameterized `set_activation_policy()`
   - Eliminated code duplication while maintaining backward compatibility
   - Reduced maintenance burden for macOS-specific functionality

These improvements enhance maintainability, reduce bug potential, and ensure consistent patterns across the backend codebase.

* fix: resolve test failures and clippy warnings

- Fix doctest in singleton.rs by using rust,ignore syntax and proper code examples
- Remove unused time::Instant import from ipc_monitor.rs
- Add #[allow(dead_code)] attributes to future-use utility modules
- All 11 unit tests now pass successfully
- All clippy checks pass with -D warnings strict mode
- Documentation tests properly ignore example code that requires full context

* refactor: migrate code to use new utility tools (partial)

Progress on systematic migration to use created utility tools:

1. **Reorganized IPC Monitor**:
   - Moved ipc_monitor.rs to src-tauri/src/ipc/monitor.rs for better organization
   - Updated module structure to emphasize IPC relationship

2. **IpcManager Singleton Migration**:
   - Replaced manual OnceLock singleton pattern with singleton_with_logging\! macro
   - Simplified initialization code and added consistent logging
   - Removed unused imports (OnceLock, logging::Type)

3. **ProxyRequestCache Singleton Migration**:
   - Migrated from once_cell::sync::OnceCell to singleton\! macro
   - Cleaner, more maintainable singleton pattern
   - Consistent with project-wide singleton approach

These migrations demonstrate the utility and effectiveness of the created tools:
- Less boilerplate code
- Consistent patterns across codebase
- Easier maintenance and debugging

* feat: complete migration to new utility tools - phase 1

Successfully migrated core components to use the created utility tools:

- Moved `ipc_monitor.rs` to `src-tauri/src/ipc/monitor.rs`
- Better organization emphasizing IPC relationship
- Updated module exports and imports

- **IpcManager**: Migrated to `singleton_with_logging\!` macro
- **ProxyRequestCache**: Migrated to `singleton\!` macro
- Eliminated ~30 lines of boilerplate singleton code
- Consistent logging and initialization patterns

- Removed unused imports (OnceLock, once_cell, logging::Type)
- Cleaner, more maintainable code structure
- All 11 unit tests pass successfully
- Zero compilation warnings

- **Lines of code reduced**: ~50+ lines of boilerplate
- **Consistency improved**: Unified singleton patterns
- **Maintainability enhanced**: Centralized utility functions
- **Test coverage maintained**: 100% test pass rate

Remaining complex monitors (traffic, memory, logs) will be migrated to use the shared IPC monitoring patterns in the next phase, which requires careful refactoring of their streaming logic.

* refactor: complete singleton pattern migration to utility macros

Migrate remaining singleton patterns across the backend to use standardized
utility macros, achieving significant code reduction and consistency improvements.

- **LogsMonitor** (ipc/logs.rs): `OnceLock` → `singleton_with_logging\!`
- **Sysopt** (core/sysopt.rs): `OnceCell` → `singleton_lazy\!`
- **Tray** (core/tray/mod.rs): Complex `OnceCell` → `singleton_lazy\!`
- **Handle** (core/handle.rs): `OnceCell` → `singleton\!`
- **CoreManager** (core/core.rs): `OnceCell` → `singleton_lazy\!`
- **TrafficMonitor** (ipc/traffic.rs): `OnceLock` → `singleton_lazy_with_logging\!`
- **MemoryMonitor** (ipc/memory.rs): `OnceLock` → `singleton_lazy_with_logging\!`

- `singleton_lazy\!` - For complex initialization patterns
- `singleton_lazy_with_logging\!` - For complex initialization with logging

- **Code Reduction**: -33 lines of boilerplate singleton code
- **DRY Compliance**: Eliminated duplicate initialization patterns
- **Consistency**: Unified singleton approach across codebase
- **Maintainability**: Centralized singleton logic in utility macros
- **Zero Breaking Changes**: All existing APIs remain compatible

All tests pass and clippy warnings resolved.

* refactor: optimize singleton macros using Default trait implementation

Simplify singleton macro usage by implementing Default trait for complex
initialization patterns, significantly improving code readability and maintainability.

- **MemoryMonitor**: Move IPC client initialization to Default impl
- **TrafficMonitor**: Move IPC client initialization to Default impl
- **Sysopt**: Move Arc<Mutex> initialization to Default impl
- **Tray**: Move struct field initialization to Default impl
- **CoreManager**: Move Arc<Mutex> initialization to Default impl

```rust
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", || {
    let ipc_path_buf = ipc_path().unwrap();
    let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
    let client = IpcStreamClient::new(ipc_path).unwrap();
    MemoryMonitor::new(client)
});
```

```rust
impl Default for MemoryMonitor { /* initialization logic */ }
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", MemoryMonitor::default);
```

- **Code Reduction**: -17 lines of macro closure code (80%+ simplification)
- **Separation of Concerns**: Initialization logic moved to proper Default impl
- **Readability**: Single-line macro calls vs multi-line closures
- **Testability**: Default implementations can be tested independently
- **Rust Idioms**: Using standard Default trait pattern
- **Performance**: Function calls more efficient than closures

All tests pass and clippy warnings resolved.

* refactor: implement MonitorData and StreamingParser traits for IPC monitors

* refactor: add timeout and retry_interval fields to IpcStreamMonitor; update TrafficMonitorState to derive Default

* refactor: migrate AppHandleManager to unified singleton control

- Replace manual singleton implementation with singleton_with_logging\! macro
- Remove std::sync::Once dependency in favor of OnceLock-based pattern
- Improve error handling for macOS activation policy methods
- Maintain thread safety with parking_lot::Mutex for AppHandle storage
- Add proper initialization check to prevent duplicate handle assignment
- Enhance logging consistency across AppHandleManager operations

* refactor: improve hotkey management with enum-based operations

- Add HotkeyFunction enum for type-safe function selection
- Add SystemHotkey enum for predefined system shortcuts
- Implement Display and FromStr traits for type conversions
- Replace string-based hotkey registration with enum methods
- Add register_system_hotkey() and unregister_system_hotkey() methods
- Maintain backward compatibility with string-based register() method
- Migrate singleton pattern to use singleton_with_logging\! macro
- Extract hotkey function execution logic into centralized execute_function()
- Update lib.rs to use new enum-based SystemHotkey operations
- Improve type safety and reduce string manipulation errors

Benefits:
- Type safety prevents invalid hotkey function names
- Centralized function execution reduces code duplication
- Enum-based API provides better IDE autocomplete support
- Maintains full backward compatibility with existing configurations

* fix: resolve LightWeightState initialization order panic

- Modify with_lightweight_status() to safely handle unmanaged state using try_state()
- Return Option<R> instead of R to gracefully handle state unavailability
- Update is_in_lightweight_mode() to use unwrap_or(false) for safe defaults
- Add state availability check in auto_lightweight_mode_init() before access
- Maintain singleton check priority while preventing early state access panics
- Fix clippy warnings for redundant pattern matching

Resolves runtime panic: "state() called before manage() for LightWeightState"

* refactor: add unreachable patterns for non-macOS in hotkey handling

* refactor: simplify SystemHotkey enum by removing redundant cfg attributes

* refactor: add macOS conditional compilation for system hotkey registration methods

* refactor: streamline hotkey unregistration and error logging for macOS
2025-07-31 14:35:13 +08:00

374 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use kode_bridge::{
errors::{AnyError, AnyResult},
IpcHttpClient, LegacyResponse,
};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
// 定义用于URL路径的编码集合只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') // 空格
.add(b'/') // 斜杠
.add(b'?') // 问号
.add(b'#') // 井号
.add(b'&') // 和号
.add(b'%'); // 百分号
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
// Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into()))
}
pub struct IpcManager {
ipc_path: String,
}
impl IpcManager {
fn new() -> Self {
let ipc_path_buf = ipc_path().unwrap();
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
Self {
ipc_path: ipc_path.to_string(),
}
}
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager {
pub async fn request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> {
let client = IpcHttpClient::new(&self.ipc_path)?;
client.request(method, path, body).await
}
}
impl IpcManager {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<serde_json::Value> {
let response = IpcManager::global().request(method, path, body).await?;
match method {
"GET" => Ok(response.json()?),
"PATCH" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
Ok(response.json()?)
}
}
"PUT" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
// 尝试解析JSON如果失败则返回错误信息
match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
}
}
}
_ => Ok(response.json()?),
}
}
// 基础代理信息获取
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/proxies";
self.send_request("GET", url, None).await
}
// 代理提供者信息获取
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/proxies";
self.send_request("GET", url, None).await
}
// 连接管理
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
let url = "/connections";
self.send_request("GET", url, None).await
}
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
let url = format!("/connections/{encoded_id}");
let response = self.send_request("DELETE", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"].as_str().unwrap_or("unknown error"),
))
}
}
pub async fn close_all_connections(&self) -> AnyResult<()> {
let url = "/connections";
let response = self.send_request("DELETE", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
}
impl IpcManager {
#[allow(dead_code)]
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
let url = "/version";
let _response = self.send_request("GET", url, None).await?;
Ok(())
}
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
let url = "/configs?force=true";
let payload = serde_json::json!({
"path": clash_config_path,
});
let _response = self.send_request("PUT", url, Some(&payload)).await?;
Ok(())
}
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
let url = "/configs";
let response = self.send_request("PATCH", url, Some(&config)).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
pub async fn test_proxy_delay(
&self,
name: &str,
test_url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url =
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 版本和配置相关
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
let url = "/version";
self.send_request("GET", url, None).await
}
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
let url = "/configs";
self.send_request("GET", url, None).await
}
pub async fn update_geo_data(&self) -> AnyResult<()> {
let url = "/configs/geo";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn upgrade_core(&self) -> AnyResult<()> {
let url = "/upgrade";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 规则相关
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
let url = "/rules";
self.send_request("GET", url, None).await
}
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/rules";
self.send_request("GET", url, None).await
}
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/rules/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 代理相关
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
// 使用 percent-encoding 进行正确的 URL 编码
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
let url = format!("/proxies/{encoded_group}");
let payload = serde_json::json!({
"name": proxy
});
let response = match self.send_request("PUT", &url, Some(&payload)).await {
Ok(resp) => resp,
Err(e) => {
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
e
);
// Always return a successful response as serde_json::Value
serde_json::json!({"code": 204})
}
};
if response["code"] == 204 {
Ok(())
} else {
let error_msg = response["message"].as_str().unwrap_or_else(|| {
if let Some(error) = response.get("error") {
error.as_str().unwrap_or("unknown error")
} else {
"failed to update proxy"
}
});
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"IPC: updateProxy failed: {}",
error_msg
);
Err(create_error(error_msg.to_string()))
}
}
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
let response = self.send_request("GET", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 延迟测试相关
pub async fn get_group_proxy_delays(
&self,
group_name: &str,
url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 调试相关
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
let url = "/debug/pprof";
match self.send_request("GET", url, None).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub async fn gc(&self) -> AnyResult<()> {
let url = "/debug/gc";
let response = self.send_request("PUT", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}