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
This commit is contained in:
Tunglies
2025-08-29 17:46:46 +08:00
committed by GitHub
parent a9951e4eca
commit 6eecd70bd5
11 changed files with 341 additions and 309 deletions

View File

@@ -17,6 +17,8 @@
- 修复应用在某些操作中可能出现的响应延迟问题 - 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题 - 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复 - 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
### 🗑️ 移除内容 ### 🗑️ 移除内容

169
src-tauri/Cargo.lock generated
View File

@@ -151,7 +151,7 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"parking_lot", "parking_lot 0.12.4",
"percent-encoding", "percent-encoding",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"wl-clipboard-rs", "wl-clipboard-rs",
@@ -964,6 +964,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "castaway"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.30" version = "1.2.30"
@@ -1112,6 +1118,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"hex", "hex",
"hmac", "hmac",
"isahc",
"kode-bridge", "kode-bridge",
"libc", "libc",
"log", "log",
@@ -1120,7 +1127,7 @@ dependencies = [
"network-interface", "network-interface",
"once_cell", "once_cell",
"open", "open",
"parking_lot", "parking_lot 0.12.4",
"percent-encoding", "percent-encoding",
"port_scanner", "port_scanner",
"regex", "regex",
@@ -1544,6 +1551,37 @@ dependencies = [
"cipher", "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]] [[package]]
name = "darling" name = "darling"
version = "0.20.11" version = "0.20.11"
@@ -1589,7 +1627,7 @@ dependencies = [
"hashbrown 0.14.5", "hashbrown 0.14.5",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core 0.9.11",
] ]
[[package]] [[package]]
@@ -1603,7 +1641,7 @@ dependencies = [
"hashbrown 0.14.5", "hashbrown 0.14.5",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core 0.9.11",
] ]
[[package]] [[package]]
@@ -3537,6 +3575,34 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@@ -3676,7 +3742,7 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"httparse", "httparse",
"interprocess", "interprocess",
"parking_lot", "parking_lot 0.12.4",
"pin-project-lite", "pin-project-lite",
"rand 0.9.2", "rand 0.9.2",
"serde", "serde",
@@ -3777,6 +3843,16 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.6" version = "0.1.6"
@@ -3785,7 +3861,7 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall", "redox_syscall 0.5.16",
] ]
[[package]] [[package]]
@@ -3797,6 +3873,18 @@ dependencies = [
"zlib-rs", "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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.8" version = "0.3.8"
@@ -3886,7 +3974,7 @@ dependencies = [
"log", "log",
"log-mdc", "log-mdc",
"once_cell", "once_cell",
"parking_lot", "parking_lot 0.12.4",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde-value", "serde-value",
@@ -4805,6 +4893,17 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.4" version = "0.12.4"
@@ -4812,7 +4911,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [ dependencies = [
"lock_api", "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]] [[package]]
@@ -4823,7 +4936,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall 0.5.16",
"smallvec", "smallvec",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@@ -5639,6 +5752,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" 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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.16" version = "0.5.16"
@@ -6507,6 +6629,17 @@ version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 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]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -6576,7 +6709,7 @@ dependencies = [
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2", "objc2-quartz-core 0.2.2",
"raw-window-handle", "raw-window-handle",
"redox_syscall", "redox_syscall 0.5.16",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -6633,7 +6766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot", "parking_lot 0.12.4",
"phf_shared 0.11.3", "phf_shared 0.11.3",
"precomputed-hash", "precomputed-hash",
"serde", "serde",
@@ -6822,7 +6955,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"parking_lot", "parking_lot 0.12.4",
"raw-window-handle", "raw-window-handle",
"scopeguard", "scopeguard",
"tao-macros", "tao-macros",
@@ -7599,7 +7732,7 @@ dependencies = [
"io-uring", "io-uring",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot 0.12.4",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
@@ -7982,6 +8115,16 @@ dependencies = [
"valuable", "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]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.2.0" version = "0.2.0"

View File

@@ -78,6 +78,7 @@ dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1" tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true } console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
isahc = { version = "1.7.2", features = ["parking_lot"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"

View File

@@ -2,10 +2,10 @@ use super::CmdResult;
use crate::{ use crate::{
config::{ config::{
profiles::{ profiles::{
profiles_append_item_safe, profiles_delete_item_safe, profiles_patch_item_safe, profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
profiles_reorder_safe, profiles_save_file_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}, core::{handle, timer::Timer, tray::Tray, CoreManager},
feat, logging, feat, logging,
@@ -225,8 +225,8 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
/// 创建新的profile /// 创建新的profile
/// 创建一个新的配置文件 /// 创建一个新的配置文件
#[tauri::command] #[tauri::command]
pub async fn create_profile(item: PrfItem, _file_data: Option<String>) -> CmdResult { pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
match profiles_append_item_safe(item).await { match profiles_append_item_with_filedata_safe(item, file_data).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => match err.to_string().as_str() { Err(err) => match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()), "the file already exists" => Err("the file already exists".into()),

View File

@@ -1,6 +1,6 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{ use crate::{
config::{profiles::profiles_append_item_safe, PrfItem}, config::{profiles_append_item_safe, PrfItem},
core::{handle, CoreManager}, core::{handle, CoreManager},
enhance, logging, enhance, logging,
process::AsyncHandler, process::AsyncHandler,

View File

@@ -4,7 +4,6 @@ use crate::utils::{
tmpl, tmpl,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
@@ -266,7 +265,7 @@ impl PrfItem {
}; };
// 使用网络管理器发送请求 // 使用网络管理器发送请求
let resp = match NetworkManager::global() let resp = match NetworkManager::new()
.get_with_interrupt( .get_with_interrupt(
url, url,
proxy_type, proxy_type,
@@ -284,7 +283,7 @@ impl PrfItem {
}; };
let status_code = resp.status(); 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}") bail!("failed to fetch remote profile with status {status_code}")
} }
@@ -350,7 +349,7 @@ impl PrfItem {
let uid = help::get_uid("R"); let uid = help::get_uid("R");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); 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" // process the charset "UTF-8 with BOM"
let data = data.trim_start_matches('\u{feff}'); let data = data.trim_start_matches('\u{feff}');

View File

@@ -740,6 +740,22 @@ impl IProfiles {
// 特殊的Send-safe helper函数完全避免跨await持有guard // 特殊的Send-safe helper函数完全避免跨await持有guard
use crate::config::Config; use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(
item: PrfItem,
file_data: Option<String>,
) -> 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<()> { pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
AsyncHandler::spawn_blocking(move || { AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async { AsyncHandler::handle().block_on(async {

View File

@@ -294,10 +294,6 @@ impl Handle {
} }
} }
pub fn is_initialized(&self) -> bool {
self.app_handle().is_some()
}
/// 获取 AppHandle /// 获取 AppHandle
pub fn app_handle(&self) -> Option<AppHandle> { pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone() self.app_handle.read().clone()

View File

@@ -122,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let start = Instant::now(); let start = Instant::now();
let response = NetworkManager::global() let response = NetworkManager::new()
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false) .get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
.await; .await;

View File

@@ -40,8 +40,8 @@ mod app_init {
Ok(result) => { Ok(result) => {
if result.is_err() { if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行"); logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if handle::Handle::global().is_initialized() { if let Some(app_handle) = handle::Handle::global().app_handle() {
handle::Handle::global().app_handle().unwrap().exit(0); app_handle.exit(0);
} else { } else {
std::process::exit(0); std::process::exit(0);
} }
@@ -309,7 +309,7 @@ pub fn run() {
app_init::init_singleton_check(); app_init::init_singleton_check();
// Initialize network manager // Initialize network manager
utils::network::NetworkManager::global().init(); utils::network::NetworkManager::new().init();
// Initialize portable flag // Initialize portable flag
let _ = utils::dirs::init_portable_flag(); let _ = utils::dirs::init_portable_flag();

View File

@@ -1,143 +1,97 @@
use anyhow::Result; use anyhow::Result;
use parking_lot::Mutex; use isahc::http::{
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response}; header::{HeaderMap, HeaderValue, USER_AGENT},
use std::{ StatusCode, Uri,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Once,
},
time::{Duration, Instant},
}; };
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;
use crate::{config::Config, logging, process::AsyncHandler, singleton_lazy};
// HTTP2 相关 #[derive(Debug)]
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024; pub struct HttpResponse {
const H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024; status: StatusCode,
const H2_MAX_FRAME_SIZE: u32 = 16 * 1024; headers: HeaderMap,
const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); body: String,
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<Runtime>,
self_proxy_client: Mutex<Option<Client>>,
system_proxy_client: Mutex<Option<Client>>,
no_proxy_client: Mutex<Option<Client>>,
init: Once,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: AtomicUsize,
} }
// Use singleton_lazy macro to replace lazy_static! impl HttpResponse {
singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new); 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<Option<HttpClient>>,
system_proxy_client: Mutex<Option<HttpClient>>,
no_proxy_client: Mutex<Option<HttpClient>>,
init: Once,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: Mutex<usize>,
}
impl NetworkManager { impl NetworkManager {
fn new() -> Self { pub fn new() -> Self {
// 创建专用的异步运行时线程数限制为4个 Self {
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),
self_proxy_client: Mutex::new(None), self_proxy_client: Mutex::new(None),
system_proxy_client: Mutex::new(None), system_proxy_client: Mutex::new(None),
no_proxy_client: Mutex::new(None), no_proxy_client: Mutex::new(None),
init: Once::new(), init: Once::new(),
last_connection_error: Mutex::new(None), last_connection_error: Mutex::new(None),
connection_error_count: AtomicUsize::new(0), connection_error_count: Mutex::new(0),
} }
} }
/// 初始化网络客户端
pub fn init(&self) { pub fn init(&self) {
self.init.call_once(|| { 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, "网络管理器初始化完成");
});
});
} }
fn record_connection_error(&self, error: &str) { async fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock(); let mut last_error = self.last_connection_error.lock().await;
*last_error = Some((Instant::now(), error.to_string())); *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 { async fn should_reset_clients(&self) -> bool {
let error_count = self.connection_error_count.load(Ordering::Relaxed); let count = *self.connection_error_count.lock().await;
let last_error = self.last_connection_error.lock(); let last_error_guard = self.last_connection_error.lock().await;
if error_count > 5 { if count > 5 {
return true; return true;
} }
if let Some((time, _)) = *last_error { if let Some((time, _)) = &*last_error_guard {
if time.elapsed() < Duration::from_secs(30) && error_count > 2 { if time.elapsed() < Duration::from_secs(30) && count > 2 {
return true; return true;
} }
} }
@@ -145,60 +99,57 @@ impl NetworkManager {
false false
} }
pub fn reset_clients(&self) { pub async fn reset_clients(&self) {
logging!(info, Type::Network, true, "正在重置所有HTTP客户端"); *self.self_proxy_client.lock().await = None;
{ *self.system_proxy_client.lock().await = None;
let mut client = self.self_proxy_client.lock(); *self.no_proxy_client.lock().await = None;
*client = None; *self.connection_error_count.lock().await = 0;
} }
{
let mut client = self.system_proxy_client.lock(); fn build_client(
*client = None; &self,
} proxy_uri: Option<Uri>,
{ default_headers: HeaderMap,
let mut client = self.no_proxy_client.lock(); accept_invalid_certs: bool,
*client = None; timeout_secs: Option<u64>,
} ) -> Result<HttpClient> {
self.connection_error_count.store(0, Ordering::Relaxed); 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( pub async fn create_request(
&self, &self,
url: &str,
proxy_type: ProxyType, proxy_type: ProxyType,
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> RequestBuilder { ) -> Result<HttpClient> {
if self.should_reset_clients() { let proxy_uri = match proxy_type {
self.reset_clients(); ProxyType::None => None,
}
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();
}
ProxyType::Localhost => { ProxyType::Localhost => {
let port = { let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_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(), None => Config::clash().await.latest_ref().get_mixed_port(),
} }
}; };
let proxy_scheme = format!("http://127.0.0.1:{port}"); let proxy_scheme = format!("http://127.0.0.1:{port}");
proxy_scheme.parse::<Uri>().ok()
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);
}
} }
ProxyType::System => { ProxyType::System => {
use sysproxy::Sysproxy;
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
let proxy_scheme = format!("http://{}:{}", p.host, p.port); let proxy_scheme = format!("http://{}:{}", p.host, p.port);
proxy_scheme.parse::<Uri>().ok()
if let Ok(proxy) = Proxy::http(&proxy_scheme) { } else {
builder = builder.proxy(proxy); None
}
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);
})
}
} }
} }
}; };
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( pub async fn get_with_interrupt(
@@ -304,51 +192,38 @@ impl NetworkManager {
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> Result<Response> { ) -> Result<HttpResponse> {
let request = self if self.should_reset_clients().await {
.create_request( self.reset_clients().await;
url, }
proxy_type,
timeout_secs,
user_agent,
accept_invalid_certs,
)
.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 response = match timeout(timeout_duration, async {
let watchdog = AsyncHandler::spawn(move || async move { let mut response = client.get_async(&url_owned).await?;
tokio::time::sleep(Duration::from_secs(timeout_duration)).await; let status = response.status();
let _ = cancel_tx.send(()); let headers = response.headers().clone();
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone); let body = response.text().await?;
}); Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body))
})
let result = tokio::select! { .await
result = request.send() => result, {
_ = cancel_rx => { Ok(res) => res?,
self.record_connection_error(&format!("Request interrupted for: {url}")); Err(_) => {
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration)); 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) => Ok(response),
Err(e) => {
self.record_connection_error(&e.to_string());
Err(anyhow::anyhow!("Failed to send HTTP request: {}", e))
}
}
} }
} }
/// 代理类型
#[derive(Debug, Clone, Copy)]
pub enum ProxyType {
None,
Localhost,
System,
}