From 9a904af3aad3a1bdb8fd08c083daa0aee49fbe55 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Tue, 3 Nov 2026 10:48:44 +0000 Subject: [PATCH] refactor: backend cleanup (#8436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(utils): drop dead reset helper and tidy init_resources - remove resolve_reset_async, which had no callers - drop the duplicated time.*.com entry from the default fake-ip-filter - init_resources no longer recreates app_home (ensure_directories during init_config already created it) and now propagates res_dir creation errors * refactor(profile): remove identical blind retry when switching node The second select_node_for_group call in switch_proxy_node was a zero-interval retry with identical arguments. Its success path was a degraded copy of the first (missing the Refresh::Proxies announcement ever since the old force-refresh emit was removed), and its distinct "回退/最终失败" log wording misled maintainers. Keep a single call, the primary success aftermath, and one error log. * refactor(enhance): simplify tun mode check and lowercase helpers - Drop the always-true second clause in use_tun's enhanced-mode check: the key is absent only when the unwrap_or default already makes current_mode == "fake-ip" true. - Collapse use_lowercase into a delegation to use_lowercase_owned. * refactor(core): drop unreachable hotkey dtor and dedupe tray predicate Remove impl Drop for Hotkey: the instance lives in a OnceLock static via the singleton! macro and is never dropped; exit-time unregistration is already handled by Hotkey::reset(). Spell the include_proxy_groups/display-mode predicate identically at both evaluation sites in tray menu building; behavior is unchanged (Option != Some("disable") is equivalent to unwrap_or("default") != "disable"). * refactor(config): fold port guards into single helper The five IClashTemp port guards duplicated the same read/parse/default logic with drifting defaults: some hardcoded 7895/7898/7899 while others referenced network::ports constants. Route them all through one guard_port_value helper and use the constants everywhere, keeping each guard's name, key and cfg attributes. * refactor(config): dedupe prfitem factories and companion seeding from_rules/from_proxies/from_groups differed only in uid prefix, type string and template, so they now delegate to a shared from_seq_item. The five current_* accessors collapse into a local macro. The identical five-block companion seeding in from_local and from_url moves into ensure_companion_items, preserving seeding order and error propagation. * refactor(config): share patch macro and profiles helpers IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each hand-rolled the same copy-Some-overwrite pattern; they now share one patch_opt_field macro (the or-else merge form is field-for-field equivalent for Option fields). The four profiles *_safe wrappers that inlined Config::profiles().with_data_modify boilerplate delegate to a with_profiles_edit helper, and record/forget_selected_node share the current-item lookup prelude via current_item_mut. * refactor(enhance): merge chain processing and drop unused builtin js payload - collapse the identical process_global_items/process_profile_items into one parameterized process_override_items (entry uid sources unchanged) - fold the identical rules/proxies/groups arms of ChainItem::from_async into a single arm with an inner dispatch - stop embedding builtin JS payloads never executed by any path: the enhance loop dispatches builtin items by uid to native implementations, so ChainItem::builtin now carries empty script data (uids unchanged) - source ProfileItems fallback literals from Default instead of a second hand-written copy in collect_profile_items * refactor(net): reuse webpki tls config and fallback Extract build_webpki_rooted_tls_config so the delay-test TLS connector and the reqwest clients share one static-webpki-roots builder; ALPN stays caller-specific (only reqwest offers h2). Collapse the duplicated platform-verifier -> static-webpki-roots retry in get/get_bytes into request_with_static_webpki_fallback, gated by the caller's certificate-verification setting. * refactor(app): extract common exit cleanup restart_app and quit duplicate the whole exit preparation: set the exiting flag, persist configs, run interactive cleanup, and on a failed core stop clear the flag, restore public DNS under the lifecycle lock on macOS, and send the stop-failed notice. Move that flow into run_exit_cleanup_flow next to clean_async, taking the notice variant as a parameter and returning the CleanupResult so each caller keeps its own tail: restart aborts before shutdown, quit always shuts down and exits with the cleanup code. * refactor(backup): share restore finalize path restore_webdav_backup and restore_local_backup duplicated the credential extraction (backup creation strips the WebDAV credentials, so both re-inject the live ones), the archive extraction and the finalize tail. Move that tail into a private restore_backup_archive helper so the two entries keep only their download/locate logic; the config-write lock scope and the off-runtime extraction land in one place. The webdav entry keeps its downloaded-file cleanup. * refactor(sysopt): drop redundant lock and busy-wait The proxy target pair sat behind an Arc with no readers and an Arc that was never cloned; both writers already serialize on update_lock, so store the pair inside that mutex and delete the RwLock. refresh_guard waited for the guard to become ready with a yield_now spin at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the same wait condition and lock scope without the spin. * refactor(sysopt): share macos bypass formatting The macOS default bypass list and its three-way composition (empty -> default, default + custom, custom only) were maintained twice: in sysopt's format_bypass and inline in proxy_control's service_bypass. Keep the literal once in constants and compose both paths through format_bypass; service_bypass keeps its NUL check and UTF-8 truncation. * refactor(core): dedupe window listeners, elevated runners, backup settings - Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label. - Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms. - Make the auto-backup watch channel the single settings source, replacing the duplicate Arc copy with a stored receiver. * refactor(enhance): fold lowercase helpers into single owned function use_lowercase_owned disappears; the surviving use_lowercase takes the owned Mapping and borrowed callers clone at the call site. * refactor(enhance): rename ChainSupport variants to Mihomo names Clash is a legacy name; the variants match the verge-mihomo and verge-mihomo-alpha cores they gate. * refactor(enhance): lowercase takes a reference and chain support is copy use_lowercase borrows the mapping and clones only the values; call sites no longer clone whole configs. ChainSupport is a fieldless enum and now Copy; builtin() registers each support pair without clones. * refactor(config): name prfitem constructor for what it builds new_seq_item states that a fresh item is built from a template; the from_ prefix wrongly implied an existing source. append_companion_item returns the uid directly: append_item bails on a missing uid, so the Option was always Some on the Ok path. * refactor(sysopt): name the proxy state lock for its full role The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock says what it guards; the guard locals follow. constants::proxy held a single sysproxy constant and is renamed sysproxy. * refactor: replace remaining clash meta naming with mihomo Doc comment, generated bindings and the prebuild sidecar helpers stop referring to the legacy Clash Meta name; installer legacy-process cleanup keeps the old executable names on purpose. * refactor(enhance): drop builtin core filtering that never discriminates Both valid cores matched a different ChainSupport variant, yet each variant registered the same uid pair, so the filtered builtin list was identical for stable and alpha. Invalid cores are coerced at startup (validate_and_fix_config / get_valid_clash_core) and the None arm of is_support was unreachable. builtin() now returns the items directly and the clash_core threading through ConfigValues goes away; re-add core discrimination when a builtin actually differs per core. * refactor(enhance): drop unreachable mode script guard merge_default_config overwrites the profile's mode with the verge-owned clash config, whose mode is always present (template seeds rule and patch semantics never clear it) and restricted to rule/global/direct; change_clash_mode is rejected by the core before persisting anything else. mode:script can therefore only arrive through a hand-crafted patch_clash call, and for that path an explicit invalid-mode validation error is more honest than silently rewriting to rule. mihomo rejects mode:script outright (invalid mode), and user merge/script chains run after the builtins, so the guard never covered them anyway. * refactor(enhance): call the hysteria alpn builtin directly builtin() existed to feed the uid-dispatch loop; with a single builtin the vec and match are ceremony. apply_builtin_scripts calls builtin_hy_alpn directly and to_script goes away; tests construct ChainItem literals the way the merge items already did. --- scripts/prebuild.mjs | 13 +- src-tauri/src/config/clash.rs | 69 ++----- src-tauri/src/config/mod.rs | 9 + src-tauri/src/config/prfitem.rs | 179 +++++++--------- src-tauri/src/config/profiles.rs | 102 ++++------ src-tauri/src/config/verge.rs | 160 +++++++-------- src-tauri/src/constants.rs | 7 + src-tauri/src/core/auto_backup.rs | 20 +- src-tauri/src/core/hotkey.rs | 9 - src-tauri/src/core/lightweight.rs | 95 +++++---- src-tauri/src/core/proxy_control.rs | 12 +- src-tauri/src/core/service.rs | 83 ++++---- src-tauri/src/core/sysopt.rs | 33 +-- src-tauri/src/core/tray/mod.rs | 3 +- src-tauri/src/core/win_uwp.rs | 13 +- src-tauri/src/enhance/builtin/meta_guard.js | 8 - src-tauri/src/enhance/builtin/meta_hy_alpn.js | 12 -- src-tauri/src/enhance/chain.rs | 74 +------ src-tauri/src/enhance/field.rs | 13 +- src-tauri/src/enhance/mod.rs | 191 ++++++------------ src-tauri/src/enhance/script.rs | 6 +- src-tauri/src/enhance/tun.rs | 2 +- src-tauri/src/feat/backup.rs | 79 ++++---- src-tauri/src/feat/clash.rs | 44 +--- src-tauri/src/feat/profile.rs | 15 -- src-tauri/src/feat/window.rs | 15 +- src-tauri/src/utils/init.rs | 7 +- src-tauri/src/utils/network.rs | 82 ++++---- src-tauri/src/utils/resolve/mod.rs | 14 -- src/services/bindings.ts | 4 +- 30 files changed, 521 insertions(+), 852 deletions(-) delete mode 100644 src-tauri/src/enhance/builtin/meta_guard.js delete mode 100644 src-tauri/src/enhance/builtin/meta_hy_alpn.js diff --git a/scripts/prebuild.mjs b/scripts/prebuild.mjs index 22dd96032..0230668ab 100644 --- a/scripts/prebuild.mjs +++ b/scripts/prebuild.mjs @@ -266,13 +266,13 @@ async function getLatestReleaseVersion() { } if (!META_MAP[`${platform}-${arch}`]) { - throw new Error(`clash meta unsupported platform "${platform}-${arch}"`) + throw new Error(`mihomo unsupported platform "${platform}-${arch}"`) } if (!META_ALPHA_MAP[`${platform}-${arch}`]) { - throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`) + throw new Error(`mihomo alpha unsupported platform "${platform}-${arch}"`) } -function clashMetaAlpha() { +function mihomoAlpha() { const name = META_ALPHA_MAP[`${platform}-${arch}`] const isWin = platform === 'win32' const urlExt = isWin ? 'zip' : 'gz' @@ -285,7 +285,7 @@ function clashMetaAlpha() { } } -function clashMeta() { +function mihomo() { const name = META_MAP[`${platform}-${arch}`] const isWin = platform === 'win32' const urlExt = isWin ? 'zip' : 'gz' @@ -733,13 +733,12 @@ const tasks = [ { name: 'verge-mihomo-alpha', func: () => - getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())), + getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())), retry: 5, }, { name: 'verge-mihomo', - func: () => - getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())), + func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())), retry: 5, }, // After both sidecar tasks: it hashes what they downloaded. diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 106c55d9f..59687278c 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -184,84 +184,41 @@ impl IClashTemp { _ => None, }) } - #[cfg(not(target_os = "windows"))] - fn guard_redir_port(config: &Mapping) -> u16 { + fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 { let mut port = config - .get("redir-port") + .get(key) .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) - .unwrap_or(7895); + .unwrap_or(default); if port == 0 { - port = 7895; + port = default; } port } + #[cfg(not(target_os = "windows"))] + fn guard_redir_port(config: &Mapping) -> u16 { + Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR) + } + #[cfg(target_os = "linux")] fn guard_tproxy_port(config: &Mapping) -> u16 { - let mut port = config - .get("tproxy-port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(network::ports::DEFAULT_TPROXY); - if port == 0 { - port = network::ports::DEFAULT_TPROXY; - } - port + Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY) } fn guard_mixed_port(config: &Mapping) -> u16 { - let raw_value = config.get("mixed-port"); - - let mut port = raw_value - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(network::ports::DEFAULT_MIXED); - - if port == 0 { - port = network::ports::DEFAULT_MIXED; - } - - port + Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED) } fn guard_socks_port(config: &Mapping) -> u16 { - let mut port = config - .get("socks-port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(7898); - if port == 0 { - port = 7898; - } - port + Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS) } fn guard_port(config: &Mapping) -> u16 { - let mut port = config - .get("port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(7899); - if port == 0 { - port = 7899; - } - port + Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP) } pub(super) fn guard_server_ctrl(config: &Mapping) -> String { diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index add685f62..1be472f88 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -14,6 +14,15 @@ mod verge; pub(crate) use self::config::Config; pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; +macro_rules! patch_opt_field { + ($lv:expr, $rv:expr, $key:tt) => { + if ($rv.$key).is_some() { + $lv.$key = $rv.$key.clone(); + } + }; +} +pub(crate) use patch_opt_field; + pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index da8728d21..eafb29cf9 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -1,3 +1,5 @@ +use super::patch_opt_field; + use crate::{ config::profiles, utils::{ @@ -131,19 +133,18 @@ impl PrfOption { match (one, other) { (Some(a_ref), Some(b_ref)) => { let mut result = a_ref.clone(); - result.user_agent = b_ref.user_agent.clone().or(result.user_agent); - result.with_proxy = b_ref.with_proxy.or(result.with_proxy); - result.self_proxy = b_ref.self_proxy.or(result.self_proxy); - result.danger_accept_invalid_certs = - b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); - result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); - result.update_interval = b_ref.update_interval.or(result.update_interval); - result.merge = b_ref.merge.clone().or(result.merge); - result.script = b_ref.script.clone().or(result.script); - result.rules = b_ref.rules.clone().or(result.rules); - result.proxies = b_ref.proxies.clone().or(result.proxies); - result.groups = b_ref.groups.clone().or(result.groups); - result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds); + patch_opt_field!(result, b_ref, user_agent); + patch_opt_field!(result, b_ref, with_proxy); + patch_opt_field!(result, b_ref, self_proxy); + patch_opt_field!(result, b_ref, danger_accept_invalid_certs); + patch_opt_field!(result, b_ref, allow_auto_update); + patch_opt_field!(result, b_ref, update_interval); + patch_opt_field!(result, b_ref, merge); + patch_opt_field!(result, b_ref, script); + patch_opt_field!(result, b_ref, rules); + patch_opt_field!(result, b_ref, proxies); + patch_opt_field!(result, b_ref, groups); + patch_opt_field!(result, b_ref, timeout_seconds); Some(result) } (Some(a_ref), None) => Some(a_ref.clone()), @@ -181,6 +182,38 @@ impl PrfItem { } } + async fn append_companion_item(item: &mut Self) -> Result { + profiles::profiles_append_item_safe(item).await?; + item.uid + .clone() + .ok_or_else(|| anyhow::anyhow!("appended companion item has no uid")) + } + + async fn ensure_companion_items( + merge: &mut Option, + script: &mut Option, + rules: &mut Option, + proxies: &mut Option, + groups: &mut Option, + ) -> Result<()> { + if merge.is_none() { + *merge = Some(Self::append_companion_item(&mut Self::from_merge(None)).await?); + } + if script.is_none() { + *script = Some(Self::append_companion_item(&mut Self::from_script(None)).await?); + } + if rules.is_none() { + *rules = Some(Self::append_companion_item(&mut Self::from_rules()).await?); + } + if proxies.is_none() { + *proxies = Some(Self::append_companion_item(&mut Self::from_proxies()).await?); + } + if groups.is_none() { + *groups = Some(Self::append_companion_item(&mut Self::from_groups()).await?); + } + Ok(()) + } + async fn from_local( name: String, desc: String, @@ -197,31 +230,7 @@ impl PrfItem { let mut proxies = opt_ref.and_then(|o| o.proxies.clone()); let mut groups = opt_ref.and_then(|o| o.groups.clone()); - if merge.is_none() { - let merge_item = &mut Self::from_merge(None); - profiles::profiles_append_item_safe(merge_item).await?; - merge = merge_item.uid.clone(); - } - if script.is_none() { - let script_item = &mut Self::from_script(None); - profiles::profiles_append_item_safe(script_item).await?; - script = script_item.uid.clone(); - } - if rules.is_none() { - let rules_item = &mut Self::from_rules(); - profiles::profiles_append_item_safe(rules_item).await?; - rules = rules_item.uid.clone(); - } - if proxies.is_none() { - let proxies_item = &mut Self::from_proxies(); - profiles::profiles_append_item_safe(proxies_item).await?; - proxies = proxies_item.uid.clone(); - } - if groups.is_none() { - let groups_item = &mut Self::from_groups(); - profiles::profiles_append_item_safe(groups_item).await?; - groups = groups_item.uid.clone(); - } + Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?; Ok(Self { uid: Some(uid), itype: Some("local".into()), @@ -375,31 +384,7 @@ impl PrfItem { bail!("profile does not contain `proxies` or `proxy-providers`"); } - if merge.is_none() { - let merge_item = &mut Self::from_merge(None); - profiles::profiles_append_item_safe(merge_item).await?; - merge = merge_item.uid.clone(); - } - if script.is_none() { - let script_item = &mut Self::from_script(None); - profiles::profiles_append_item_safe(script_item).await?; - script = script_item.uid.clone(); - } - if rules.is_none() { - let rules_item = &mut Self::from_rules(); - profiles::profiles_append_item_safe(rules_item).await?; - rules = rules_item.uid.clone(); - } - if proxies.is_none() { - let proxies_item = &mut Self::from_proxies(); - profiles::profiles_append_item_safe(proxies_item).await?; - proxies = proxies_item.uid.clone(); - } - if groups.is_none() { - let groups_item = &mut Self::from_groups(); - profiles::profiles_append_item_safe(groups_item).await?; - groups = groups_item.uid.clone(); - } + Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?; Ok(Self { uid: Some(uid), @@ -461,46 +446,30 @@ impl PrfItem { } } - fn from_rules() -> Self { - let uid = help::get_uid("r").into(); + fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self { + let uid = help::get_uid(prefix).into(); let file = format!("{uid}.yaml").into(); // yaml ext Self { uid: Some(uid), - itype: Some("rules".into()), + itype: Some(itype.into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_RULES.into()), + file_data: Some(template.into()), ..Default::default() } } + fn from_rules() -> Self { + Self::new_seq_item("r", "rules", tmpl::ITEM_RULES) + } + fn from_proxies() -> Self { - let uid = help::get_uid("p").into(); - let file = format!("{uid}.yaml").into(); // yaml ext - - Self { - uid: Some(uid), - itype: Some("proxies".into()), - file: Some(file), - updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_PROXIES.into()), - ..Default::default() - } + Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES) } fn from_groups() -> Self { - let uid = help::get_uid("g").into(); - let file = format!("{uid}.yaml").into(); // yaml ext - - Self { - uid: Some(uid), - itype: Some("groups".into()), - file: Some(file), - updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_GROUPS.into()), - ..Default::default() - } + Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS) } pub(crate) async fn read_file(&self) -> Result { @@ -516,26 +485,20 @@ impl PrfItem { } } +macro_rules! current_field { + ($name:ident, $field:ident) => { + pub(crate) fn $name(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.$field.as_ref()) + } + }; +} + impl PrfItem { - pub(crate) fn current_merge(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.merge.as_ref()) - } - - pub(crate) fn current_script(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.script.as_ref()) - } - - pub(crate) fn current_rules(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.rules.as_ref()) - } - - pub(crate) fn current_proxies(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.proxies.as_ref()) - } - - pub(crate) fn current_groups(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.groups.as_ref()) - } + current_field!(current_merge, merge); + current_field!(current_script, script); + current_field!(current_rules, rules); + current_field!(current_proxies, proxies); + current_field!(current_groups, groups); } fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool { diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 4b149b716..23826cdc1 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,5 +1,5 @@ use super::{ - PrfOption, + PrfOption, patch_opt_field, prfitem::{PrfItem, PrfSelected, normalize_profile_home_url}, }; use crate::core::notify::{Refresh, announce}; @@ -86,14 +86,6 @@ impl ProfileDeletePlan { } } -macro_rules! patch { - ($lv: expr, $rv: expr, $key: tt) => { - if ($rv.$key).is_some() { - $lv.$key = $rv.$key.to_owned(); - } - }; -} - impl IProfiles { fn take_item_file_by_uid(items: &mut Vec, target_uid: Option<&str>) -> Option { let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?; @@ -244,15 +236,15 @@ impl IProfiles { for each in items.iter_mut() { if each.uid.as_deref() == Some(uid) { - patch!(each, item, itype); - patch!(each, item, name); - patch!(each, item, desc); - patch!(each, item, file); - patch!(each, item, url); - patch!(each, item, selected); - patch!(each, item, extra); - patch!(each, item, updated); - patch!(each, item, option); + patch_opt_field!(each, item, itype); + patch_opt_field!(each, item, name); + patch_opt_field!(each, item, desc); + patch_opt_field!(each, item, file); + patch_opt_field!(each, item, url); + patch_opt_field!(each, item, selected); + patch_opt_field!(each, item, extra); + patch_opt_field!(each, item, updated); + patch_opt_field!(each, item, option); self.items = Some(items); return self.save_file().await; @@ -420,6 +412,21 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file profiles_append_item_safe(item).await } +/// Applies `edit` to the committed profiles; commits only when the edit succeeds. +async fn with_profiles_edit(edit: F) -> Result<()> +where + F: FnOnce(IProfiles) -> Fut + Send, + Fut: std::future::Future> + Send, +{ + Config::profiles() + .await + .with_data_modify(|profiles| async move { + let profiles = edit(profiles).await?; + Ok((profiles, ())) + }) + .await +} + pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { let profiles = Config::profiles().await; profiles_append_item_to_safe(&profiles, item).await @@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft, it } pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.patch_item(index, item).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await } pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.reorder(active_id, over_id).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await } pub(crate) async fn profiles_save_file_safe() -> Result<()> { - Config::profiles() - .await - .with_data_modify(|profiles| async move { - profiles.save_file().await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await } pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.update_item(index, item).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await } #[derive(Debug, PartialEq, Eq)] @@ -713,19 +696,21 @@ where Ok(()) } +fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> { + let current = profiles.current.clone()?; + profiles + .items + .as_mut()? + .iter_mut() + .find(|item| item.uid.as_ref() == Some(¤t)) +} + /// Records a backend-made selection so the next core start restores it. pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> { let group_name = String::from(group_name); let node = String::from(node); persist_selected_update(true, move |mut profiles| async move { - let Some(current) = profiles.current.clone() else { - return Ok((profiles, false)); - }; - let Some(item) = profiles - .items - .as_mut() - .and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t))) - else { + let Some(item) = current_item_mut(&mut profiles) else { return Ok((profiles, false)); }; @@ -760,14 +745,7 @@ fn remove_selected_node(selected: &mut Vec, group_name: &str) -> bo pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> { persist_selected_update(true, move |mut profiles| async move { - let Some(current) = profiles.current.clone() else { - return Ok((profiles, false)); - }; - let Some(item) = profiles - .items - .as_mut() - .and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t))) - else { + let Some(item) = current_item_mut(&mut profiles) else { return Ok((profiles, false)); }; diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 45b0e18ec..d3b58dcf0 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::{ - config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, + config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted}, constants::network, utils::{dirs, help}, }; @@ -39,7 +39,7 @@ pub struct IVerge { pub traffic_graph: Option, - /// show memory info (only for Clash Meta) + /// show memory info (only for Mihomo) pub enable_memory_usage: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -432,98 +432,90 @@ impl IVerge { #[allow(clippy::cognitive_complexity)] pub fn patch_config(&mut self, patch: &Self) { - macro_rules! patch { - ($key: tt) => { - if patch.$key.is_some() { - self.$key = patch.$key.clone(); - } - }; - } + patch_opt_field!(self, patch, app_log_level); + patch_opt_field!(self, patch, app_log_max_size); + patch_opt_field!(self, patch, app_log_max_count); - patch!(app_log_level); - patch!(app_log_max_size); - patch!(app_log_max_count); - - patch!(language); - patch!(theme_mode); - patch!(tray_event); - patch!(env_type); - patch!(start_page); - patch!(startup_script); - patch!(traffic_graph); - patch!(enable_memory_usage); - patch!(enable_group_icon); - patch!(pause_render_traffic_stats_on_blur); + patch_opt_field!(self, patch, language); + patch_opt_field!(self, patch, theme_mode); + patch_opt_field!(self, patch, tray_event); + patch_opt_field!(self, patch, env_type); + patch_opt_field!(self, patch, start_page); + patch_opt_field!(self, patch, startup_script); + patch_opt_field!(self, patch, traffic_graph); + patch_opt_field!(self, patch, enable_memory_usage); + patch_opt_field!(self, patch, enable_group_icon); + patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur); #[cfg(target_os = "macos")] - patch!(tray_icon); - patch!(menu_icon); - patch!(menu_order); - patch!(proxy_group_tools_position); - patch!(notice_position); - patch!(collapse_navbar); - patch!(common_tray_icon); - patch!(sysproxy_tray_icon); - patch!(tun_tray_icon); + patch_opt_field!(self, patch, tray_icon); + patch_opt_field!(self, patch, menu_icon); + patch_opt_field!(self, patch, menu_order); + patch_opt_field!(self, patch, proxy_group_tools_position); + patch_opt_field!(self, patch, notice_position); + patch_opt_field!(self, patch, collapse_navbar); + patch_opt_field!(self, patch, common_tray_icon); + patch_opt_field!(self, patch, sysproxy_tray_icon); + patch_opt_field!(self, patch, tun_tray_icon); - patch!(enable_tun_mode); - patch!(enable_auto_launch); - patch!(enable_silent_start); - patch!(enable_hover_jump_navigator); - patch!(hover_jump_navigator_delay); + patch_opt_field!(self, patch, enable_tun_mode); + patch_opt_field!(self, patch, enable_auto_launch); + patch_opt_field!(self, patch, enable_silent_start); + patch_opt_field!(self, patch, enable_hover_jump_navigator); + patch_opt_field!(self, patch, hover_jump_navigator_delay); #[cfg(not(target_os = "windows"))] - patch!(verge_redir_port); + patch_opt_field!(self, patch, verge_redir_port); #[cfg(not(target_os = "windows"))] - patch!(verge_redir_enabled); + patch_opt_field!(self, patch, verge_redir_enabled); #[cfg(target_os = "linux")] - patch!(verge_tproxy_port); + patch_opt_field!(self, patch, verge_tproxy_port); #[cfg(target_os = "linux")] - patch!(verge_tproxy_enabled); - patch!(verge_mixed_port); - patch!(verge_socks_port); - patch!(verge_socks_enabled); - patch!(verge_port); - patch!(verge_http_enabled); - patch!(enable_system_proxy); - patch!(enable_proxy_guard); - patch!(enable_bypass_check); - patch!(use_default_bypass); - patch!(system_proxy_bypass); - patch!(proxy_guard_duration); - patch!(proxy_auto_config); - patch!(pac_file_content); - patch!(proxy_host); - patch!(theme_setting); - patch!(web_ui_list); - patch!(clash_core); - patch!(hotkeys); - patch!(enable_global_hotkey); + patch_opt_field!(self, patch, verge_tproxy_enabled); + patch_opt_field!(self, patch, verge_mixed_port); + patch_opt_field!(self, patch, verge_socks_port); + patch_opt_field!(self, patch, verge_socks_enabled); + patch_opt_field!(self, patch, verge_port); + patch_opt_field!(self, patch, verge_http_enabled); + patch_opt_field!(self, patch, enable_system_proxy); + patch_opt_field!(self, patch, enable_proxy_guard); + patch_opt_field!(self, patch, enable_bypass_check); + patch_opt_field!(self, patch, use_default_bypass); + patch_opt_field!(self, patch, system_proxy_bypass); + patch_opt_field!(self, patch, proxy_guard_duration); + patch_opt_field!(self, patch, proxy_auto_config); + patch_opt_field!(self, patch, pac_file_content); + patch_opt_field!(self, patch, proxy_host); + patch_opt_field!(self, patch, theme_setting); + patch_opt_field!(self, patch, web_ui_list); + patch_opt_field!(self, patch, clash_core); + patch_opt_field!(self, patch, hotkeys); + patch_opt_field!(self, patch, enable_global_hotkey); - patch!(auto_close_connection); - patch!(auto_check_update); - patch!(default_latency_test); - patch!(default_latency_timeout); - patch!(enable_auto_delay_detection); - patch!(auto_delay_detection_interval_minutes); - patch!(enable_builtin_enhanced); - patch!(proxy_layout_column); - patch!(test_list); - patch!(auto_log_clean); - patch!(enable_auto_backup_schedule); - patch!(auto_backup_interval_hours); - patch!(auto_backup_on_change); + patch_opt_field!(self, patch, auto_close_connection); + patch_opt_field!(self, patch, auto_check_update); + patch_opt_field!(self, patch, default_latency_test); + patch_opt_field!(self, patch, default_latency_timeout); + patch_opt_field!(self, patch, enable_auto_delay_detection); + patch_opt_field!(self, patch, auto_delay_detection_interval_minutes); + patch_opt_field!(self, patch, enable_builtin_enhanced); + patch_opt_field!(self, patch, proxy_layout_column); + patch_opt_field!(self, patch, test_list); + patch_opt_field!(self, patch, auto_log_clean); + patch_opt_field!(self, patch, enable_auto_backup_schedule); + patch_opt_field!(self, patch, auto_backup_interval_hours); + patch_opt_field!(self, patch, auto_backup_on_change); - patch!(webdav_url); - patch!(webdav_username); - patch!(webdav_password); + patch_opt_field!(self, patch, webdav_url); + patch_opt_field!(self, patch, webdav_username); + patch_opt_field!(self, patch, webdav_password); #[cfg(target_os = "macos")] - patch!(enable_tray_speed); - patch!(tray_proxy_groups_display_mode); - patch!(tray_inline_outbound_modes); - patch!(enable_auto_light_weight_mode); - patch!(auto_light_weight_minutes); - patch!(enable_dns_settings); - patch!(home_cards); - patch!(enable_external_controller); + patch_opt_field!(self, patch, enable_tray_speed); + patch_opt_field!(self, patch, tray_proxy_groups_display_mode); + patch_opt_field!(self, patch, tray_inline_outbound_modes); + patch_opt_field!(self, patch, enable_auto_light_weight_mode); + patch_opt_field!(self, patch, auto_light_weight_minutes); + patch_opt_field!(self, patch, enable_dns_settings); + patch_opt_field!(self, patch, home_cards); + patch_opt_field!(self, patch, enable_external_controller); } pub fn get_log_level(&self) -> LevelFilter { diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index da67ce13e..3f85ced31 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -14,6 +14,13 @@ pub mod network { } } +pub mod sysproxy { + /// Single source of the macOS default bypass list; both proxy backends compose from it. + #[cfg(target_os = "macos")] + pub const MACOS_DEFAULT_BYPASS: &str = + "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; +} + pub mod timing { use super::Duration; diff --git a/src-tauri/src/core/auto_backup.rs b/src-tauri/src/core/auto_backup.rs index 9b0fd9dad..dad3f62c1 100644 --- a/src-tauri/src/core/auto_backup.rs +++ b/src-tauri/src/core/auto_backup.rs @@ -7,11 +7,10 @@ use crate::{ use anyhow::Result; use chrono::Local; use clash_verge_logging::{Type, logging}; -use parking_lot::RwLock; use std::{ path::PathBuf, sync::{ - Arc, OnceLock, + OnceLock, atomic::{AtomicBool, AtomicI64, Ordering}, }, time::{Duration, UNIX_EPOCH}, @@ -82,8 +81,9 @@ impl Default for AutoBackupSettings { } pub struct AutoBackupManager { - settings: Arc>, settings_tx: watch::Sender, + // The watch channel is the single settings store; no duplicate copies. + settings_rx: watch::Receiver, runner_started: AtomicBool, exec_lock: Mutex<()>, last_backup: AtomicI64, @@ -93,10 +93,10 @@ impl AutoBackupManager { pub fn global() -> &'static Self { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { - let (tx, _rx) = watch::channel(AutoBackupSettings::default()); + let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default()); Self { - settings: Arc::new(RwLock::new(AutoBackupSettings::default())), - settings_tx: tx, + settings_tx, + settings_rx, runner_started: AtomicBool::new(false), exec_lock: Mutex::new(()), last_backup: AtomicI64::new(0), @@ -106,9 +106,6 @@ impl AutoBackupManager { pub async fn init(&self) -> Result<()> { let settings = Self::load_settings().await; - { - *self.settings.write() = settings; - } let _ = self.settings_tx.send_replace(settings); self.maybe_start_runner(settings); Ok(()) @@ -116,9 +113,6 @@ impl AutoBackupManager { pub async fn refresh_settings(&self) -> Result<()> { let settings = Self::load_settings().await; - { - *self.settings.write() = settings; - } let _ = self.settings_tx.send_replace(settings); self.maybe_start_runner(settings); Ok(()) @@ -193,7 +187,7 @@ impl AutoBackupManager { } async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { - let snapshot = *self.settings.read(); + let snapshot = *self.settings_rx.borrow(); if trigger.is_schedule() && !snapshot.schedule_enabled { return Ok(()); diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 75027d176..d23ae2a57 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -466,12 +466,3 @@ impl Hotkey { (del_list, add_list) } } - -impl Drop for Hotkey { - fn drop(&mut self) { - let app_handle = handle::Handle::app_handle(); - if let Err(e) = app_handle.global_shortcut().unregister_all() { - logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e); - } - } -} diff --git a/src-tauri/src/core/lightweight.rs b/src-tauri/src/core/lightweight.rs index 9b4cb0041..fd1e4d363 100644 --- a/src-tauri/src/core/lightweight.rs +++ b/src-tauri/src/core/lightweight.rs @@ -40,8 +40,47 @@ impl LightweightState { static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); -static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); -static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); +struct WindowListener { + handler_id: AtomicU32, + event: &'static str, + label: &'static str, +} + +impl WindowListener { + fn setup(&self, on_event: impl Fn() + Send + 'static) { + if let Some(window) = WindowManager::get_main_window() { + let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel); + if previous_handler_id != 0 { + window.unlisten(previous_handler_id); + logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label); + } + let handler_id = window.listen(self.event, move |_event| on_event()); + self.handler_id.store(handler_id, Ordering::Release); + } + } + + fn cancel(&self) { + let id = self.handler_id.swap(0, Ordering::AcqRel); + if id != 0 { + if let Some(window) = WindowManager::get_main_window() { + window.unlisten(id); + } + logging!(debug, Type::Lightweight, "取消了{}监听", self.label); + } + } +} + +static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener { + handler_id: AtomicU32::new(0), + event: "tauri://close-requested", + label: "窗口关闭", +}; + +static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener { + handler_id: AtomicU32::new(0), + event: "tauri://focus", + label: "窗口焦点", +}; static CANCEL_TX: Mutex>> = Mutex::new(None); @@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() { } fn setup_window_close_listener() { - if let Some(window) = WindowManager::get_main_window() { - let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); - if previous_handler_id != 0 { - window.unlisten(previous_handler_id); - logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听"); - } - let handler_id = window.listen("tauri://close-requested", move |_event| { - std::mem::drop(AsyncHandler::spawn(|| async { - setup_light_weight_timer().await; - })); - logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); - }); - WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release); - } + WINDOW_CLOSE_LISTENER.setup(|| { + std::mem::drop(AsyncHandler::spawn(|| async { + setup_light_weight_timer().await; + })); + logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); + }); } fn cancel_window_close_listener() { - let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); - if id != 0 { - if let Some(window) = WindowManager::get_main_window() { - window.unlisten(id); - } - logging!(debug, Type::Lightweight, "取消了窗口关闭监听"); - } + WINDOW_CLOSE_LISTENER.cancel(); } fn setup_webview_focus_listener() { - if let Some(window) = WindowManager::get_main_window() { - let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); - if previous_handler_id != 0 { - window.unlisten(previous_handler_id); - logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听"); - } - let handler_id = window.listen("tauri://focus", move |_event| { - cancel_light_weight_timer(); - logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); - }); - WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release); - } + WEBVIEW_FOCUS_LISTENER.setup(|| { + cancel_light_weight_timer(); + logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); + }); } fn cancel_webview_focus_listener() { - let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); - if id != 0 { - if let Some(window) = WindowManager::get_main_window() { - window.unlisten(id); - } - logging!(debug, Type::Lightweight, "取消了窗口焦点监听"); - } + WEBVIEW_FOCUS_LISTENER.cancel(); } async fn setup_light_weight_timer() { diff --git a/src-tauri/src/core/proxy_control.rs b/src-tauri/src/core/proxy_control.rs index e9f482247..19bc09456 100644 --- a/src-tauri/src/core/proxy_control.rs +++ b/src-tauri/src/core/proxy_control.rs @@ -6,7 +6,7 @@ use crate::{ notification::{self, FailedOperation}, runstate::RUN_STATE, service, - sysopt::Sysopt, + sysopt::{Sysopt, format_bypass}, }, process::AsyncHandler, utils::{ @@ -204,8 +204,6 @@ const fn refusal_classification(service_usable: Option) -> SysproxyFailure } const LOOPBACK_HOST: &str = "127.0.0.1"; -const MACOS_DEFAULT_BYPASS: &str = - "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; const MAX_SERVICE_BYPASS_LEN: usize = 8192; static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new(); @@ -327,13 +325,7 @@ fn service_bypass(verge: &IVerge) -> Result { let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); ensure!(!custom.contains('\0'), "system proxy bypass contains NUL"); - let mut bypass = if custom.is_empty() { - MACOS_DEFAULT_BYPASS.to_owned() - } else if verge.use_default_bypass.unwrap_or(true) { - format!("{MACOS_DEFAULT_BYPASS},{custom}") - } else { - custom.to_owned() - }; + let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into(); truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN); Ok(bypass) } diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index d21331d86..97426598a 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -603,50 +603,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R } #[cfg(target_os = "windows")] -fn uninstall_service() -> Result<()> { - logging!(info, Type::Service, "uninstall service"); - +pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; use std::os::windows::process::CommandExt as _; - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || { - Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe")) - })?; - - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let token = Token::with_current_process()?; let level = token.privilege_level()?; let status = match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, - _ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?, + PrivilegeLevel::NotPrivileged => { + let mut command = RunasCommand::new(program); + if hide_window { + command.show(false); + } + command.status()? + } + _ => { + let mut command = StdCommand::new(program); + if hide_window { + // 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false). + command.creation_flags(0x08000000); + } + command.status()? + } }; + Ok(status) +} +fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result) -> Result { + let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?; + if !uninstall_path.exists() { + bail!(format!("uninstaller not found: {uninstall_path:?}")); + } + Ok(uninstall_path) +} + +fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> { if !status.success() { bail!( "failed to uninstall service with status {}", status.code().unwrap_or(-1) ); } - Ok(()) } +#[cfg(target_os = "windows")] +fn uninstall_service() -> Result<()> { + logging!(info, Type::Service, "uninstall service"); + + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || { + Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe")) + })?; + + let status = run_maybe_elevated(&uninstall_path, true)?; + ensure_uninstall_success(&status) +} + #[cfg(target_os = "linux")] fn uninstall_service() -> Result<()> { logging!(info, Type::Service, "uninstall service"); - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || { Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall")) })?; - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let elevator = crate::utils::help::linux_elevator(); let status = if linux_running_as_root() { StdCommand::new(&uninstall_path).status()? @@ -676,14 +697,7 @@ fn uninstall_service() -> Result<()> { status.code().unwrap_or(-1) ); - if !status.success() { - bail!( - "failed to uninstall service with status {}", - status.code().unwrap_or(-1) - ); - } - - Ok(()) + ensure_uninstall_success(&status) } #[cfg(target_os = "linux")] @@ -698,14 +712,10 @@ fn linux_running_as_root() -> bool { fn uninstall_service() -> Result<()> { logging!(info, Type::Service, "uninstall service"); - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || { Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall")) })?; - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let uninstall_path = macos_service_tool_path(&uninstall_path)?; let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned(); @@ -721,14 +731,7 @@ fn uninstall_service() -> Result<()> { let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; - if !status.success() { - bail!( - "failed to uninstall service with status {}", - status.code().unwrap_or(-1) - ); - } - - Ok(()) + ensure_uninstall_success(&status) } fn install_service() -> Result<()> { diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 9b3d158dc..c32b4ac15 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState } pub(crate) struct Sysopt { - update_lock: TokioMutex<()>, + // Owns the proxy target pair; every reader and writer goes through this lock. + proxy_state_lock: TokioMutex<(Sysproxy, Autoproxy)>, guard_operation_lock: TokioMutex<()>, reset_sysproxy: AtomicBool, - inner_proxy: Arc>, guard: Arc>, } impl Default for Sysopt { fn default() -> Self { Self { - update_lock: TokioMutex::new(()), + proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())), guard_operation_lock: TokioMutex::new(()), reset_sysproxy: AtomicBool::new(false), - inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), } } @@ -269,10 +268,9 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172 #[cfg(any(target_os = "linux", target_os = "macos"))] static BYPASS_SEPARATOR: &str = ","; #[cfg(target_os = "macos")] -static DEFAULT_BYPASS: &str = - "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; +static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS; -fn format_bypass(use_default: bool, custom_bypass: &str) -> String { +pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String { if custom_bypass.is_empty() { DEFAULT_BYPASS.into() } else if use_default { @@ -361,7 +359,7 @@ impl Sysopt { } } while self.access_guard().read().get_state().is_pendding() { - tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(10)).await; } // `start()` false may also mean a guard was already running. !self.access_guard().read().get_state().is_stopped() @@ -409,18 +407,21 @@ impl Sysopt { /// configuration instead of a partially-applied one (e.g. SOCKS already /// disabled but HTTP still enabled mid-transition). pub(crate) async fn wait_idle(&self) { - let _ = self.update_lock.lock().await; + let _ = self.proxy_state_lock.lock().await; } /// init the sysproxy + #[allow( + clippy::significant_drop_tightening, + reason = "the proxy state lock must stay held until the OS write finishes; wait_idle relies on it" + )] pub(super) async fn update_sysproxy(&self) -> Result<()> { - let _lock = self.update_lock.lock().await; + let mut proxy_state = self.proxy_state_lock.lock().await; let verge = Config::verge().await.latest_arc(); // Configured, not live: this runs while the Core is being started or restarted, and // asking a Core that is not up yet would only fall back here anyway. let port = MixedPort::desired().await; let pac_port = server::embedded_server_port()?; - // 先 await, 避免持有锁导致的 Send 问题 let bypass = get_bypass().await; let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( @@ -431,7 +432,7 @@ impl Sysopt { ); let (sys, auto, guard_type) = { - let (sys, auto) = &mut *self.inner_proxy.write(); + let (sys, auto) = &mut *proxy_state; sys.host = proxy_host.into(); sys.port = port; sys.bypass = bypass.into(); @@ -528,6 +529,10 @@ impl Sysopt { } /// reset the sysproxy + #[allow( + clippy::significant_drop_tightening, + reason = "the proxy state lock must stay held until the disable finishes; wait_idle relies on it" + )] pub(super) async fn reset_sysproxy(&self) -> Result<()> { if self .reset_sysproxy @@ -539,13 +544,13 @@ impl Sysopt { defer! { self.reset_sysproxy.store(false, Ordering::SeqCst); } - let _lock = self.update_lock.lock().await; + let mut proxy_state = self.proxy_state_lock.lock().await; let _guard_operation = self.guard_operation_lock.lock().await; let drained = self.stop_proxy_guard_locked().await; // 直接关闭所有代理 let (sys, auto) = { - let (sys, auto) = &mut *self.inner_proxy.write(); + let (sys, auto) = &mut *proxy_state; sys.enable = false; auto.enable = false; (sys.clone(), auto.clone()) diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index ae8c63c2c..f7fa4a6d6 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -636,7 +636,8 @@ async fn create_tray_menu( .tray_proxy_groups_display_mode .as_deref() .unwrap_or("default"); - let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable"; + let include_proxy_groups = + options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable"); let proxy_group_order_map = runtime_proxy_groups_order; diff --git a/src-tauri/src/core/win_uwp.rs b/src-tauri/src/core/win_uwp.rs index 3db9a1a44..47a186a0a 100644 --- a/src-tauri/src/core/win_uwp.rs +++ b/src-tauri/src/core/win_uwp.rs @@ -1,10 +1,7 @@ #![cfg(target_os = "windows")] -use crate::utils::dirs; +use crate::{core::service, utils::dirs}; use anyhow::{Result, bail}; -use deelevate::{PrivilegeLevel, Token}; -use runas::Command as RunasCommand; -use std::process::Command as StdCommand; pub fn invoke_uwptools() -> Result<()> { let resource_dir = dirs::app_resources_dir()?; @@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> { bail!("enableLoopback exe not found"); } - let token = Token::with_current_process()?; - let level = token.privilege_level()?; - - match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?, - _ => StdCommand::new(tool_path).status()?, - }; + service::run_maybe_elevated(&tool_path, false)?; Ok(()) } diff --git a/src-tauri/src/enhance/builtin/meta_guard.js b/src-tauri/src/enhance/builtin/meta_guard.js deleted file mode 100644 index d427974c1..000000000 --- a/src-tauri/src/enhance/builtin/meta_guard.js +++ /dev/null @@ -1,8 +0,0 @@ -// This function is exported for use by the Clash core -// eslint-disable-next-line unused-imports/no-unused-vars -function main(config, _name) { - if (config.mode === 'script') { - config.mode = 'rule' - } - return config -} diff --git a/src-tauri/src/enhance/builtin/meta_hy_alpn.js b/src-tauri/src/enhance/builtin/meta_hy_alpn.js deleted file mode 100644 index d7e57dd2a..000000000 --- a/src-tauri/src/enhance/builtin/meta_hy_alpn.js +++ /dev/null @@ -1,12 +0,0 @@ -// This function is exported for use by the Clash core -// eslint-disable-next-line unused-imports/no-unused-vars -function main(config, _name) { - if (Array.isArray(config.proxies)) { - config.proxies.forEach((p, i) => { - if (p.type === 'hysteria' && typeof p.alpn === 'string') { - config.proxies[i].alpn = [p.alpn] - } - }) - } - return config -} diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 797b02cd7..df25c712f 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -22,12 +22,6 @@ pub enum ChainType { Groups(SeqMap), } -#[derive(Debug, Clone)] -pub enum ChainSupport { - ClashMeta, - ClashMetaAlpha, -} - // Helper trait to allow async conversion pub trait AsyncChainItemFrom { async fn from_async(item: &PrfItem) -> Option; @@ -53,70 +47,16 @@ impl AsyncChainItemFrom for Option { uid, data: ChainType::Merge(help::read_mapping(&path).await.ok()?), }), - "rules" => { + "rules" | "proxies" | "groups" => { let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Rules(seq_map), - }) - } - "proxies" => { - let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Proxies(seq_map), - }) - } - "groups" => { - let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Groups(seq_map), - }) + let data = match itype { + "rules" => ChainType::Rules(seq_map), + "proxies" => ChainType::Proxies(seq_map), + _ => ChainType::Groups(seq_map), + }; + Some(ChainItem { uid, data }) } _ => None, } } } -impl ChainItem { - /// 内建支持一些脚本 - pub fn builtin() -> Vec<(ChainSupport, Self)> { - // meta 的一些处理 - let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); - - // meta 1.13.2 alpn string 转 数组 - let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); - - // meta 的一些处理 - let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); - - // meta 1.13.2 alpn string 转 数组 - let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); - - vec![ - (ChainSupport::ClashMeta, hy_alpn), - (ChainSupport::ClashMeta, meta_guard), - (ChainSupport::ClashMetaAlpha, hy_alpn_alpha), - (ChainSupport::ClashMetaAlpha, meta_guard_alpha), - ] - } - - pub fn to_script, D: Into>(uid: U, data: D) -> Self { - Self { - uid: uid.into(), - data: ChainType::Script(data.into()), - } - } -} - -impl ChainSupport { - pub fn is_support(&self, core: Option<&String>) -> bool { - match core { - Some(core) => matches!( - (self, core.as_str()), - (Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha") - ), - None => true, - } - } -} diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs index 04e73a786..038980f55 100644 --- a/src-tauri/src/enhance/field.rs +++ b/src-tauri/src/enhance/field.rs @@ -27,7 +27,7 @@ fn lowercase_key(key: &str) -> Value { pub fn use_lowercase(config: &Mapping) -> Mapping { let mut lowercased = Mapping::new(); - for (key, value) in config.into_iter() { + for (key, value) in config.iter() { if let Some(key_str) = key.as_str() { lowercased.insert(lowercase_key(key_str), value.clone()); } @@ -35,17 +35,6 @@ pub fn use_lowercase(config: &Mapping) -> Mapping { lowercased } -pub fn use_lowercase_owned(config: Mapping) -> Mapping { - let mut lowercased = Mapping::new(); - - for (key, value) in config { - if let Some(key_str) = key.as_str() { - lowercased.insert(lowercase_key(key_str), value); - } - } - lowercased -} - pub fn use_sort(mut config: Mapping) -> Mapping { let mut sorted = Mapping::new(); HANDLE_FIELDS.into_iter().for_each(|key| { diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index ba0ba5708..6fab9c2be 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -8,7 +8,7 @@ mod tun; use self::{ chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, - field::{use_keys, use_lowercase_owned, use_sort}, + field::{use_keys, use_lowercase, use_sort}, merge::use_merge, script::use_script, seq::{SeqMap, use_seq}, @@ -34,7 +34,6 @@ type ResultLog = Vec<(String, String)>; #[derive(Debug)] struct ConfigValues { clash_config: Mapping, - clash_core: Option, enable_tun: bool, enable_builtin: bool, socks_enabled: bool, @@ -128,8 +127,7 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues { let dns_settings = verge_arc.dns_settings_for(profile_uid); let dns_override_confirmation = dns_settings.confirmation; - let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( - Some(verge_arc.get_valid_clash_core()), + let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( enable_tun_mode.unwrap_or(false), enable_builtin_enhanced.unwrap_or(true), verge_socks_enabled.unwrap_or(false), @@ -148,7 +146,6 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues { ConfigValues { clash_config, - clash_core, enable_tun, enable_builtin, socks_enabled, @@ -199,35 +196,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result { let name = current_item.name.clone().unwrap_or_default(); + let ProfileItems { + merge_item: default_merge, + script_item: default_script, + rules_item: default_rules, + proxies_item: default_proxies, + groups_item: default_groups, + global_merge: default_global_merge, + global_script: default_global_script, + .. + } = ProfileItems::default(); + let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!( - chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Merge(Mapping::new()), - },), - chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - },), - chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Rules(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Proxies(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Groups(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem { - uid: "Merge".into(), - data: ChainType::Merge(Mapping::new()), - },), - chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem { - uid: "Script".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - },), + chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,), + chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,), + chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,), + chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,), + chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,), + chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,), + chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,), ); Ok(ProfileItems { @@ -243,16 +230,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result { }) } -async fn process_global_items( +async fn process_override_items( mut config: Mapping, mut exists_keys: Vec, mut result_map: HashMap, - global_merge: ChainItem, - global_script: ChainItem, + merge_item: ChainItem, + script_item: ChainItem, profile_name: &String, authoritative: &AuthoritativeFields, ) -> (Mapping, Vec, HashMap) { - if let ChainType::Merge(merge) = global_merge.data { + if let ChainType::Merge(merge) = merge_item.data { exists_keys.extend(use_keys(&merge)); let before = authoritative.current(&config); config = use_merge(&merge, config); @@ -262,11 +249,11 @@ async fn process_global_items( .map(discarded_note) .collect(); if !notes.is_empty() { - result_map.entry(global_merge.uid).or_default().extend(notes); + result_map.entry(merge_item.uid).or_default().extend(notes); } } - if let ChainType::Script(script) = global_script.data { + if let ChainType::Script(script) = script_item.data { let before = authoritative.current(&config); let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; exists_keys.extend(changed_keys); @@ -277,7 +264,7 @@ async fn process_global_items( .into_iter() .map(discarded_note), ); - result_map.insert(global_script.uid, logs); + result_map.insert(script_item.uid, logs); } (config, exists_keys, result_map) @@ -544,46 +531,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping { config } -async fn process_profile_items( - mut config: Mapping, - mut exists_keys: Vec, - mut result_map: HashMap, - merge_item: ChainItem, - script_item: ChainItem, - profile_name: &String, - authoritative: &AuthoritativeFields, -) -> (Mapping, Vec, HashMap) { - if let ChainType::Merge(merge) = merge_item.data { - exists_keys.extend(use_keys(&merge)); - let before = authoritative.current(&config); - config = use_merge(&merge, config); - let notes: ResultLog = authoritative - .overridden(&before, &authoritative.current(&config)) - .into_iter() - .map(discarded_note) - .collect(); - if !notes.is_empty() { - result_map.entry(merge_item.uid).or_default().extend(notes); - } - } - - if let ChainType::Script(script) = script_item.data { - let before = authoritative.current(&config); - let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; - exists_keys.extend(changed_keys); - config = res_config; - logs.extend( - authoritative - .overridden(&before, &authoritative.current(&config)) - .into_iter() - .map(discarded_note), - ); - result_map.insert(script_item.uid, logs); - } - - (config, exists_keys, result_map) -} - /// Merges the app's clash config over the profile; switches are parameters to keep it pure. fn merge_default_config( mut config: Mapping, @@ -655,25 +602,12 @@ fn merge_default_config( config } -fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable_builtin: bool) -> Mapping { +fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping { if enable_builtin { - let items: Vec<_> = ChainItem::builtin() - .into_iter() - .filter(|(s, _)| s.is_support(clash_core.as_ref())) - .map(|(_, c)| c) - .collect(); - if !items.is_empty() { - // The JS path saw a lowercased view; lowercase once, only when a builtin runs. - config = use_lowercase_owned(config); - for item in items { - logging!(debug, Type::Core, "run builtin script {}", item.uid); - config = match item.uid.as_str() { - "verge_hy_alpn" => builtin_hy_alpn(config), - "verge_meta_guard" => builtin_meta_guard(config), - _ => config, - }; - } - } + // The JS path saw a lowercased view; lowercase once, only when the builtin runs. + config = use_lowercase(&config); + logging!(debug, Type::Core, "run builtin script verge_hy_alpn"); + config = builtin_hy_alpn(config); } config @@ -693,14 +627,6 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping { config } -fn builtin_meta_guard(mut config: Mapping) -> Mapping { - if config.get("mode").and_then(Value::as_str) == Some("script") { - config.insert(Value::from("mode"), Value::from("rule")); - } - - config -} - fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { // built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"]; @@ -879,7 +805,6 @@ pub async fn enhance( let cfg_vals = get_config_values(profile_uid).await; let ConfigValues { clash_config, - clash_core, enable_tun, enable_builtin, socks_enabled, @@ -929,13 +854,13 @@ pub async fn enhance( tproxy_enabled, ); - let config = apply_builtin_scripts(config, clash_core, enable_builtin); + let config = apply_builtin_scripts(config, enable_builtin); let config = use_tun(config, enable_tun); let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await; let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings); - let (config, exists_keys, result_map) = process_global_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, result_map, @@ -946,7 +871,7 @@ pub async fn enhance( ) .await; - let (config, exists_keys, result_map) = process_profile_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, result_map, @@ -1428,7 +1353,7 @@ mod authoritative_field_tests { mod tests { use super::{ AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address, - process_global_items, process_profile_items, use_keys, + process_override_items, use_keys, }; use std::collections::HashMap; @@ -1476,22 +1401,28 @@ mod tests { data: ChainType::Merge(merge.clone()), }; let profile_name = "test-profile".into(); - let (config, keys, logs) = process_global_items( + let (config, keys, logs) = process_override_items( config, vec![], HashMap::new(), merge_item("global-merge"), - ChainItem::to_script("global-script", script), + ChainItem { + uid: "global-script".into(), + data: ChainType::Script(script.into()), + }, &profile_name, &authoritative, ) .await; - let (config, _, logs) = process_profile_items( + let (config, _, logs) = process_override_items( config, keys, logs, merge_item("profile-merge"), - ChainItem::to_script("profile-script", script), + ChainItem { + uid: "profile-script".into(), + data: ChainType::Script(script.into()), + }, &profile_name, &authoritative, ) @@ -1536,16 +1467,19 @@ mod tests { nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}", )), }; - let global_script = ChainItem::to_script( - "Script", - r#"function main(config) { + let global_script = ChainItem { + uid: "Script".into(), + data: ChainType::Script( + r#"function main(config) { config["global-script-wins"] = "global-script"; config["profile-merge-wins"] = "global-script"; config["profile-script-wins"] = "global-script"; config.nested.winner = "global-script"; return config; - }"#, - ); + }"# + .into(), + ), + }; let profile_merge = ChainItem { uid: "profile-merge".into(), data: ChainType::Merge(mapping( @@ -1553,18 +1487,21 @@ mod tests { nested: {winner: profile-merge}}", )), }; - let profile_script = ChainItem::to_script( - "profile-script", - r#"function main(config) { + let profile_script = ChainItem { + uid: "profile-script".into(), + data: ChainType::Script( + r#"function main(config) { config["profile-script-wins"] = "profile-script"; config.nested.winner = "profile-script"; return config; - }"#, - ); + }"# + .into(), + ), + }; let profile_name = "test-profile".into(); let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new()); - let (config, exists_keys, result_map) = process_global_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, HashMap::new(), @@ -1574,7 +1511,7 @@ mod tests { &authoritative, ) .await; - let (config, exists_keys, _) = process_profile_items( + let (config, exists_keys, _) = process_override_items( config, exists_keys, result_map, diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs index 2aaee1121..b69ee7494 100644 --- a/src-tauri/src/enhance/script.rs +++ b/src-tauri/src/enhance/script.rs @@ -1,6 +1,6 @@ use crate::{process::AsyncHandler, utils::tmpl}; -use super::field::{use_lowercase, use_lowercase_owned}; +use super::field::use_lowercase; use anyhow::{Error, Result}; use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute}; use clash_verge_logging::{Type, logging}; @@ -22,7 +22,7 @@ type EvalOutcome = (Result, Vec<(String, String)>) pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult { // The template is identity-plus-lowercasing; skip the Boa round-trip. if script == tmpl::ITEM_SCRIPT { - return (use_lowercase_owned(config), vec![], vec![]); + return (use_lowercase(&config), vec![], vec![]); } let json = match serde_json::to_string(&use_lowercase(&config)) { @@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome { } match parse_json_safely(&result) { - Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()), + Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()), Err(err) => { logging!( error, diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index f5d1af7ab..e56810135 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { .and_then(|v| v.as_str()) .unwrap_or("fake-ip"); - if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) { + if current_mode == "fake-ip" { revise!(dns_val, "enable", true); revise!(dns_val, "ipv6", ipv6_val); diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 89f9b6d6e..df2e42395 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -43,14 +43,14 @@ async fn finalize_restored_verge_config( let restored_clash = IClashTemp::new().await; let clash_draft = Config::clash().await; clash_draft.edit_draft(|d| { - *d = restored_clash.clone(); + *d = restored_clash; }); clash_draft.apply(); let restored_profiles = IProfiles::new().await; let profiles_draft = Config::profiles().await; profiles_draft.edit_draft(|d| { - *d = restored_profiles.clone(); + *d = restored_profiles; }); profiles_draft.apply(); @@ -62,20 +62,20 @@ async fn finalize_restored_verge_config( *d = restored.clone(); }); verge_draft.apply(); + } - // Turn it off here; a failing core side effect below would otherwise skip this step. - if !restored.enable_system_proxy.unwrap_or_default() { - let result = async { - let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; - proxy_control::apply().await?; - proxy_control::refresh_guard().await - } - .await; - if let Err(err) = result { - logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); - if SystemProxyStateUnknown::is_in(&err) { - return Err(err); - } + // Turn it off here; a failing core side effect below would otherwise skip this step. + if !restored.enable_system_proxy.unwrap_or_default() { + let result = async { + let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; + proxy_control::apply().await?; + proxy_control::refresh_guard().await + } + .await; + if let Err(err) = result { + logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); + if SystemProxyStateUnknown::is_in(&err) { + return Err(err); } } } @@ -133,14 +133,27 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> { }) } +/// Backup creation strips the WebDAV credentials, so the live ones are kept. +async fn restore_backup_archive(path: PathBuf) -> Result<()> { + let (webdav_url, webdav_username, webdav_password) = { + let verge = Config::verge().await; + let verge = verge.latest_arc(); + ( + verge.webdav_url.clone(), + verge.webdav_username.clone(), + verge.webdav_password.clone(), + ) + }; + + let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&path)).await??; + let mut zip = zip::ZipArchive::new(file)?; + let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; + zip.extract(app_home_dir()?)?; + finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await +} + #[tracing::instrument(skip_all, level = "info", fields(filename = %filename))] pub async fn restore_webdav_backup(filename: String) -> Result<()> { - let verge = Config::verge().await; - let verge_data = verge.latest_arc(); - let webdav_url = verge_data.webdav_url.clone(); - let webdav_username = verge_data.webdav_username.clone(); - let webdav_password = verge_data.webdav_password.clone(); - let backup_storage_path = app_home_dir() .map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))? .join(filename.as_str()); @@ -157,12 +170,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { err })?; - let value = backup_storage_path.clone(); - let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; - let mut zip = zip::ZipArchive::new(file)?; - let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; - zip.extract(app_home_dir()?)?; - let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; + let res = restore_backup_archive(backup_storage_path.clone()).await; let _ = backup_storage_path.remove_if_exists().await; res } @@ -331,22 +339,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { return Err(anyhow!("Backup file not found: {}", filename)); } - let (webdav_url, webdav_username, webdav_password) = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - ( - verge.webdav_url.clone(), - verge.webdav_username.clone(), - verge.webdav_password.clone(), - ) - }; - - let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; - let mut zip = zip::ZipArchive::new(file)?; - let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; - zip.extract(app_home_dir()?)?; - finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; - Ok(()) + restore_backup_archive(target_path).await } #[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))] diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 0f90dffc4..9b1d84b3c 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce}; use crate::{ config::{Config, MixedPort}, core::{CoreManager, handle, tray}, - feat::clean_async, + feat::run_exit_cleanup_flow, process::AsyncHandler, utils, }; @@ -14,15 +14,8 @@ use smartstring::alias::String; use std::sync::{Arc, LazyLock}; #[allow(clippy::expect_used)] -static TLS_CONFIG: LazyLock> = LazyLock::new(|| { - let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) - .with_safe_default_protocol_versions() - .expect("Failed to set TLS versions") - .with_root_certificates(root_store) - .with_no_client_auth(); - Arc::new(config) -}); +static TLS_CONFIG: LazyLock> = + 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 { @@ -38,38 +31,9 @@ pub async fn restart_clash_core() { pub async fn restart_app() { logging!(debug, Type::System, "启动重启应用流程"); - handle::Handle::global().set_is_exiting(); - - Config::apply_all_and_save_file().await; - - let cleanup_result = clean_async().await; - - logging!( - info, - Type::System, - "资源清理完成,退出代码: {}", - if cleanup_result.all_success { 0 } else { 1 } - ); + let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await; if !cleanup_result.core_stopped { - handle::Handle::global().clear_is_exiting(); - // A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore. - // A core that kept running keeps its DNS, whatever an unapplied config draft says. - #[cfg(target_os = "macos")] - { - let manager = CoreManager::global(); - let _lifecycle = manager.lifecycle_lock.lock().await; - if matches!( - *manager.get_running_mode(), - crate::core::manager::RunningMode::NotRunning - ) { - crate::utils::resolve::dns::sync_public_dns().await; - } - } - handle::Handle::notice( - NoticeStatus::AppRestartCoreStopFailed, - cleanup_result.stop_error.unwrap_or_default(), - ); return; } diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index 6e633db50..dc171a5a7 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { record_switched_node(group_name, proxy_name).await; announce(Refresh::Proxies); let _ = tray::Tray::global().update_menu().await; - return; } Err(err) => { logging!(error, Type::Tray, "切换代理失败: {err:?}"); } } - - match handle::Handle::mihomo() - .select_node_for_group(group_name, proxy_name) - .await - { - Ok(_) => { - logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name); - record_switched_node(group_name, proxy_name).await; - let _ = tray::Tray::global().update_menu().await; - } - Err(err) => { - logging!(error, Type::Tray, "代理切换最终失败: {err:?}"); - } - } } async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result)>> { diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index e618701ed..ebb6cb397 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() { logging!(info, Type::Window, "Window toggle result: {result:?}"); } -pub async fn quit() { - logging!(debug, Type::System, "启动退出流程"); - // 设置退出标志 +pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult { handle::Handle::global().set_is_exiting(); Config::apply_all_and_save_file().await; @@ -162,11 +160,18 @@ pub async fn quit() { } } handle::Handle::notice( - NoticeStatus::AppQuitCoreStopFailed, - cleanup_result.stop_error.unwrap_or_default(), + stop_failed_notice, + cleanup_result.stop_error.clone().unwrap_or_default(), ); } + cleanup_result +} + +pub async fn quit() { + logging!(debug, Type::System, "启动退出流程"); + let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await; + utils::server::shutdown_embedded_server(); let app_handle = handle::Handle::app_handle(); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index be7f016a1..927fc797f 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> { Value::String("*.arpa".into()), Value::String("time.*.com".into()), Value::String("ntp.*.com".into()), - Value::String("time.*.com".into()), Value::String("+.market.xiaomi.com".into()), Value::String("localhost.ptlogin2.qq.com".into()), Value::String("*.msftncsi.com".into()), @@ -509,11 +508,9 @@ pub async fn init_resources() -> Result<()> { let app_dir = dirs::app_home_dir()?; let res_dir = dirs::app_resources_dir()?; - if !app_dir.exists() { - std::mem::drop(fs::create_dir_all(&app_dir).await); - } + // app_home is created by `ensure_directories` in `init_config`, which always runs first. if !res_dir.exists() { - std::mem::drop(fs::create_dir_all(&res_dir).await); + fs::create_dir_all(&res_dir).await?; } let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"]; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index d325fecbd..89d2ddc6f 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -48,6 +48,17 @@ enum TlsRootMode { StaticWebpkiRoots, } +/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer. +pub(crate) fn build_webpki_rooted_tls_config() -> Result { + let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) + .with_safe_default_protocol_versions()? + .with_root_certificates(root_store) + .with_no_client_auth(); + + Ok(config) +} + pub struct NetworkManager; impl NetworkManager { @@ -102,13 +113,7 @@ impl NetworkManager { } fn build_static_webpki_tls_config() -> Result { - let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let mut config = - rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) - .with_safe_default_protocol_versions()? - .with_root_certificates(root_store) - .with_no_client_auth(); - + let mut config = build_webpki_rooted_tls_config()?; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; Ok(config) @@ -155,6 +160,23 @@ impl NetworkManager { detail.contains("protocolversion") || detail.contains("protocol version") } + /// `allow_fallback` stays false when the caller already disabled certificate verification. + async fn request_with_static_webpki_fallback(allow_fallback: bool, mut request: F) -> Result + where + F: FnMut(TlsRootMode) -> Fut, + Fut: std::future::Future>, + { + match request(TlsRootMode::PlatformVerifier).await { + Ok(value) => Ok(value), + Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => { + request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| { + fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") + }) + } + Err(err) => Err(err), + } + } + async fn get_with_tls_mode( &self, url: &str, @@ -264,34 +286,21 @@ impl NetworkManager { user_agent: Option, accept_invalid_certs: bool, ) -> Result { - let platform_result = self - .get_with_tls_mode( - url, - proxy_type, - timeout_secs, - user_agent.clone(), - accept_invalid_certs, - TlsRootMode::PlatformVerifier, - ) - .await; - - match platform_result { - Ok(response) => Ok(response), - Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self - .get_with_tls_mode( + Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| { + let user_agent = user_agent.clone(); + async move { + self.get_with_tls_mode( url, proxy_type, timeout_secs, user_agent, accept_invalid_certs, - TlsRootMode::StaticWebpkiRoots, + tls_root_mode, ) .await - .map_err(|fallback_err| { - fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") - }), - Err(err) => Err(err), - } + } + }) + .await } async fn get_bytes_with_tls_mode( @@ -340,19 +349,10 @@ impl NetworkManager { timeout_secs: Option, max_bytes: usize, ) -> Result> { - let platform_result = self - .get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier) - .await; - - match platform_result { - Ok(body) => Ok(body), - Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self - .get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots) + Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move { + self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode) .await - .map_err(|fallback_err| { - fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") - }), - Err(err) => Err(err), - } + }) + .await } } diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 7ba959ae3..fa408f2af 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -1,8 +1,6 @@ use crate::core::notify::NoticeStatus; use std::sync::atomic::{AtomicBool, Ordering}; -use anyhow::Result; - use crate::{ config::Config, core::{ @@ -97,18 +95,6 @@ async fn resolve_setup() { resolve_done(); } -pub async fn resolve_reset_async() -> Result<(), anyhow::Error> { - CoreManager::global().stop_core().await?; - - #[cfg(target_os = "macos")] - { - use dns::restore_public_dns; - restore_public_dns().await?; - } - - Ok(()) -} - fn init_scheme() { logging_error!(Type::Setup, init::init_scheme()); } diff --git a/src/services/bindings.ts b/src/services/bindings.ts index 4a1623bdc..073da075a 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -365,7 +365,7 @@ export type IVerge_Deserialize = { start_page?: string | null startup_script?: string | null traffic_graph?: boolean | null - /** show memory info (only for Clash Meta) */ + /** show memory info (only for Mihomo) */ enable_memory_usage?: boolean | null enable_group_icon?: boolean | null pause_render_traffic_stats_on_blur?: boolean | null @@ -455,7 +455,7 @@ export type IVerge_Serialize = { start_page: string | null startup_script: string | null traffic_graph: boolean | null - /** show memory info (only for Clash Meta) */ + /** show memory info (only for Mihomo) */ enable_memory_usage: boolean | null enable_group_icon?: boolean | null pause_render_traffic_stats_on_blur: boolean | null