From 6eecd70bd57cc9fd259a3c35be8f6103bbdd350f Mon Sep 17 00:00:00 2001 From: Tunglies Date: Fri, 29 Aug 2025 17:46:46 +0800 Subject: [PATCH] fix(subscription): resolve issues causing import failures in some cases #4534, #4436, #4552, #4519, #4517, #4503, #4336, #4301 (#4553) * fix(subscription): resolve issues causing import failures in some cases #4534, #4436, #4552, #4519, #4517, #4503, #4336, #4301 * fix(profile): update profile creation to include file data handling * fix(app): improve singleton instance exit handling * fix: remove unsued handle method --- UPDATELOG.md | 2 + src-tauri/Cargo.lock | 169 +++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/src/cmd/profile.rs | 10 +- src-tauri/src/config/config.rs | 2 +- src-tauri/src/config/prfitem.rs | 7 +- src-tauri/src/config/profiles.rs | 16 ++ src-tauri/src/core/handle.rs | 4 - src-tauri/src/feat/clash.rs | 2 +- src-tauri/src/lib.rs | 6 +- src-tauri/src/utils/network.rs | 431 +++++++++++-------------------- 11 files changed, 341 insertions(+), 309 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index d65fe076..eccbcd4f 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -17,6 +17,8 @@ - 修复应用在某些操作中可能出现的响应延迟问题 - 修复任务管理中的潜在并发问题 - 修复通过托盘重启应用无法恢复 +- 修复订阅在某些情况下无法导入 +- 修复无法新建订阅时使用远程链接 ### 🗑️ 移除内容 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c11d1602..574ab039 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -151,7 +151,7 @@ dependencies = [ "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", - "parking_lot", + "parking_lot 0.12.4", "percent-encoding", "windows-sys 0.59.0", "wl-clipboard-rs", @@ -964,6 +964,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + [[package]] name = "cc" version = "1.2.30" @@ -1112,6 +1118,7 @@ dependencies = [ "getrandom 0.3.3", "hex", "hmac", + "isahc", "kode-bridge", "libc", "log", @@ -1120,7 +1127,7 @@ dependencies = [ "network-interface", "once_cell", "open", - "parking_lot", + "parking_lot 0.12.4", "percent-encoding", "port_scanner", "regex", @@ -1544,6 +1551,37 @@ dependencies = [ "cipher", ] +[[package]] +name = "curl" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.6.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.83+curl-8.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5830daf304027db10c82632a464879d46a3f7c4ba17a31592657ad16c719b483" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.59.0", +] + [[package]] name = "darling" version = "0.20.11" @@ -1589,7 +1627,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] @@ -1603,7 +1641,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.11", ] [[package]] @@ -3537,6 +3575,34 @@ dependencies = [ "once_cell", ] +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel 1.9.0", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener 2.5.3", + "futures-lite 1.13.0", + "http 0.2.12", + "log", + "mime", + "once_cell", + "parking_lot 0.11.2", + "polling 2.8.0", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + [[package]] name = "itertools" version = "0.12.1" @@ -3676,7 +3742,7 @@ dependencies = [ "http 1.3.1", "httparse", "interprocess", - "parking_lot", + "parking_lot 0.12.4", "pin-project-lite", "rand 0.9.2", "serde", @@ -3777,6 +3843,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libnghttp2-sys" +version = "0.1.11+1.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6c24e48a7167cffa7119da39d577fa482e66c688a4aac016bee862e1a713c4" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libredox" version = "0.1.6" @@ -3785,7 +3861,7 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall", + "redox_syscall 0.5.16", ] [[package]] @@ -3797,6 +3873,18 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3886,7 +3974,7 @@ dependencies = [ "log", "log-mdc", "once_cell", - "parking_lot", + "parking_lot 0.12.4", "rand 0.8.5", "serde", "serde-value", @@ -4805,6 +4893,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.4" @@ -4812,7 +4911,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -4823,7 +4936,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.16", "smallvec", "windows-targets 0.52.6", ] @@ -5639,6 +5752,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.16" @@ -6507,6 +6629,17 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel 1.9.0", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -6576,7 +6709,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.16", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -6633,7 +6766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "parking_lot", + "parking_lot 0.12.4", "phf_shared 0.11.3", "precomputed-hash", "serde", @@ -6822,7 +6955,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation 0.3.1", "once_cell", - "parking_lot", + "parking_lot 0.12.4", "raw-window-handle", "scopeguard", "tao-macros", @@ -7599,7 +7732,7 @@ dependencies = [ "io-uring", "libc", "mio", - "parking_lot", + "parking_lot 0.12.4", "pin-project-lite", "signal-hook-registry", "slab", @@ -7982,6 +8115,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 659850ab..5a2365b3 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -78,6 +78,7 @@ dashmap = "6.1.0" tauri-plugin-notification = "2.3.1" console-subscriber = { version = "0.4.1", optional = true } tokio-stream = "0.1.17" +isahc = { version = "1.7.2", features = ["parking_lot"] } [target.'cfg(windows)'.dependencies] runas = "=1.2.0" diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 0311bed7..d78f6446 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -2,10 +2,10 @@ use super::CmdResult; use crate::{ config::{ profiles::{ - profiles_append_item_safe, profiles_delete_item_safe, profiles_patch_item_safe, - profiles_reorder_safe, profiles_save_file_safe, + profiles_append_item_with_filedata_safe, profiles_delete_item_safe, + profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe, }, - Config, IProfiles, PrfItem, PrfOption, + profiles_append_item_safe, Config, IProfiles, PrfItem, PrfOption, }, core::{handle, timer::Timer, tray::Tray, CoreManager}, feat, logging, @@ -225,8 +225,8 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { /// 创建新的profile /// 创建一个新的配置文件 #[tauri::command] -pub async fn create_profile(item: PrfItem, _file_data: Option) -> CmdResult { - match profiles_append_item_safe(item).await { +pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { + match profiles_append_item_with_filedata_safe(item, file_data).await { Ok(_) => Ok(()), Err(err) => match err.to_string().as_str() { "the file already exists" => Err("the file already exists".into()), diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index ad0be9e8..22e63fc4 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -1,6 +1,6 @@ use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; use crate::{ - config::{profiles::profiles_append_item_safe, PrfItem}, + config::{profiles_append_item_safe, PrfItem}, core::{handle, CoreManager}, enhance, logging, process::AsyncHandler, diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 1049e939..e3c07d6d 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -4,7 +4,6 @@ use crate::utils::{ tmpl, }; use anyhow::{bail, Context, Result}; -use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; use std::{fs, time::Duration}; @@ -266,7 +265,7 @@ impl PrfItem { }; // 使用网络管理器发送请求 - let resp = match NetworkManager::global() + let resp = match NetworkManager::new() .get_with_interrupt( url, proxy_type, @@ -284,7 +283,7 @@ impl PrfItem { }; let status_code = resp.status(); - if !StatusCode::is_success(&status_code) { + if !status_code.is_success() { bail!("failed to fetch remote profile with status {status_code}") } @@ -350,7 +349,7 @@ impl PrfItem { let uid = help::get_uid("R"); let file = format!("{uid}.yaml"); let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); - let data = resp.text_with_charset("utf-8").await?; + let data = resp.text_with_charset()?; // process the charset "UTF-8 with BOM" let data = data.trim_start_matches('\u{feff}'); diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 2d951918..ef557370 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -740,6 +740,22 @@ impl IProfiles { // 特殊的Send-safe helper函数,完全避免跨await持有guard use crate::config::Config; +pub async fn profiles_append_item_with_filedata_safe( + item: PrfItem, + file_data: Option, +) -> Result<()> { + AsyncHandler::spawn_blocking(move || { + AsyncHandler::handle().block_on(async { + let item = PrfItem::from(item, file_data).await?; + let profiles = Config::profiles().await; + let mut profiles_guard = profiles.data_mut(); + profiles_guard.append_item(item).await + }) + }) + .await + .map_err(|e| anyhow::anyhow!("Task join error: {}", e))? +} + pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> { AsyncHandler::spawn_blocking(move || { AsyncHandler::handle().block_on(async { diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 38f87eee..6405b6d4 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -294,10 +294,6 @@ impl Handle { } } - pub fn is_initialized(&self) -> bool { - self.app_handle().is_some() - } - /// 获取 AppHandle pub fn app_handle(&self) -> Option { self.app_handle.read().clone() diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 9085523d..d5aa9e0e 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -122,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { let start = Instant::now(); - let response = NetworkManager::global() + let response = NetworkManager::new() .get_with_interrupt(&url, proxy_type, Some(10), user_agent, false) .await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a14f56b5..f867d7f1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -40,8 +40,8 @@ mod app_init { Ok(result) => { if result.is_err() { logging!(info, Type::Setup, true, "检测到已有应用实例运行"); - if handle::Handle::global().is_initialized() { - handle::Handle::global().app_handle().unwrap().exit(0); + if let Some(app_handle) = handle::Handle::global().app_handle() { + app_handle.exit(0); } else { std::process::exit(0); } @@ -309,7 +309,7 @@ pub fn run() { app_init::init_singleton_check(); // Initialize network manager - utils::network::NetworkManager::global().init(); + utils::network::NetworkManager::new().init(); // Initialize portable flag let _ = utils::dirs::init_portable_flag(); diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index f4efa1cb..e68bccd3 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -1,143 +1,97 @@ use anyhow::Result; -use parking_lot::Mutex; -use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response}; -use std::{ - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Once, - }, - time::{Duration, Instant}, +use isahc::http::{ + header::{HeaderMap, HeaderValue, USER_AGENT}, + StatusCode, Uri, }; -use tokio::runtime::{Builder, Runtime}; +use isahc::prelude::*; +use isahc::{config::SslOption, HttpClient}; +use std::sync::Once; +use std::time::{Duration, Instant}; +use sysproxy::Sysproxy; +use tokio::sync::Mutex; +use tokio::time::timeout; -use crate::utils::logging::Type; -use crate::{config::Config, logging, process::AsyncHandler, singleton_lazy}; +use crate::config::Config; -// HTTP2 相关 -const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024; -const H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024; -const H2_MAX_FRAME_SIZE: u32 = 16 * 1024; -const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); -const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5); -const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -const POOL_MAX_IDLE_PER_HOST: usize = 5; -const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(15); - -/// 网络管理器 -pub struct NetworkManager { - runtime: Arc, - self_proxy_client: Mutex>, - system_proxy_client: Mutex>, - no_proxy_client: Mutex>, - init: Once, - last_connection_error: Mutex>, - connection_error_count: AtomicUsize, +#[derive(Debug)] +pub struct HttpResponse { + status: StatusCode, + headers: HeaderMap, + body: String, } -// Use singleton_lazy macro to replace lazy_static! -singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new); +impl HttpResponse { + pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { + Self { + status, + headers, + body, + } + } + + pub fn status(&self) -> StatusCode { + self.status + } + + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + pub fn text_with_charset(&self) -> Result<&str> { + Ok(&self.body) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ProxyType { + None, + Localhost, + System, +} + +pub struct NetworkManager { + self_proxy_client: Mutex>, + system_proxy_client: Mutex>, + no_proxy_client: Mutex>, + init: Once, + last_connection_error: Mutex>, + connection_error_count: Mutex, +} impl NetworkManager { - fn new() -> Self { - // 创建专用的异步运行时,线程数限制为4个 - let runtime = match Builder::new_multi_thread() - .worker_threads(4) - .thread_name("clash-verge-network") - .enable_io() - .enable_time() - .build() - { - Ok(runtime) => runtime, - Err(e) => { - log::error!( - "Failed to create network runtime: {}. Using fallback single-threaded runtime.", - e - ); - // Fallback to current thread runtime - match Builder::new_current_thread() - .enable_io() - .enable_time() - .thread_name("clash-verge-network-fallback") - .build() - { - Ok(fallback_runtime) => fallback_runtime, - Err(fallback_err) => { - log::error!( - "Failed to create fallback runtime: {}. This is critical.", - fallback_err - ); - std::process::exit(1); - } - } - } - }; - - NetworkManager { - runtime: Arc::new(runtime), + pub fn new() -> Self { + Self { self_proxy_client: Mutex::new(None), system_proxy_client: Mutex::new(None), no_proxy_client: Mutex::new(None), init: Once::new(), last_connection_error: Mutex::new(None), - connection_error_count: AtomicUsize::new(0), + connection_error_count: Mutex::new(0), } } - /// 初始化网络客户端 pub fn init(&self) { - self.init.call_once(|| { - self.runtime.spawn(async { - logging!(info, Type::Network, true, "初始化网络管理器"); - - // 创建无代理客户端 - let no_proxy_client = match ClientBuilder::new() - .use_rustls_tls() - .no_proxy() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) - .build() - { - Ok(client) => client, - Err(e) => { - logging!( - error, - Type::Network, - true, - "Failed to build no_proxy client: {}", - e - ); - return; - } - }; - - let mut no_proxy_guard = NetworkManager::global().no_proxy_client.lock(); - *no_proxy_guard = Some(no_proxy_client); - - logging!(info, Type::Network, true, "网络管理器初始化完成"); - }); - }); + self.init.call_once(|| {}); } - fn record_connection_error(&self, error: &str) { - let mut last_error = self.last_connection_error.lock(); + async fn record_connection_error(&self, error: &str) { + let mut last_error = self.last_connection_error.lock().await; *last_error = Some((Instant::now(), error.to_string())); - self.connection_error_count.fetch_add(1, Ordering::Relaxed); + let mut count = self.connection_error_count.lock().await; + *count += 1; } - fn should_reset_clients(&self) -> bool { - let error_count = self.connection_error_count.load(Ordering::Relaxed); - let last_error = self.last_connection_error.lock(); + async fn should_reset_clients(&self) -> bool { + let count = *self.connection_error_count.lock().await; + let last_error_guard = self.last_connection_error.lock().await; - if error_count > 5 { + if count > 5 { return true; } - if let Some((time, _)) = *last_error { - if time.elapsed() < Duration::from_secs(30) && error_count > 2 { + if let Some((time, _)) = &*last_error_guard { + if time.elapsed() < Duration::from_secs(30) && count > 2 { return true; } } @@ -145,60 +99,57 @@ impl NetworkManager { false } - pub fn reset_clients(&self) { - logging!(info, Type::Network, true, "正在重置所有HTTP客户端"); - { - let mut client = self.self_proxy_client.lock(); - *client = None; - } - { - let mut client = self.system_proxy_client.lock(); - *client = None; - } - { - let mut client = self.no_proxy_client.lock(); - *client = None; - } - self.connection_error_count.store(0, Ordering::Relaxed); + pub async fn reset_clients(&self) { + *self.self_proxy_client.lock().await = None; + *self.system_proxy_client.lock().await = None; + *self.no_proxy_client.lock().await = None; + *self.connection_error_count.lock().await = 0; + } + + fn build_client( + &self, + proxy_uri: Option, + default_headers: HeaderMap, + accept_invalid_certs: bool, + timeout_secs: Option, + ) -> Result { + let proxy_uri_clone = proxy_uri.clone(); + let headers_clone = default_headers.clone(); + let client = { + let mut builder = HttpClient::builder(); + + builder = match proxy_uri_clone { + Some(uri) => builder.proxy(Some(uri)), + None => builder.proxy(None), + }; + + for (name, value) in headers_clone.iter() { + builder = builder.default_header(name, value); + } + + if accept_invalid_certs { + builder = builder.ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS); + } + + if let Some(secs) = timeout_secs { + builder = builder.timeout(Duration::from_secs(secs)); + } + + Ok(builder.build()?) + }; + + client } - /// 创建带有自定义选项的HTTP请求 pub async fn create_request( &self, - url: &str, proxy_type: ProxyType, timeout_secs: Option, user_agent: Option, accept_invalid_certs: bool, - ) -> RequestBuilder { - if self.should_reset_clients() { - self.reset_clients(); - } - - let mut builder = ClientBuilder::new() - .use_rustls_tls() - .pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST) - .pool_idle_timeout(POOL_IDLE_TIMEOUT) - .connect_timeout(DEFAULT_CONNECT_TIMEOUT) - .http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE) - .http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE) - .http2_adaptive_window(true) - .http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL)) - .http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT) - .http2_max_frame_size(H2_MAX_FRAME_SIZE) - .tcp_keepalive(Some(Duration::from_secs(10))) - .http2_max_header_list_size(16 * 1024); - - if let Some(timeout) = timeout_secs { - builder = builder.timeout(Duration::from_secs(timeout)); - } else { - builder = builder.timeout(DEFAULT_REQUEST_TIMEOUT); - } - - match proxy_type { - ProxyType::None => { - builder = builder.no_proxy(); - } + ) -> Result { + let proxy_uri = match proxy_type { + ProxyType::None => None, ProxyType::Localhost => { let port = { let verge_port = Config::verge().await.latest_ref().verge_mixed_port; @@ -207,94 +158,31 @@ impl NetworkManager { None => Config::clash().await.latest_ref().get_mixed_port(), } }; - let proxy_scheme = format!("http://127.0.0.1:{port}"); - - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } + proxy_scheme.parse::().ok() } ProxyType::System => { - use sysproxy::Sysproxy; - if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { let proxy_scheme = format!("http://{}:{}", p.host, p.port); - - if let Ok(proxy) = Proxy::http(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::https(&proxy_scheme) { - builder = builder.proxy(proxy); - } - if let Ok(proxy) = Proxy::all(&proxy_scheme) { - builder = builder.proxy(proxy); - } - } - } - } - - builder = builder.danger_accept_invalid_certs(accept_invalid_certs); - - if let Some(ua) = user_agent { - builder = builder.user_agent(ua); - } else { - use crate::utils::resolve::VERSION; - - let version = match VERSION.get() { - Some(v) => format!("clash-verge/v{v}"), - None => "clash-verge/unknown".to_string(), - }; - - builder = builder.user_agent(version); - } - - let client = match builder.build() { - Ok(client) => client, - Err(e) => { - logging!( - error, - Type::Network, - true, - "Failed to build custom HTTP client: {}", - e - ); - // Return a simple no-proxy client as fallback - match ClientBuilder::new() - .use_rustls_tls() - .no_proxy() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .build() - { - Ok(fallback_client) => fallback_client, - Err(fallback_err) => { - logging!( - error, - Type::Network, - true, - "Failed to create fallback client: {}", - fallback_err - ); - self.record_connection_error(&format!( - "Critical client build failure: {}", - fallback_err - )); - // Return a minimal client that will likely fail but won't panic - ClientBuilder::new().build().unwrap_or_else(|_| { - // If even the most basic client fails, this is truly critical - std::process::exit(1); - }) - } + proxy_scheme.parse::().ok() + } else { + None } } }; - client.get(url) + let mut headers = HeaderMap::new(); + headers.insert( + USER_AGENT, + HeaderValue::from_str( + &user_agent + .unwrap_or_else(|| format!("clash-verge/v{}", env!("CARGO_PKG_VERSION"))), + )?, + ); + + let client = self.build_client(proxy_uri, headers, accept_invalid_certs, timeout_secs)?; + + Ok(client) } pub async fn get_with_interrupt( @@ -304,51 +192,38 @@ impl NetworkManager { timeout_secs: Option, user_agent: Option, accept_invalid_certs: bool, - ) -> Result { - let request = self - .create_request( - url, - proxy_type, - timeout_secs, - user_agent, - accept_invalid_certs, - ) - .await; + ) -> Result { + if self.should_reset_clients().await { + self.reset_clients().await; + } - let timeout_duration = timeout_secs.unwrap_or(20); + let client = self + .create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs) + .await?; - let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); + let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20)); + let url_owned = url.to_string(); - let url_clone = url.to_string(); - let watchdog = AsyncHandler::spawn(move || async move { - tokio::time::sleep(Duration::from_secs(timeout_duration)).await; - let _ = cancel_tx.send(()); - logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone); - }); - - let result = tokio::select! { - result = request.send() => result, - _ = cancel_rx => { - self.record_connection_error(&format!("Request interrupted for: {url}")); - return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration)); + let response = match timeout(timeout_duration, async { + let mut response = client.get_async(&url_owned).await?; + let status = response.status(); + let headers = response.headers().clone(); + let body = response.text().await?; + Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body)) + }) + .await + { + Ok(res) => res?, + Err(_) => { + self.record_connection_error(&format!("Request interrupted: {}", url)) + .await; + return Err(anyhow::anyhow!( + "Request interrupted after {}s", + timeout_duration.as_secs() + )); } }; - watchdog.abort(); - match result { - Ok(response) => Ok(response), - Err(e) => { - self.record_connection_error(&e.to_string()); - Err(anyhow::anyhow!("Failed to send HTTP request: {}", e)) - } - } + Ok(response) } } - -/// 代理类型 -#[derive(Debug, Clone, Copy)] -pub enum ProxyType { - None, - Localhost, - System, -}