fix(proxy): unify node switching and connection cleanup

Route frontend and tray node selections through the same backend flow for
selection, persistence, state refresh, and connection cleanup. Read the
group's previous selection before switching and preserve the original
rule of closing only connections whose chains contain that selection.

Run optional cleanup in the background and keep its failures separate
from successful selection. Remove redundant frontend effects and unused
IPC commands, and refresh the proxy view for every successful switch.

Refs: 32095daf90
Refs: bea0dde074
This commit is contained in:
Tunglies
2026-11-07 19:53:24 +08:00
parent 603e616f73
commit 0218924c3c
12 changed files with 115 additions and 255 deletions
+3 -57
View File
@@ -5,16 +5,10 @@ use crate::{
core::{
handle::Handle,
proxy_view::{GroupScope, ProxyViewBuilder, ProxyViewInput, ProxyViewV1},
tray::Tray,
},
process::AsyncHandler,
};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping;
use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
use std::collections::{BTreeMap, HashSet};
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
@@ -27,15 +21,12 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
#[tauri::command]
#[specta::specta]
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
crate::config::profiles::forget_selected_node(&group_name)
pub async fn switch_proxy_node(group_name: String, proxy_name: String, unfix: bool) -> CmdResult<()> {
crate::feat::switch_proxy_node(&group_name, &proxy_name, unfix)
.await
.stringify_err()
}
static TRAY_SYNC_RUNNING: AtomicBool = AtomicBool::new(false);
static TRAY_SYNC_PENDING: AtomicBool = AtomicBool::new(false);
fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
let mut seen = HashSet::new();
@@ -124,48 +115,3 @@ pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
providers: providers.ok(),
}))
}
#[tauri::command]
#[specta::specta]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
if TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
AsyncHandler::spawn(move || async move {
run_tray_sync_loop().await;
});
} else {
TRAY_SYNC_PENDING.store(true, Ordering::Release);
}
Ok(())
}
async fn run_tray_sync_loop() {
// Permanent watcher: keeps syncing until a pass finds no pending sync request.
loop {
match Tray::global().update_menu().await {
Ok(_) => {
logging!(debug, Type::Cmd, "Tray proxy selection synced successfully");
}
Err(e) => {
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e:#}");
}
}
if !TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel) {
TRAY_SYNC_RUNNING.store(false, Ordering::Release);
if TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel)
&& TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
continue;
}
break;
}
}
}
+3 -1
View File
@@ -1000,7 +1000,9 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
Some((g, p)) => (g, p),
None => return,
};
feat::switch_proxy_node(group_name, proxy_name).await;
if let Err(err) = feat::switch_proxy_node(group_name, proxy_name, false).await {
logging!(error, Type::Tray, "Failed to switch proxy: {err:#}");
}
}
_ => {
logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id);
+73 -24
View File
@@ -4,6 +4,7 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe},
core::{CoreManager, handle, tray, validate::ValidationOutcome},
process::AsyncHandler,
utils::help::{mask_err, mask_url},
};
use anyhow::{Result, bail};
@@ -21,37 +22,85 @@ pub async fn toggle_proxy_profile(profile_index: String) {
}
}
/// Tell the profile which node this group is on now.
///
/// The core is already switched by the time this runs, so failing to record is not a reason to
/// report the switch as failed — but it is worth a log line, because what the profile holds is
/// what gets re-applied the next time a core starts.
async fn record_switched_node(group_name: &str, proxy_name: &str) {
if let Err(error) = crate::config::profiles::record_selected_node(group_name, proxy_name).await {
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str, unfix: bool) -> Result<()> {
let mihomo = handle::Handle::mihomo();
let previous_proxy = if Config::verge().await.data_arc().auto_close_connection.unwrap_or(false) {
match mihomo.get_proxy_by_name(group_name).await {
Ok(group) => group.now.filter(|name| !name.is_empty()),
Err(err) => {
logging!(
warn,
Type::Core,
"Failed to read previous proxy selection for {group_name}: {err}"
);
None
}
}
} else {
None
};
if unfix {
mihomo.unfixed_proxy(group_name).await?;
} else {
mihomo.select_node_for_group(group_name, proxy_name).await?;
}
let save_result = if unfix {
crate::config::profiles::forget_selected_node(group_name).await
} else {
crate::config::profiles::record_selected_node(group_name, proxy_name).await
};
// The core selection is already applied; persistence failure must not report it as failed.
if let Err(error) = save_result {
logging!(
warn,
Type::Config,
"切换代理成功但未能记录到配置: {} -> {}, 错误: {error:#}",
group_name,
proxy_name
"Failed to persist proxy selection for {group_name}: {error:#}"
);
}
}
announce(Refresh::Proxies);
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
match handle::Handle::mihomo()
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
record_switched_node(group_name, proxy_name).await;
announce(Refresh::Proxies);
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(error, Type::Tray, "切换代理失败: {err:?}");
}
if let Some(previous_proxy) = previous_proxy {
AsyncHandler::spawn(move || async move {
let connections = match mihomo.get_connections().await {
Ok(connections) => connections,
Err(err) => {
logging!(
warn,
Type::Core,
"Failed to read connections after switching proxy: {err}"
);
return;
}
};
futures::future::join_all(
connections
.connections
.unwrap_or_default()
.into_iter()
.filter(|connection| connection.chains.contains(&previous_proxy))
.map(|connection| async move {
if let Err(err) = mihomo.close_connection(&connection.id).await {
logging!(
warn,
Type::Core,
"Failed to close connection {} after switching proxy: {err}",
connection.id
);
}
}),
)
.await;
});
}
if let Err(err) = tray::Tray::global().update_menu().await {
logging!(
warn,
Type::Tray,
"Failed to refresh tray after switching proxy: {err:#}"
);
}
Ok(())
}
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
+1 -2
View File
@@ -45,9 +45,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::switch_proxy_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,