mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-06 00:32:13 +08:00
* fix(backup): serialize restore with config write lock Acquire the configuration write lock at the entry of finalize_restored_verge_config so the clash, profiles and verge draft applies all happen under it, instead of only the verge layer. A concurrent patch (patch_clash / patch_verge via try_lock_config_write) could otherwise stage a draft that the unlocked restore applies overwrite, or interleave its persistence with the restore. Also drop the pointless clones in the clash and profiles edit_draft closures; edit_draft takes FnOnce so the restored values can be moved in. The lock is released before patch_verge, which re-acquires it via try_lock_config_write. * perf(backup): extract archive off the runtime Run File::open, ZipArchive::new and the extraction of the backup archive inside spawn_blocking. Extracting a multi-file backup blocked a tokio worker for hundreds of milliseconds to seconds while the runtime kept serving other work on the remaining workers. Move the PROFILE_WRITE_LOCK acquisition after the extraction, right before finalize_restored_verge_config: the lock protects the config-data transition (extracted files reloaded into drafts), not the plain filesystem writes, so profile operations no longer queue behind the blocking unzip. * fix(clash): take config write lock when changing mode change_clash_mode committed the clash draft and saved config.yaml without claiming CONFIG_WRITE_LOCK, so a concurrent patch_clash transaction could have its staged draft committed here or its file-snapshot rollback swallow the mode change. Take lock_config_write (waiting) rather than try_lock_config_write: two of the three callers (hotkey, tray) discard the result, so fail-fast would silently drop the switch, and the lock is only attempted after the mihomo patch already applied, where an Err would roll the frontend back while the core runs the new mode.
158 lines
5.6 KiB
Rust
158 lines
5.6 KiB
Rust
use crate::core::notify::NoticeStatus;
|
|
use crate::core::notify::{Refresh, announce};
|
|
use crate::{
|
|
config::{Config, MixedPort},
|
|
core::{CoreManager, handle, tray},
|
|
feat::run_exit_cleanup_flow,
|
|
process::AsyncHandler,
|
|
utils,
|
|
};
|
|
use bytes::BytesMut;
|
|
use clash_verge_logging::{Type, logging};
|
|
use serde_yaml_ng::{Mapping, Value};
|
|
use smartstring::alias::String;
|
|
use std::sync::{Arc, LazyLock};
|
|
|
|
#[allow(clippy::expect_used)]
|
|
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
|
|
LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
|
|
|
|
pub async fn restart_clash_core() {
|
|
match CoreManager::global().restart_core().await {
|
|
Ok(_) => {
|
|
announce(Refresh::Clash);
|
|
}
|
|
Err(err) => {
|
|
handle::Handle::notice(NoticeStatus::SetConfigError, format!("{err:#}"));
|
|
logging!(error, Type::Core, "restart core failed: {err:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn restart_app() {
|
|
logging!(debug, Type::System, "启动重启应用流程");
|
|
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
|
|
|
|
if !cleanup_result.core_stopped {
|
|
return;
|
|
}
|
|
|
|
utils::server::shutdown_embedded_server();
|
|
let app_handle = handle::Handle::app_handle();
|
|
app_handle.restart();
|
|
}
|
|
|
|
fn after_change_clash_mode() {
|
|
AsyncHandler::spawn(move || async {
|
|
if let Err(err) = handle::Handle::mihomo().close_all_connections().await {
|
|
logging!(
|
|
error,
|
|
Type::Core,
|
|
"Failed to close all connections after changing clash mode: {err}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Propagates mihomo PATCH failures so the frontend can roll back its optimistic mode.
|
|
pub async fn change_clash_mode(mode: String) -> Result<(), String> {
|
|
let mut mapping = Mapping::new();
|
|
mapping.insert(Value::from("mode"), Value::from(mode.as_str()));
|
|
let json_value = serde_json::json!({
|
|
"mode": mode
|
|
});
|
|
if let Err(err) = handle::Handle::mihomo().patch_base_config(&json_value).await {
|
|
logging!(error, Type::Core, "change clash mode failed: {err}");
|
|
return Err(err.to_string().into());
|
|
}
|
|
|
|
let clash = Config::clash().await;
|
|
let save_result = {
|
|
let _config_write = Config::lock_config_write().await;
|
|
clash.edit_draft(|d| d.patch_config(&mapping));
|
|
clash.apply();
|
|
clash.data_arc().save_config().await
|
|
};
|
|
if save_result.is_ok() {
|
|
announce(Refresh::Clash);
|
|
tray::Tray::global().update_menu_and_icon().await;
|
|
}
|
|
|
|
let is_auto_close_connection = Config::verge().await.data_arc().auto_close_connection.unwrap_or(false);
|
|
if is_auto_close_connection {
|
|
after_change_clash_mode();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test delay to a URL through proxy.
|
|
/// HTTPS: measures TLS handshake time. HTTP: measures HEAD round-trip time.
|
|
#[tracing::instrument(skip_all, level = "trace", fields(url = %url))]
|
|
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
|
use tokio::net::TcpStream;
|
|
use tokio::time::Instant;
|
|
|
|
let parsed = tauri::Url::parse(&url)?;
|
|
let is_https = parsed.scheme() == "https";
|
|
let host = parsed
|
|
.host_str()
|
|
.ok_or_else(|| anyhow::anyhow!("Invalid URL: no host"))?
|
|
.to_string();
|
|
let port = parsed.port().unwrap_or(if is_https { 443 } else { 80 });
|
|
|
|
let verge = Config::verge().await.latest_arc();
|
|
let proxy_enabled = verge.enable_system_proxy.unwrap_or(false) || verge.enable_tun_mode.unwrap_or(false);
|
|
let proxy_port = if proxy_enabled {
|
|
Some(MixedPort::desired().await)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
tokio::time::timeout(Duration::from_secs(10), async {
|
|
let start = Instant::now();
|
|
let mut buf = BytesMut::with_capacity(1024);
|
|
|
|
if is_https {
|
|
let stream = match proxy_port {
|
|
Some(pp) => {
|
|
let mut s = TcpStream::connect(format!("127.0.0.1:{pp}")).await?;
|
|
s.write_all(format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n").as_bytes())
|
|
.await?;
|
|
s.read_buf(&mut buf).await?;
|
|
if !buf.windows(3).any(|w| w == b"200") {
|
|
return Err(anyhow::anyhow!("Proxy CONNECT failed"));
|
|
}
|
|
s
|
|
}
|
|
None => TcpStream::connect(format!("{host}:{port}")).await?,
|
|
};
|
|
let connector = tokio_rustls::TlsConnector::from(Arc::clone(&TLS_CONFIG));
|
|
let server_name = rustls::pki_types::ServerName::try_from(host.as_str())
|
|
.map_err(|_| anyhow::anyhow!("Invalid DNS name: {host}"))?
|
|
.to_owned();
|
|
connector.connect(server_name, stream).await?;
|
|
} else {
|
|
let (mut stream, req) = match proxy_port {
|
|
Some(pp) => (
|
|
TcpStream::connect(format!("127.0.0.1:{pp}")).await?,
|
|
format!("HEAD {url} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"),
|
|
),
|
|
None => (
|
|
TcpStream::connect(format!("{host}:{port}")).await?,
|
|
format!("HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"),
|
|
),
|
|
};
|
|
stream.write_all(req.as_bytes()).await?;
|
|
let _ = stream.read(&mut buf).await?;
|
|
}
|
|
|
|
Ok((start.elapsed().as_millis() as u32).max(1))
|
|
})
|
|
.await
|
|
.unwrap_or(Ok(10000u32))
|
|
}
|