Compare commits

..
Author SHA1 Message Date
Tunglies acef575935 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.
2026-10-28 09:15:46 +08:00
Tunglies f489c7afa6 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.
2026-10-28 09:15:46 +08:00
Tunglies 238d4de328 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.
2026-10-28 09:15:46 +08:00
Tunglies ab0e1b957a 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.
2026-10-28 09:15:46 +08:00
Tunglies 1313692b3d 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.
2026-10-28 09:15:46 +08:00
Tunglies a5d0b8dacb 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.
2026-10-28 09:15:46 +08:00
Tunglies 1a5094caf0 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.
2026-10-28 09:15:46 +08:00
Tunglies 93f70f4c36 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.
2026-10-28 09:15:46 +08:00
Tunglies e5b691b47a 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.
2026-10-28 09:15:45 +08:00
Tunglies 7e9fa61f52 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<RwLock> copy with a stored receiver.
2026-10-28 09:15:45 +08:00
Tunglies acde039820 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.
2026-10-28 09:15:45 +08:00
Tunglies 0474c9cd0c refactor(sysopt): drop redundant lock and busy-wait
The proxy target pair sat behind an Arc<RwLock> 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.
2026-10-28 09:15:45 +08:00
Tunglies c82719af45 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.
2026-10-28 09:15:45 +08:00
Tunglies 808f27abb4 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.
2026-10-28 09:14:31 +08:00
Tunglies ad0035edb9 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.
2026-10-28 09:14:31 +08:00
Tunglies 9063181468 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
2026-10-28 09:14:31 +08:00
Tunglies bfb50ac0d7 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.
2026-10-28 09:14:31 +08:00
Tunglies b79c5615a5 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.
2026-10-28 09:14:31 +08:00
Tunglies 8702779375 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.
2026-10-28 09:14:31 +08:00
Tunglies a6d680236f 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").
2026-10-28 09:14:31 +08:00
Tunglies 4c778a93db 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.
2026-10-28 09:14:31 +08:00
Tunglies dbaee8a5e4 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.
2026-10-28 09:14:30 +08:00
Tunglies 3bb708233e 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
2026-10-28 09:14:30 +08:00
30 changed files with 521 additions and 852 deletions
+6 -7
View File
@@ -266,13 +266,13 @@ async function getLatestReleaseVersion() {
} }
if (!META_MAP[`${platform}-${arch}`]) { 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}`]) { 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 name = META_ALPHA_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32' const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz' const urlExt = isWin ? 'zip' : 'gz'
@@ -285,7 +285,7 @@ function clashMetaAlpha() {
} }
} }
function clashMeta() { function mihomo() {
const name = META_MAP[`${platform}-${arch}`] const name = META_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32' const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz' const urlExt = isWin ? 'zip' : 'gz'
@@ -733,13 +733,12 @@ const tasks = [
{ {
name: 'verge-mihomo-alpha', name: 'verge-mihomo-alpha',
func: () => func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())), getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())),
retry: 5, retry: 5,
}, },
{ {
name: 'verge-mihomo', name: 'verge-mihomo',
func: () => func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5, retry: 5,
}, },
// After both sidecar tasks: it hashes what they downloaded. // After both sidecar tasks: it hashes what they downloaded.
+13 -56
View File
@@ -184,84 +184,41 @@ impl IClashTemp {
_ => None, _ => None,
}) })
} }
#[cfg(not(target_os = "windows"))] fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
fn guard_redir_port(config: &Mapping) -> u16 {
let mut port = config let mut port = config
.get("redir-port") .get(key)
.and_then(|value| match value { .and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(), Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None, _ => None,
}) })
.unwrap_or(7895); .unwrap_or(default);
if port == 0 { if port == 0 {
port = 7895; port = default;
} }
port 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")] #[cfg(target_os = "linux")]
fn guard_tproxy_port(config: &Mapping) -> u16 { fn guard_tproxy_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
.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
} }
fn guard_mixed_port(config: &Mapping) -> u16 { fn guard_mixed_port(config: &Mapping) -> u16 {
let raw_value = config.get("mixed-port"); Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
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
} }
fn guard_socks_port(config: &Mapping) -> u16 { fn guard_socks_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
.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
} }
fn guard_port(config: &Mapping) -> u16 { fn guard_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
.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
} }
pub(super) fn guard_server_ctrl(config: &Mapping) -> String { pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
+9
View File
@@ -14,6 +14,15 @@ mod verge;
pub(crate) use self::config::Config; pub(crate) use self::config::Config;
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; 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) { 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;"; return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
} }
+71 -108
View File
@@ -1,3 +1,5 @@
use super::patch_opt_field;
use crate::{ use crate::{
config::profiles, config::profiles,
utils::{ utils::{
@@ -131,19 +133,18 @@ impl PrfOption {
match (one, other) { match (one, other) {
(Some(a_ref), Some(b_ref)) => { (Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone(); let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent); patch_opt_field!(result, b_ref, user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy); patch_opt_field!(result, b_ref, with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy); patch_opt_field!(result, b_ref, self_proxy);
result.danger_accept_invalid_certs = patch_opt_field!(result, b_ref, danger_accept_invalid_certs);
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); patch_opt_field!(result, b_ref, allow_auto_update);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); patch_opt_field!(result, b_ref, update_interval);
result.update_interval = b_ref.update_interval.or(result.update_interval); patch_opt_field!(result, b_ref, merge);
result.merge = b_ref.merge.clone().or(result.merge); patch_opt_field!(result, b_ref, script);
result.script = b_ref.script.clone().or(result.script); patch_opt_field!(result, b_ref, rules);
result.rules = b_ref.rules.clone().or(result.rules); patch_opt_field!(result, b_ref, proxies);
result.proxies = b_ref.proxies.clone().or(result.proxies); patch_opt_field!(result, b_ref, groups);
result.groups = b_ref.groups.clone().or(result.groups); patch_opt_field!(result, b_ref, timeout_seconds);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result) Some(result)
} }
(Some(a_ref), None) => Some(a_ref.clone()), (Some(a_ref), None) => Some(a_ref.clone()),
@@ -181,6 +182,38 @@ impl PrfItem {
} }
} }
async fn append_companion_item(item: &mut Self) -> Result<String> {
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<String>,
script: &mut Option<String>,
rules: &mut Option<String>,
proxies: &mut Option<String>,
groups: &mut Option<String>,
) -> 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( async fn from_local(
name: String, name: String,
desc: String, desc: String,
@@ -197,31 +230,7 @@ impl PrfItem {
let mut proxies = opt_ref.and_then(|o| o.proxies.clone()); let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone()); let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() { Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
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();
}
Ok(Self { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("local".into()), itype: Some("local".into()),
@@ -375,31 +384,7 @@ impl PrfItem {
bail!("profile does not contain `proxies` or `proxy-providers`"); bail!("profile does not contain `proxies` or `proxy-providers`");
} }
if merge.is_none() { Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
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();
}
Ok(Self { Ok(Self {
uid: Some(uid), uid: Some(uid),
@@ -461,46 +446,30 @@ impl PrfItem {
} }
} }
fn from_rules() -> Self { fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self {
let uid = help::get_uid("r").into(); let uid = help::get_uid(prefix).into();
let file = format!("{uid}.yaml").into(); // yaml ext let file = format!("{uid}.yaml").into(); // yaml ext
Self { Self {
uid: Some(uid), uid: Some(uid),
itype: Some("rules".into()), itype: Some(itype.into()),
file: Some(file), file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_RULES.into()), file_data: Some(template.into()),
..Default::default() ..Default::default()
} }
} }
fn from_rules() -> Self {
Self::new_seq_item("r", "rules", tmpl::ITEM_RULES)
}
fn from_proxies() -> Self { fn from_proxies() -> Self {
let uid = help::get_uid("p").into(); Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES)
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()
}
} }
fn from_groups() -> Self { fn from_groups() -> Self {
let uid = help::get_uid("g").into(); Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS)
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()
}
} }
pub(crate) async fn read_file(&self) -> Result<String> { pub(crate) async fn read_file(&self) -> Result<String> {
@@ -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 { impl PrfItem {
pub(crate) fn current_merge(&self) -> Option<&String> { current_field!(current_merge, merge);
self.option.as_ref().and_then(|o| o.merge.as_ref()) current_field!(current_script, script);
} current_field!(current_rules, rules);
current_field!(current_proxies, proxies);
pub(crate) fn current_script(&self) -> Option<&String> { current_field!(current_groups, groups);
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())
}
} }
fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool { fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool {
+40 -62
View File
@@ -1,5 +1,5 @@
use super::{ use super::{
PrfOption, PrfOption, patch_opt_field,
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url}, prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
}; };
use crate::core::notify::{Refresh, announce}; 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 { impl IProfiles {
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> { fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?; let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
@@ -244,15 +236,15 @@ impl IProfiles {
for each in items.iter_mut() { for each in items.iter_mut() {
if each.uid.as_deref() == Some(uid) { if each.uid.as_deref() == Some(uid) {
patch!(each, item, itype); patch_opt_field!(each, item, itype);
patch!(each, item, name); patch_opt_field!(each, item, name);
patch!(each, item, desc); patch_opt_field!(each, item, desc);
patch!(each, item, file); patch_opt_field!(each, item, file);
patch!(each, item, url); patch_opt_field!(each, item, url);
patch!(each, item, selected); patch_opt_field!(each, item, selected);
patch!(each, item, extra); patch_opt_field!(each, item, extra);
patch!(each, item, updated); patch_opt_field!(each, item, updated);
patch!(each, item, option); patch_opt_field!(each, item, option);
self.items = Some(items); self.items = Some(items);
return self.save_file().await; 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 profiles_append_item_safe(item).await
} }
/// Applies `edit` to the committed profiles; commits only when the edit succeeds.
async fn with_profiles_edit<F, Fut>(edit: F) -> Result<()>
where
F: FnOnce(IProfiles) -> Fut + Send,
Fut: std::future::Future<Output = Result<IProfiles>> + 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<()> { pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
profiles_append_item_to_safe(&profiles, item).await profiles_append_item_to_safe(&profiles, item).await
@@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
} }
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> { pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.patch_item(index, item).await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> { pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.reorder(active_id, over_id).await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_save_file_safe() -> Result<()> { pub(crate) async fn profiles_save_file_safe() -> Result<()> {
Config::profiles() with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await
.await
.with_data_modify(|profiles| async move {
profiles.save_file().await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> { pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.update_item(index, item).await?;
Ok((profiles, ()))
})
.await
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -713,19 +696,21 @@ where
Ok(()) 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(&current))
}
/// Records a backend-made selection so the next core start restores it. /// 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<()> { pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
let group_name = String::from(group_name); let group_name = String::from(group_name);
let node = String::from(node); let node = String::from(node);
persist_selected_update(true, move |mut profiles| async move { persist_selected_update(true, move |mut profiles| async move {
let Some(current) = profiles.current.clone() else { let Some(item) = current_item_mut(&mut profiles) 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(&current)))
else {
return Ok((profiles, false)); return Ok((profiles, false));
}; };
@@ -760,14 +745,7 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> { pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
persist_selected_update(true, move |mut profiles| async move { persist_selected_update(true, move |mut profiles| async move {
let Some(current) = profiles.current.clone() else { let Some(item) = current_item_mut(&mut profiles) 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(&current)))
else {
return Ok((profiles, false)); return Ok((profiles, false));
}; };
+76 -84
View File
@@ -1,6 +1,6 @@
use crate::config::Config; use crate::config::Config;
use crate::{ use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted},
constants::network, constants::network,
utils::{dirs, help}, utils::{dirs, help},
}; };
@@ -39,7 +39,7 @@ pub struct IVerge {
pub traffic_graph: Option<bool>, pub traffic_graph: Option<bool>,
/// show memory info (only for Clash Meta) /// show memory info (only for Mihomo)
pub enable_memory_usage: Option<bool>, pub enable_memory_usage: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -432,98 +432,90 @@ impl IVerge {
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &Self) { pub fn patch_config(&mut self, patch: &Self) {
macro_rules! patch { patch_opt_field!(self, patch, app_log_level);
($key: tt) => { patch_opt_field!(self, patch, app_log_max_size);
if patch.$key.is_some() { patch_opt_field!(self, patch, app_log_max_count);
self.$key = patch.$key.clone();
}
};
}
patch!(app_log_level); patch_opt_field!(self, patch, language);
patch!(app_log_max_size); patch_opt_field!(self, patch, theme_mode);
patch!(app_log_max_count); patch_opt_field!(self, patch, tray_event);
patch_opt_field!(self, patch, env_type);
patch!(language); patch_opt_field!(self, patch, start_page);
patch!(theme_mode); patch_opt_field!(self, patch, startup_script);
patch!(tray_event); patch_opt_field!(self, patch, traffic_graph);
patch!(env_type); patch_opt_field!(self, patch, enable_memory_usage);
patch!(start_page); patch_opt_field!(self, patch, enable_group_icon);
patch!(startup_script); patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_group_icon);
patch!(pause_render_traffic_stats_on_blur);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
patch!(tray_icon); patch_opt_field!(self, patch, tray_icon);
patch!(menu_icon); patch_opt_field!(self, patch, menu_icon);
patch!(menu_order); patch_opt_field!(self, patch, menu_order);
patch!(proxy_group_tools_position); patch_opt_field!(self, patch, proxy_group_tools_position);
patch!(notice_position); patch_opt_field!(self, patch, notice_position);
patch!(collapse_navbar); patch_opt_field!(self, patch, collapse_navbar);
patch!(common_tray_icon); patch_opt_field!(self, patch, common_tray_icon);
patch!(sysproxy_tray_icon); patch_opt_field!(self, patch, sysproxy_tray_icon);
patch!(tun_tray_icon); patch_opt_field!(self, patch, tun_tray_icon);
patch!(enable_tun_mode); patch_opt_field!(self, patch, enable_tun_mode);
patch!(enable_auto_launch); patch_opt_field!(self, patch, enable_auto_launch);
patch!(enable_silent_start); patch_opt_field!(self, patch, enable_silent_start);
patch!(enable_hover_jump_navigator); patch_opt_field!(self, patch, enable_hover_jump_navigator);
patch!(hover_jump_navigator_delay); patch_opt_field!(self, patch, hover_jump_navigator_delay);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
patch!(verge_redir_port); patch_opt_field!(self, patch, verge_redir_port);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
patch!(verge_redir_enabled); patch_opt_field!(self, patch, verge_redir_enabled);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
patch!(verge_tproxy_port); patch_opt_field!(self, patch, verge_tproxy_port);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
patch!(verge_tproxy_enabled); patch_opt_field!(self, patch, verge_tproxy_enabled);
patch!(verge_mixed_port); patch_opt_field!(self, patch, verge_mixed_port);
patch!(verge_socks_port); patch_opt_field!(self, patch, verge_socks_port);
patch!(verge_socks_enabled); patch_opt_field!(self, patch, verge_socks_enabled);
patch!(verge_port); patch_opt_field!(self, patch, verge_port);
patch!(verge_http_enabled); patch_opt_field!(self, patch, verge_http_enabled);
patch!(enable_system_proxy); patch_opt_field!(self, patch, enable_system_proxy);
patch!(enable_proxy_guard); patch_opt_field!(self, patch, enable_proxy_guard);
patch!(enable_bypass_check); patch_opt_field!(self, patch, enable_bypass_check);
patch!(use_default_bypass); patch_opt_field!(self, patch, use_default_bypass);
patch!(system_proxy_bypass); patch_opt_field!(self, patch, system_proxy_bypass);
patch!(proxy_guard_duration); patch_opt_field!(self, patch, proxy_guard_duration);
patch!(proxy_auto_config); patch_opt_field!(self, patch, proxy_auto_config);
patch!(pac_file_content); patch_opt_field!(self, patch, pac_file_content);
patch!(proxy_host); patch_opt_field!(self, patch, proxy_host);
patch!(theme_setting); patch_opt_field!(self, patch, theme_setting);
patch!(web_ui_list); patch_opt_field!(self, patch, web_ui_list);
patch!(clash_core); patch_opt_field!(self, patch, clash_core);
patch!(hotkeys); patch_opt_field!(self, patch, hotkeys);
patch!(enable_global_hotkey); patch_opt_field!(self, patch, enable_global_hotkey);
patch!(auto_close_connection); patch_opt_field!(self, patch, auto_close_connection);
patch!(auto_check_update); patch_opt_field!(self, patch, auto_check_update);
patch!(default_latency_test); patch_opt_field!(self, patch, default_latency_test);
patch!(default_latency_timeout); patch_opt_field!(self, patch, default_latency_timeout);
patch!(enable_auto_delay_detection); patch_opt_field!(self, patch, enable_auto_delay_detection);
patch!(auto_delay_detection_interval_minutes); patch_opt_field!(self, patch, auto_delay_detection_interval_minutes);
patch!(enable_builtin_enhanced); patch_opt_field!(self, patch, enable_builtin_enhanced);
patch!(proxy_layout_column); patch_opt_field!(self, patch, proxy_layout_column);
patch!(test_list); patch_opt_field!(self, patch, test_list);
patch!(auto_log_clean); patch_opt_field!(self, patch, auto_log_clean);
patch!(enable_auto_backup_schedule); patch_opt_field!(self, patch, enable_auto_backup_schedule);
patch!(auto_backup_interval_hours); patch_opt_field!(self, patch, auto_backup_interval_hours);
patch!(auto_backup_on_change); patch_opt_field!(self, patch, auto_backup_on_change);
patch!(webdav_url); patch_opt_field!(self, patch, webdav_url);
patch!(webdav_username); patch_opt_field!(self, patch, webdav_username);
patch!(webdav_password); patch_opt_field!(self, patch, webdav_password);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
patch!(enable_tray_speed); patch_opt_field!(self, patch, enable_tray_speed);
patch!(tray_proxy_groups_display_mode); patch_opt_field!(self, patch, tray_proxy_groups_display_mode);
patch!(tray_inline_outbound_modes); patch_opt_field!(self, patch, tray_inline_outbound_modes);
patch!(enable_auto_light_weight_mode); patch_opt_field!(self, patch, enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes); patch_opt_field!(self, patch, auto_light_weight_minutes);
patch!(enable_dns_settings); patch_opt_field!(self, patch, enable_dns_settings);
patch!(home_cards); patch_opt_field!(self, patch, home_cards);
patch!(enable_external_controller); patch_opt_field!(self, patch, enable_external_controller);
} }
pub fn get_log_level(&self) -> LevelFilter { pub fn get_log_level(&self) -> LevelFilter {
+7
View File
@@ -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,<local>";
}
pub mod timing { pub mod timing {
use super::Duration; use super::Duration;
+7 -13
View File
@@ -7,11 +7,10 @@ use crate::{
use anyhow::Result; use anyhow::Result;
use chrono::Local; use chrono::Local;
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use std::{ use std::{
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, OnceLock, OnceLock,
atomic::{AtomicBool, AtomicI64, Ordering}, atomic::{AtomicBool, AtomicI64, Ordering},
}, },
time::{Duration, UNIX_EPOCH}, time::{Duration, UNIX_EPOCH},
@@ -82,8 +81,9 @@ impl Default for AutoBackupSettings {
} }
pub struct AutoBackupManager { pub struct AutoBackupManager {
settings: Arc<RwLock<AutoBackupSettings>>,
settings_tx: watch::Sender<AutoBackupSettings>, settings_tx: watch::Sender<AutoBackupSettings>,
// The watch channel is the single settings store; no duplicate copies.
settings_rx: watch::Receiver<AutoBackupSettings>,
runner_started: AtomicBool, runner_started: AtomicBool,
exec_lock: Mutex<()>, exec_lock: Mutex<()>,
last_backup: AtomicI64, last_backup: AtomicI64,
@@ -93,10 +93,10 @@ impl AutoBackupManager {
pub fn global() -> &'static Self { pub fn global() -> &'static Self {
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new(); static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
INSTANCE.get_or_init(|| { INSTANCE.get_or_init(|| {
let (tx, _rx) = watch::channel(AutoBackupSettings::default()); let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default());
Self { Self {
settings: Arc::new(RwLock::new(AutoBackupSettings::default())), settings_tx,
settings_tx: tx, settings_rx,
runner_started: AtomicBool::new(false), runner_started: AtomicBool::new(false),
exec_lock: Mutex::new(()), exec_lock: Mutex::new(()),
last_backup: AtomicI64::new(0), last_backup: AtomicI64::new(0),
@@ -106,9 +106,6 @@ impl AutoBackupManager {
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
let settings = Self::load_settings().await; let settings = Self::load_settings().await;
{
*self.settings.write() = settings;
}
let _ = self.settings_tx.send(settings); let _ = self.settings_tx.send(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
@@ -116,9 +113,6 @@ impl AutoBackupManager {
pub async fn refresh_settings(&self) -> Result<()> { pub async fn refresh_settings(&self) -> Result<()> {
let settings = Self::load_settings().await; let settings = Self::load_settings().await;
{
*self.settings.write() = settings;
}
let _ = self.settings_tx.send(settings); let _ = self.settings_tx.send(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
@@ -193,7 +187,7 @@ impl AutoBackupManager {
} }
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { 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 { if trigger.is_schedule() && !snapshot.schedule_enabled {
return Ok(()); return Ok(());
-9
View File
@@ -466,12 +466,3 @@ impl Hotkey {
(del_list, add_list) (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);
}
}
}
+53 -42
View File
@@ -40,8 +40,47 @@ impl LightweightState {
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); struct WindowListener {
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); 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<Option<oneshot::Sender<()>>> = Mutex::new(None); static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
@@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() {
} }
fn setup_window_close_listener() { fn setup_window_close_listener() {
if let Some(window) = WindowManager::get_main_window() { WINDOW_CLOSE_LISTENER.setup(|| {
let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); std::mem::drop(AsyncHandler::spawn(|| async {
if previous_handler_id != 0 { setup_light_weight_timer().await;
window.unlisten(previous_handler_id); }));
logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听"); logging!(info, 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);
}
} }
fn cancel_window_close_listener() { fn cancel_window_close_listener() {
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); WINDOW_CLOSE_LISTENER.cancel();
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
}
} }
fn setup_webview_focus_listener() { fn setup_webview_focus_listener() {
if let Some(window) = WindowManager::get_main_window() { WEBVIEW_FOCUS_LISTENER.setup(|| {
let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); cancel_light_weight_timer();
if previous_handler_id != 0 { logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
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);
}
} }
fn cancel_webview_focus_listener() { fn cancel_webview_focus_listener() {
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); WEBVIEW_FOCUS_LISTENER.cancel();
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
}
} }
async fn setup_light_weight_timer() { async fn setup_light_weight_timer() {
+2 -10
View File
@@ -6,7 +6,7 @@ use crate::{
notification::{self, FailedOperation}, notification::{self, FailedOperation},
runstate::RUN_STATE, runstate::RUN_STATE,
service, service,
sysopt::Sysopt, sysopt::{Sysopt, format_bypass},
}, },
process::AsyncHandler, process::AsyncHandler,
utils::{ utils::{
@@ -204,8 +204,6 @@ const fn refusal_classification(service_usable: Option<bool>) -> SysproxyFailure
} }
const LOOPBACK_HOST: &str = "127.0.0.1"; 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,<local>";
const MAX_SERVICE_BYPASS_LEN: usize = 8192; const MAX_SERVICE_BYPASS_LEN: usize = 8192;
static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new(); static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new();
@@ -327,13 +325,7 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL"); ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
let mut bypass = if custom.is_empty() { let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into();
MACOS_DEFAULT_BYPASS.to_owned()
} else if verge.use_default_bypass.unwrap_or(true) {
format!("{MACOS_DEFAULT_BYPASS},{custom}")
} else {
custom.to_owned()
};
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN); truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
Ok(bypass) Ok(bypass)
} }
+43 -40
View File
@@ -593,50 +593,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn uninstall_service() -> Result<()> { pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result<std::process::ExitStatus> {
logging!(info, Type::Service, "uninstall service");
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt as _; 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 token = Token::with_current_process()?;
let level = token.privilege_level()?; let level = token.privilege_level()?;
let status = match level { let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, PrivilegeLevel::NotPrivileged => {
_ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?, 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<PathBuf>) -> Result<PathBuf> {
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() { if !status.success() {
bail!( bail!(
"failed to uninstall service with status {}", "failed to uninstall service with status {}",
status.code().unwrap_or(-1) status.code().unwrap_or(-1)
); );
} }
Ok(()) 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")] #[cfg(target_os = "linux")]
fn uninstall_service() -> Result<()> { fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service"); 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")) 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 elevator = crate::utils::help::linux_elevator();
let status = if linux_running_as_root() { let status = if linux_running_as_root() {
StdCommand::new(&uninstall_path).status()? StdCommand::new(&uninstall_path).status()?
@@ -666,14 +687,7 @@ fn uninstall_service() -> Result<()> {
status.code().unwrap_or(-1) status.code().unwrap_or(-1)
); );
if !status.success() { ensure_uninstall_success(&status)
bail!(
"failed to uninstall service with status {}",
status.code().unwrap_or(-1)
);
}
Ok(())
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -688,14 +702,10 @@ fn linux_running_as_root() -> bool {
fn uninstall_service() -> Result<()> { fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service"); 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")) 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_path = macos_service_tool_path(&uninstall_path)?;
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned(); let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
@@ -711,14 +721,7 @@ fn uninstall_service() -> Result<()> {
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
if !status.success() { ensure_uninstall_success(&status)
bail!(
"failed to uninstall service with status {}",
status.code().unwrap_or(-1)
);
}
Ok(())
} }
fn install_service() -> Result<()> { fn install_service() -> Result<()> {
+19 -14
View File
@@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState
} }
pub(crate) struct Sysopt { 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<()>, guard_operation_lock: TokioMutex<()>,
reset_sysproxy: AtomicBool, reset_sysproxy: AtomicBool,
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
guard: Arc<RwLock<GuardMonitor>>, guard: Arc<RwLock<GuardMonitor>>,
} }
impl Default for Sysopt { impl Default for Sysopt {
fn default() -> Self { fn default() -> Self {
Self { Self {
update_lock: TokioMutex::new(()), proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())),
guard_operation_lock: TokioMutex::new(()), guard_operation_lock: TokioMutex::new(()),
reset_sysproxy: AtomicBool::new(false), 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)))), 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"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
static BYPASS_SEPARATOR: &str = ","; static BYPASS_SEPARATOR: &str = ",";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS;
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
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() { if custom_bypass.is_empty() {
DEFAULT_BYPASS.into() DEFAULT_BYPASS.into()
} else if use_default { } else if use_default {
@@ -361,7 +359,7 @@ impl Sysopt {
} }
} }
while self.access_guard().read().get_state().is_pendding() { 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. // `start()` false may also mean a guard was already running.
!self.access_guard().read().get_state().is_stopped() !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 /// configuration instead of a partially-applied one (e.g. SOCKS already
/// disabled but HTTP still enabled mid-transition). /// disabled but HTTP still enabled mid-transition).
pub(crate) async fn wait_idle(&self) { pub(crate) async fn wait_idle(&self) {
let _ = self.update_lock.lock().await; let _ = self.proxy_state_lock.lock().await;
} }
/// init the sysproxy /// 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<()> { 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(); let verge = Config::verge().await.latest_arc();
// Configured, not live: this runs while the Core is being started or restarted, and // 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. // asking a Core that is not up yet would only fall back here anyway.
let port = MixedPort::desired().await; let port = MixedPort::desired().await;
let pac_port = server::embedded_server_port()?; let pac_port = server::embedded_server_port()?;
// 先 await, 避免持有锁导致的 Send 问题
let bypass = get_bypass().await; let bypass = get_bypass().await;
let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
@@ -431,7 +432,7 @@ impl Sysopt {
); );
let (sys, auto, guard_type) = { 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.host = proxy_host.into();
sys.port = port; sys.port = port;
sys.bypass = bypass.into(); sys.bypass = bypass.into();
@@ -528,6 +529,10 @@ impl Sysopt {
} }
/// reset the sysproxy /// 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<()> { pub(super) async fn reset_sysproxy(&self) -> Result<()> {
if self if self
.reset_sysproxy .reset_sysproxy
@@ -539,13 +544,13 @@ impl Sysopt {
defer! { defer! {
self.reset_sysproxy.store(false, Ordering::SeqCst); 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 _guard_operation = self.guard_operation_lock.lock().await;
let drained = self.stop_proxy_guard_locked().await; let drained = self.stop_proxy_guard_locked().await;
// 直接关闭所有代理 // 直接关闭所有代理
let (sys, auto) = { let (sys, auto) = {
let (sys, auto) = &mut *self.inner_proxy.write(); let (sys, auto) = &mut *proxy_state;
sys.enable = false; sys.enable = false;
auto.enable = false; auto.enable = false;
(sys.clone(), auto.clone()) (sys.clone(), auto.clone())
+2 -1
View File
@@ -636,7 +636,8 @@ async fn create_tray_menu(
.tray_proxy_groups_display_mode .tray_proxy_groups_display_mode
.as_deref() .as_deref()
.unwrap_or("default"); .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; let proxy_group_order_map = runtime_proxy_groups_order;
+2 -11
View File
@@ -1,10 +1,7 @@
#![cfg(target_os = "windows")] #![cfg(target_os = "windows")]
use crate::utils::dirs; use crate::{core::service, utils::dirs};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub fn invoke_uwptools() -> Result<()> { pub fn invoke_uwptools() -> Result<()> {
let resource_dir = dirs::app_resources_dir()?; let resource_dir = dirs::app_resources_dir()?;
@@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> {
bail!("enableLoopback exe not found"); bail!("enableLoopback exe not found");
} }
let token = Token::with_current_process()?; service::run_maybe_elevated(&tool_path, false)?;
let level = token.privilege_level()?;
match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
_ => StdCommand::new(tool_path).status()?,
};
Ok(()) Ok(())
} }
@@ -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
}
@@ -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
}
+7 -67
View File
@@ -22,12 +22,6 @@ pub enum ChainType {
Groups(SeqMap), Groups(SeqMap),
} }
#[derive(Debug, Clone)]
pub enum ChainSupport {
ClashMeta,
ClashMetaAlpha,
}
// Helper trait to allow async conversion // Helper trait to allow async conversion
pub trait AsyncChainItemFrom { pub trait AsyncChainItemFrom {
async fn from_async(item: &PrfItem) -> Option<ChainItem>; async fn from_async(item: &PrfItem) -> Option<ChainItem>;
@@ -53,70 +47,16 @@ impl AsyncChainItemFrom for Option<ChainItem> {
uid, uid,
data: ChainType::Merge(help::read_mapping(&path).await.ok()?), data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}), }),
"rules" => { "rules" | "proxies" | "groups" => {
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?; let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem { let data = match itype {
uid, "rules" => ChainType::Rules(seq_map),
data: ChainType::Rules(seq_map), "proxies" => ChainType::Proxies(seq_map),
}) _ => ChainType::Groups(seq_map),
} };
"proxies" => { Some(ChainItem { uid, data })
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Proxies(seq_map),
})
}
"groups" => {
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Groups(seq_map),
})
} }
_ => None, _ => 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<U: Into<String>, D: Into<String>>(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,
}
}
}
+1 -12
View File
@@ -27,7 +27,7 @@ fn lowercase_key(key: &str) -> Value {
pub fn use_lowercase(config: &Mapping) -> Mapping { pub fn use_lowercase(config: &Mapping) -> Mapping {
let mut lowercased = Mapping::new(); 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() { if let Some(key_str) = key.as_str() {
lowercased.insert(lowercase_key(key_str), value.clone()); lowercased.insert(lowercase_key(key_str), value.clone());
} }
@@ -35,17 +35,6 @@ pub fn use_lowercase(config: &Mapping) -> Mapping {
lowercased 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 { pub fn use_sort(mut config: Mapping) -> Mapping {
let mut sorted = Mapping::new(); let mut sorted = Mapping::new();
HANDLE_FIELDS.into_iter().for_each(|key| { HANDLE_FIELDS.into_iter().for_each(|key| {
+64 -127
View File
@@ -8,7 +8,7 @@ mod tun;
use self::{ use self::{
chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, chain::{AsyncChainItemFrom as _, ChainItem, ChainType},
field::{use_keys, use_lowercase_owned, use_sort}, field::{use_keys, use_lowercase, use_sort},
merge::use_merge, merge::use_merge,
script::use_script, script::use_script,
seq::{SeqMap, use_seq}, seq::{SeqMap, use_seq},
@@ -34,7 +34,6 @@ type ResultLog = Vec<(String, String)>;
#[derive(Debug)] #[derive(Debug)]
struct ConfigValues { struct ConfigValues {
clash_config: Mapping, clash_config: Mapping,
clash_core: Option<String>,
enable_tun: bool, enable_tun: bool,
enable_builtin: bool, enable_builtin: bool,
socks_enabled: 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_settings = verge_arc.dns_settings_for(profile_uid);
let dns_override_confirmation = dns_settings.confirmation; let dns_override_confirmation = dns_settings.confirmation;
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
Some(verge_arc.get_valid_clash_core()),
enable_tun_mode.unwrap_or(false), enable_tun_mode.unwrap_or(false),
enable_builtin_enhanced.unwrap_or(true), enable_builtin_enhanced.unwrap_or(true),
verge_socks_enabled.unwrap_or(false), verge_socks_enabled.unwrap_or(false),
@@ -148,7 +146,6 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
ConfigValues { ConfigValues {
clash_config, clash_config,
clash_core,
enable_tun, enable_tun,
enable_builtin, enable_builtin,
socks_enabled, socks_enabled,
@@ -199,35 +196,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
let name = current_item.name.clone().unwrap_or_default(); 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!( 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 { chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,),
uid: "".into(), chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,),
data: ChainType::Merge(Mapping::new()), 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(&script_uid).ok(), || ChainItem { chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,),
uid: "".into(), chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,),
},),
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()),
},),
); );
Ok(ProfileItems { Ok(ProfileItems {
@@ -243,16 +230,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
}) })
} }
async fn process_global_items( async fn process_override_items(
mut config: Mapping, mut config: Mapping,
mut exists_keys: Vec<String>, mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>, mut result_map: HashMap<String, ResultLog>,
global_merge: ChainItem, merge_item: ChainItem,
global_script: ChainItem, script_item: ChainItem,
profile_name: &String, profile_name: &String,
authoritative: &AuthoritativeFields, authoritative: &AuthoritativeFields,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) { ) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Merge(merge) = global_merge.data { if let ChainType::Merge(merge) = merge_item.data {
exists_keys.extend(use_keys(&merge)); exists_keys.extend(use_keys(&merge));
let before = authoritative.current(&config); let before = authoritative.current(&config);
config = use_merge(&merge, config); config = use_merge(&merge, config);
@@ -262,11 +249,11 @@ async fn process_global_items(
.map(discarded_note) .map(discarded_note)
.collect(); .collect();
if !notes.is_empty() { 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 before = authoritative.current(&config);
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
exists_keys.extend(changed_keys); exists_keys.extend(changed_keys);
@@ -277,7 +264,7 @@ async fn process_global_items(
.into_iter() .into_iter()
.map(discarded_note), .map(discarded_note),
); );
result_map.insert(global_script.uid, logs); result_map.insert(script_item.uid, logs);
} }
(config, exists_keys, result_map) (config, exists_keys, result_map)
@@ -544,46 +531,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping {
config config
} }
async fn process_profile_items(
mut config: Mapping,
mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>,
merge_item: ChainItem,
script_item: ChainItem,
profile_name: &String,
authoritative: &AuthoritativeFields,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
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. /// Merges the app's clash config over the profile; switches are parameters to keep it pure.
fn merge_default_config( fn merge_default_config(
mut config: Mapping, mut config: Mapping,
@@ -655,25 +602,12 @@ fn merge_default_config(
config config
} }
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping { fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping {
if enable_builtin { if enable_builtin {
let items: Vec<_> = ChainItem::builtin() // The JS path saw a lowercased view; lowercase once, only when the builtin runs.
.into_iter() config = use_lowercase(&config);
.filter(|(s, _)| s.is_support(clash_core.as_ref())) logging!(debug, Type::Core, "run builtin script verge_hy_alpn");
.map(|(_, c)| c) config = builtin_hy_alpn(config);
.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,
};
}
}
} }
config config
@@ -693,14 +627,6 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping {
config 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 { fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
// built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in // built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in
const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"]; 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 cfg_vals = get_config_values(profile_uid).await;
let ConfigValues { let ConfigValues {
clash_config, clash_config,
clash_core,
enable_tun, enable_tun,
enable_builtin, enable_builtin,
socks_enabled, socks_enabled,
@@ -929,13 +854,13 @@ pub async fn enhance(
tproxy_enabled, 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 = use_tun(config, enable_tun);
let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await; let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await;
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings); 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, config,
exists_keys, exists_keys,
result_map, result_map,
@@ -946,7 +871,7 @@ pub async fn enhance(
) )
.await; .await;
let (config, exists_keys, result_map) = process_profile_items( let (config, exists_keys, result_map) = process_override_items(
config, config,
exists_keys, exists_keys,
result_map, result_map,
@@ -1428,7 +1353,7 @@ mod authoritative_field_tests {
mod tests { mod tests {
use super::{ use super::{
AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address, 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; use std::collections::HashMap;
@@ -1476,22 +1401,28 @@ mod tests {
data: ChainType::Merge(merge.clone()), data: ChainType::Merge(merge.clone()),
}; };
let profile_name = "test-profile".into(); let profile_name = "test-profile".into();
let (config, keys, logs) = process_global_items( let (config, keys, logs) = process_override_items(
config, config,
vec![], vec![],
HashMap::new(), HashMap::new(),
merge_item("global-merge"), merge_item("global-merge"),
ChainItem::to_script("global-script", script), ChainItem {
uid: "global-script".into(),
data: ChainType::Script(script.into()),
},
&profile_name, &profile_name,
&authoritative, &authoritative,
) )
.await; .await;
let (config, _, logs) = process_profile_items( let (config, _, logs) = process_override_items(
config, config,
keys, keys,
logs, logs,
merge_item("profile-merge"), merge_item("profile-merge"),
ChainItem::to_script("profile-script", script), ChainItem {
uid: "profile-script".into(),
data: ChainType::Script(script.into()),
},
&profile_name, &profile_name,
&authoritative, &authoritative,
) )
@@ -1536,16 +1467,19 @@ mod tests {
nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}", nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}",
)), )),
}; };
let global_script = ChainItem::to_script( let global_script = ChainItem {
"Script", uid: "Script".into(),
r#"function main(config) { data: ChainType::Script(
r#"function main(config) {
config["global-script-wins"] = "global-script"; config["global-script-wins"] = "global-script";
config["profile-merge-wins"] = "global-script"; config["profile-merge-wins"] = "global-script";
config["profile-script-wins"] = "global-script"; config["profile-script-wins"] = "global-script";
config.nested.winner = "global-script"; config.nested.winner = "global-script";
return config; return config;
}"#, }"#
); .into(),
),
};
let profile_merge = ChainItem { let profile_merge = ChainItem {
uid: "profile-merge".into(), uid: "profile-merge".into(),
data: ChainType::Merge(mapping( data: ChainType::Merge(mapping(
@@ -1553,18 +1487,21 @@ mod tests {
nested: {winner: profile-merge}}", nested: {winner: profile-merge}}",
)), )),
}; };
let profile_script = ChainItem::to_script( let profile_script = ChainItem {
"profile-script", uid: "profile-script".into(),
r#"function main(config) { data: ChainType::Script(
r#"function main(config) {
config["profile-script-wins"] = "profile-script"; config["profile-script-wins"] = "profile-script";
config.nested.winner = "profile-script"; config.nested.winner = "profile-script";
return config; return config;
}"#, }"#
); .into(),
),
};
let profile_name = "test-profile".into(); let profile_name = "test-profile".into();
let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new()); 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, config,
exists_keys, exists_keys,
HashMap::new(), HashMap::new(),
@@ -1574,7 +1511,7 @@ mod tests {
&authoritative, &authoritative,
) )
.await; .await;
let (config, exists_keys, _) = process_profile_items( let (config, exists_keys, _) = process_override_items(
config, config,
exists_keys, exists_keys,
result_map, result_map,
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::{process::AsyncHandler, utils::tmpl}; use crate::{process::AsyncHandler, utils::tmpl};
use super::field::{use_lowercase, use_lowercase_owned}; use super::field::use_lowercase;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute}; use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
@@ -22,7 +22,7 @@ type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>)
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult { pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
// The template is identity-plus-lowercasing; skip the Boa round-trip. // The template is identity-plus-lowercasing; skip the Boa round-trip.
if script == tmpl::ITEM_SCRIPT { 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)) { 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) { 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) => { Err(err) => {
logging!( logging!(
error, error,
+1 -1
View File
@@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("fake-ip"); .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, "enable", true);
revise!(dns_val, "ipv6", ipv6_val); revise!(dns_val, "ipv6", ipv6_val);
+36 -43
View File
@@ -43,14 +43,14 @@ async fn finalize_restored_verge_config(
let restored_clash = IClashTemp::new().await; let restored_clash = IClashTemp::new().await;
let clash_draft = Config::clash().await; let clash_draft = Config::clash().await;
clash_draft.edit_draft(|d| { clash_draft.edit_draft(|d| {
*d = restored_clash.clone(); *d = restored_clash;
}); });
clash_draft.apply(); clash_draft.apply();
let restored_profiles = IProfiles::new().await; let restored_profiles = IProfiles::new().await;
let profiles_draft = Config::profiles().await; let profiles_draft = Config::profiles().await;
profiles_draft.edit_draft(|d| { profiles_draft.edit_draft(|d| {
*d = restored_profiles.clone(); *d = restored_profiles;
}); });
profiles_draft.apply(); profiles_draft.apply();
@@ -62,20 +62,20 @@ async fn finalize_restored_verge_config(
*d = restored.clone(); *d = restored.clone();
}); });
verge_draft.apply(); verge_draft.apply();
}
// Turn it off here; a failing core side effect below would otherwise skip this step. // Turn it off here; a failing core side effect below would otherwise skip this step.
if !restored.enable_system_proxy.unwrap_or_default() { if !restored.enable_system_proxy.unwrap_or_default() {
let result = async { let result = async {
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
proxy_control::apply().await?; proxy_control::apply().await?;
proxy_control::refresh_guard().await proxy_control::refresh_guard().await
} }
.await; .await;
if let Err(err) = result { if let Err(err) = result {
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
if SystemProxyStateUnknown::is_in(&err) { if SystemProxyStateUnknown::is_in(&err) {
return Err(err); return Err(err);
}
} }
} }
} }
@@ -134,14 +134,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))] #[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
pub async fn restore_webdav_backup(filename: String) -> Result<()> { 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() let backup_storage_path = app_home_dir()
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))? .map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
.join(filename.as_str()); .join(filename.as_str());
@@ -158,12 +171,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
err err
})?; })?;
let value = backup_storage_path.clone(); let res = restore_backup_archive(backup_storage_path.clone()).await;
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 _ = backup_storage_path.remove_if_exists().await; let _ = backup_storage_path.remove_if_exists().await;
res res
} }
@@ -332,22 +340,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
return Err(anyhow!("Backup file not found: {}", filename)); return Err(anyhow!("Backup file not found: {}", filename));
} }
let (webdav_url, webdav_username, webdav_password) = { restore_backup_archive(target_path).await
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(())
} }
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))] #[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
+4 -40
View File
@@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce};
use crate::{ use crate::{
config::{Config, MixedPort}, config::{Config, MixedPort},
core::{CoreManager, handle, tray}, core::{CoreManager, handle, tray},
feat::clean_async, feat::run_exit_cleanup_flow,
process::AsyncHandler, process::AsyncHandler,
utils, utils,
}; };
@@ -14,15 +14,8 @@ use smartstring::alias::String;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| { static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
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)
});
pub async fn restart_clash_core() { pub async fn restart_clash_core() {
match CoreManager::global().restart_core().await { match CoreManager::global().restart_core().await {
@@ -38,38 +31,9 @@ pub async fn restart_clash_core() {
pub async fn restart_app() { pub async fn restart_app() {
logging!(debug, Type::System, "启动重启应用流程"); logging!(debug, Type::System, "启动重启应用流程");
handle::Handle::global().set_is_exiting(); let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
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 }
);
if !cleanup_result.core_stopped { 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; return;
} }
-15
View File
@@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
record_switched_node(group_name, proxy_name).await; record_switched_node(group_name, proxy_name).await;
announce(Refresh::Proxies); announce(Refresh::Proxies);
let _ = tray::Tray::global().update_menu().await; let _ = tray::Tray::global().update_menu().await;
return;
} }
Err(err) => { Err(err) => {
logging!(error, Type::Tray, "切换代理失败: {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<Option<(String, Option<PrfOption>)>> { async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
+10 -5
View File
@@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() {
logging!(info, Type::Window, "Window toggle result: {result:?}"); logging!(info, Type::Window, "Window toggle result: {result:?}");
} }
pub async fn quit() { pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult {
logging!(debug, Type::System, "启动退出流程");
// 设置退出标志
handle::Handle::global().set_is_exiting(); handle::Handle::global().set_is_exiting();
Config::apply_all_and_save_file().await; Config::apply_all_and_save_file().await;
@@ -162,11 +160,18 @@ pub async fn quit() {
} }
} }
handle::Handle::notice( handle::Handle::notice(
NoticeStatus::AppQuitCoreStopFailed, stop_failed_notice,
cleanup_result.stop_error.unwrap_or_default(), 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(); utils::server::shutdown_embedded_server();
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
+2 -5
View File
@@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> {
Value::String("*.arpa".into()), Value::String("*.arpa".into()),
Value::String("time.*.com".into()), Value::String("time.*.com".into()),
Value::String("ntp.*.com".into()), Value::String("ntp.*.com".into()),
Value::String("time.*.com".into()),
Value::String("+.market.xiaomi.com".into()), Value::String("+.market.xiaomi.com".into()),
Value::String("localhost.ptlogin2.qq.com".into()), Value::String("localhost.ptlogin2.qq.com".into()),
Value::String("*.msftncsi.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 app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir()?; let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() { // app_home is created by `ensure_directories` in `init_config`, which always runs first.
std::mem::drop(fs::create_dir_all(&app_dir).await);
}
if !res_dir.exists() { 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"]; let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
+41 -41
View File
@@ -48,6 +48,17 @@ enum TlsRootMode {
StaticWebpkiRoots, StaticWebpkiRoots,
} }
/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer.
pub(crate) fn build_webpki_rooted_tls_config() -> Result<rustls::ClientConfig> {
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; pub struct NetworkManager;
impl NetworkManager { impl NetworkManager {
@@ -102,13 +113,7 @@ impl NetworkManager {
} }
fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> { fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> {
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); let mut config = build_webpki_rooted_tls_config()?;
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();
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(config) Ok(config)
@@ -155,6 +160,23 @@ impl NetworkManager {
detail.contains("protocolversion") || detail.contains("protocol version") 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<T, F, Fut>(allow_fallback: bool, mut request: F) -> Result<T>
where
F: FnMut(TlsRootMode) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
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( async fn get_with_tls_mode(
&self, &self,
url: &str, url: &str,
@@ -264,34 +286,21 @@ impl NetworkManager {
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let platform_result = self Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| {
.get_with_tls_mode( let user_agent = user_agent.clone();
url, async move {
proxy_type, self.get_with_tls_mode(
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(
url, url,
proxy_type, proxy_type,
timeout_secs, timeout_secs,
user_agent, user_agent,
accept_invalid_certs, accept_invalid_certs,
TlsRootMode::StaticWebpkiRoots, tls_root_mode,
) )
.await .await
.map_err(|fallback_err| { }
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") })
}), .await
Err(err) => Err(err),
}
} }
async fn get_bytes_with_tls_mode( async fn get_bytes_with_tls_mode(
@@ -340,19 +349,10 @@ impl NetworkManager {
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
max_bytes: usize, max_bytes: usize,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let platform_result = self Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move {
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier) self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode)
.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)
.await .await
.map_err(|fallback_err| { })
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") .await
}),
Err(err) => Err(err),
}
} }
} }
-14
View File
@@ -1,8 +1,6 @@
use crate::core::notify::NoticeStatus; use crate::core::notify::NoticeStatus;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Result;
use crate::{ use crate::{
config::Config, config::Config,
core::{ core::{
@@ -97,18 +95,6 @@ async fn resolve_setup() {
resolve_done(); 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() { fn init_scheme() {
logging_error!(Type::Setup, init::init_scheme()); logging_error!(Type::Setup, init::init_scheme());
} }
+2 -2
View File
@@ -365,7 +365,7 @@ export type IVerge_Deserialize = {
start_page?: string | null start_page?: string | null
startup_script?: string | null startup_script?: string | null
traffic_graph?: boolean | null traffic_graph?: boolean | null
/** show memory info (only for Clash Meta) */ /** show memory info (only for Mihomo) */
enable_memory_usage?: boolean | null enable_memory_usage?: boolean | null
enable_group_icon?: boolean | null enable_group_icon?: boolean | null
pause_render_traffic_stats_on_blur?: boolean | null pause_render_traffic_stats_on_blur?: boolean | null
@@ -455,7 +455,7 @@ export type IVerge_Serialize = {
start_page: string | null start_page: string | null
startup_script: string | null startup_script: string | null
traffic_graph: boolean | null traffic_graph: boolean | null
/** show memory info (only for Clash Meta) */ /** show memory info (only for Mihomo) */
enable_memory_usage: boolean | null enable_memory_usage: boolean | null
enable_group_icon?: boolean | null enable_group_icon?: boolean | null
pause_render_traffic_stats_on_blur: boolean | null pause_render_traffic_stats_on_blur: boolean | null