mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 04:12:08 +08:00
* refactor(utils): drop dead reset helper and tidy init_resources
- remove resolve_reset_async, which had no callers
- drop the duplicated time.*.com entry from the default fake-ip-filter
- init_resources no longer recreates app_home (ensure_directories during
init_config already created it) and now propagates res_dir creation errors
* refactor(profile): remove identical blind retry when switching node
The second select_node_for_group call in switch_proxy_node was a
zero-interval retry with identical arguments. Its success path was a
degraded copy of the first (missing the Refresh::Proxies announcement
ever since the old force-refresh emit was removed), and its distinct
"回退/最终失败" log wording misled maintainers. Keep a single call,
the primary success aftermath, and one error log.
* refactor(enhance): simplify tun mode check and lowercase helpers
- Drop the always-true second clause in use_tun's enhanced-mode check:
the key is absent only when the unwrap_or default already makes
current_mode == "fake-ip" true.
- Collapse use_lowercase into a delegation to use_lowercase_owned.
* refactor(core): drop unreachable hotkey dtor and dedupe tray predicate
Remove impl Drop for Hotkey: the instance lives in a OnceLock static via
the singleton! macro and is never dropped; exit-time unregistration is
already handled by Hotkey::reset().
Spell the include_proxy_groups/display-mode predicate identically at
both evaluation sites in tray menu building; behavior is unchanged
(Option != Some("disable") is equivalent to unwrap_or("default") != "disable").
* refactor(config): fold port guards into single helper
The five IClashTemp port guards duplicated the same read/parse/default
logic with drifting defaults: some hardcoded 7895/7898/7899 while others
referenced network::ports constants. Route them all through one
guard_port_value helper and use the constants everywhere, keeping each
guard's name, key and cfg attributes.
* refactor(config): dedupe prfitem factories and companion seeding
from_rules/from_proxies/from_groups differed only in uid prefix, type
string and template, so they now delegate to a shared from_seq_item.
The five current_* accessors collapse into a local macro. The identical
five-block companion seeding in from_local and from_url moves into
ensure_companion_items, preserving seeding order and error propagation.
* refactor(config): share patch macro and profiles helpers
IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each
hand-rolled the same copy-Some-overwrite pattern; they now share one
patch_opt_field macro (the or-else merge form is field-for-field
equivalent for Option fields). The four profiles *_safe wrappers that
inlined Config::profiles().with_data_modify boilerplate delegate to a
with_profiles_edit helper, and record/forget_selected_node share the
current-item lookup prelude via current_item_mut.
* refactor(enhance): merge chain processing and drop unused builtin js payload
- collapse the identical process_global_items/process_profile_items into
one parameterized process_override_items (entry uid sources unchanged)
- fold the identical rules/proxies/groups arms of ChainItem::from_async
into a single arm with an inner dispatch
- stop embedding builtin JS payloads never executed by any path: the
enhance loop dispatches builtin items by uid to native implementations,
so ChainItem::builtin now carries empty script data (uids unchanged)
- source ProfileItems fallback literals from Default instead of a second
hand-written copy in collect_profile_items
* refactor(net): reuse webpki tls config and fallback
Extract build_webpki_rooted_tls_config so the delay-test TLS connector
and the reqwest clients share one static-webpki-roots builder; ALPN
stays caller-specific (only reqwest offers h2).
Collapse the duplicated platform-verifier -> static-webpki-roots retry
in get/get_bytes into request_with_static_webpki_fallback, gated by the
caller's certificate-verification setting.
* refactor(app): extract common exit cleanup
restart_app and quit duplicate the whole exit preparation: set the
exiting flag, persist configs, run interactive cleanup, and on a failed
core stop clear the flag, restore public DNS under the lifecycle lock
on macOS, and send the stop-failed notice.
Move that flow into run_exit_cleanup_flow next to clean_async, taking
the notice variant as a parameter and returning the CleanupResult so
each caller keeps its own tail: restart aborts before shutdown, quit
always shuts down and exits with the cleanup code.
* refactor(backup): share restore finalize path
restore_webdav_backup and restore_local_backup duplicated the
credential extraction (backup creation strips the WebDAV credentials,
so both re-inject the live ones), the archive extraction and the
finalize tail. Move that tail into a private restore_backup_archive
helper so the two entries keep only their download/locate logic; the
config-write lock scope and the off-runtime extraction land in one
place. The webdav entry keeps its downloaded-file cleanup.
* refactor(sysopt): drop redundant lock and busy-wait
The proxy target pair sat behind an Arc<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.
* refactor(sysopt): share macos bypass formatting
The macOS default bypass list and its three-way composition (empty ->
default, default + custom, custom only) were maintained twice: in
sysopt's format_bypass and inline in proxy_control's service_bypass.
Keep the literal once in constants and compose both paths through
format_bypass; service_bypass keeps its NUL check and UTF-8 truncation.
* refactor(core): dedupe window listeners, elevated runners, backup settings
- Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label.
- Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms.
- Make the auto-backup watch channel the single settings source, replacing the duplicate Arc<RwLock> copy with a stored receiver.
* refactor(enhance): fold lowercase helpers into single owned function
use_lowercase_owned disappears; the surviving use_lowercase takes the
owned Mapping and borrowed callers clone at the call site.
* refactor(enhance): rename ChainSupport variants to Mihomo names
Clash is a legacy name; the variants match the verge-mihomo and
verge-mihomo-alpha cores they gate.
* refactor(enhance): lowercase takes a reference and chain support is copy
use_lowercase borrows the mapping and clones only the values; call
sites no longer clone whole configs. ChainSupport is a fieldless enum
and now Copy; builtin() registers each support pair without clones.
* refactor(config): name prfitem constructor for what it builds
new_seq_item states that a fresh item is built from a template; the
from_ prefix wrongly implied an existing source. append_companion_item
returns the uid directly: append_item bails on a missing uid, so the
Option was always Some on the Ok path.
* refactor(sysopt): name the proxy state lock for its full role
The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock
says what it guards; the guard locals follow. constants::proxy held a
single sysproxy constant and is renamed sysproxy.
* refactor: replace remaining clash meta naming with mihomo
Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.
* refactor(enhance): drop builtin core filtering that never discriminates
Both valid cores matched a different ChainSupport variant, yet each
variant registered the same uid pair, so the filtered builtin list was
identical for stable and alpha. Invalid cores are coerced at startup
(validate_and_fix_config / get_valid_clash_core) and the None arm of
is_support was unreachable. builtin() now returns the items directly
and the clash_core threading through ConfigValues goes away; re-add
core discrimination when a builtin actually differs per core.
* refactor(enhance): drop unreachable mode script guard
merge_default_config overwrites the profile's mode with the verge-owned
clash config, whose mode is always present (template seeds rule and
patch semantics never clear it) and restricted to rule/global/direct;
change_clash_mode is rejected by the core before persisting anything
else. mode:script can therefore only arrive through a hand-crafted
patch_clash call, and for that path an explicit invalid-mode validation
error is more honest than silently rewriting to rule. mihomo rejects
mode:script outright (invalid mode), and user merge/script chains run
after the builtins, so the guard never covered them anyway.
* refactor(enhance): call the hysteria alpn builtin directly
builtin() existed to feed the uid-dispatch loop; with a single
builtin the vec and match are ceremony. apply_builtin_scripts calls
builtin_hy_alpn directly and to_script goes away; tests construct
ChainItem literals the way the merge items already did.
234 lines
9.2 KiB
Rust
234 lines
9.2 KiB
Rust
use crate::core::notify::NoticeStatus;
|
|
use crate::core::notify::{Refresh, announce};
|
|
use crate::{
|
|
cmd,
|
|
config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe},
|
|
core::{CoreManager, handle, tray, validate::ValidationOutcome},
|
|
utils::help::{mask_err, mask_url},
|
|
};
|
|
use anyhow::{Result, bail};
|
|
use clash_verge_logging::{Type, logging};
|
|
use smartstring::alias::String;
|
|
|
|
/// Toggle proxy profile
|
|
pub async fn toggle_proxy_profile(profile_index: String) {
|
|
if let Err(err) = cmd::patch_profiles_config_by_profile_index(profile_index.clone()).await {
|
|
logging!(
|
|
error,
|
|
Type::Config,
|
|
"toggle proxy profile {profile_index} failed: {err}"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Tell the profile which node this group is on now.
|
|
///
|
|
/// The core is already switched by the time this runs, so failing to record is not a reason to
|
|
/// report the switch as failed — but it is worth a log line, because what the profile holds is
|
|
/// what gets re-applied the next time a core starts.
|
|
async fn record_switched_node(group_name: &str, proxy_name: &str) {
|
|
if let Err(error) = crate::config::profiles::record_selected_node(group_name, proxy_name).await {
|
|
logging!(
|
|
warn,
|
|
Type::Config,
|
|
"切换代理成功但未能记录到配置: {} -> {}, 错误: {error:#}",
|
|
group_name,
|
|
proxy_name
|
|
);
|
|
}
|
|
}
|
|
|
|
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
|
|
match handle::Handle::mihomo()
|
|
.select_node_for_group(group_name, proxy_name)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
record_switched_node(group_name, proxy_name).await;
|
|
announce(Refresh::Proxies);
|
|
let _ = tray::Tray::global().update_menu().await;
|
|
}
|
|
Err(err) => {
|
|
logging!(error, Type::Tray, "切换代理失败: {err:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
|
|
let profiles = Config::profiles().await;
|
|
let profiles = profiles.latest_arc();
|
|
let item = profiles.get_item(uid)?;
|
|
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
|
|
|
if !is_remote {
|
|
logging!(info, Type::Config, "[订阅更新] 不是远程订阅,跳过更新");
|
|
Ok(None)
|
|
} else if item.url.is_none() {
|
|
logging!(warn, Type::Config, "[订阅更新] 缺少URL,无法更新");
|
|
bail!("failed to get the profile item url");
|
|
} else if !ignore_auto_update && !item.option.as_ref().and_then(|o| o.allow_auto_update).unwrap_or(true) {
|
|
logging!(info, Type::Config, "[订阅更新] 禁止自动更新,跳过更新");
|
|
Ok(None)
|
|
} else {
|
|
logging!(
|
|
info,
|
|
Type::Config,
|
|
"[订阅更新] 远程订阅,URL: {}",
|
|
mask_url(
|
|
item.url
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
|
)
|
|
);
|
|
Ok(Some((
|
|
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
|
|
item.option.clone(),
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(uid = %uid, strategy = tracing::field::Empty, configured_elapsed = tracing::field::Empty, clash_elapsed = tracing::field::Empty, system_elapsed = tracing::field::Empty))]
|
|
async fn perform_profile_update(
|
|
uid: &String,
|
|
url: &String,
|
|
opt: Option<&PrfOption>,
|
|
option: Option<&PrfOption>,
|
|
is_mannual_trigger: bool,
|
|
) -> Result<()> {
|
|
let merged_opt = PrfOption::merge(opt, option);
|
|
let profiles = Config::profiles().await;
|
|
let profiles_arc = profiles.latest_arc();
|
|
let profile_name = profiles_arc
|
|
.get_item(uid)
|
|
.ok()
|
|
.and_then(|item| item.name.clone())
|
|
.unwrap_or_else(|| String::from("UnKnown Profile"));
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum Strategy {
|
|
Configured,
|
|
Clash,
|
|
System,
|
|
}
|
|
|
|
let result =
|
|
crate::utils::retry::try_strategies(Strategy::Configured, [Strategy::Clash, Strategy::System], |strategy| {
|
|
let mut options = merged_opt.clone();
|
|
async move {
|
|
let (name, success, failure) = match strategy {
|
|
Strategy::Configured => (
|
|
"configured",
|
|
"[订阅更新] 更新订阅配置成功",
|
|
"[订阅更新] 直接更新失败: {},尝试使用Clash代理更新",
|
|
),
|
|
Strategy::Clash => (
|
|
"clash",
|
|
"[订阅更新] 使用 Clash代理 更新订阅配置成功",
|
|
"[订阅更新] Clash代理更新失败: {},尝试使用系统代理更新",
|
|
),
|
|
Strategy::System => (
|
|
"system",
|
|
"[订阅更新] 使用 系统代理 更新订阅配置成功",
|
|
"[订阅更新] 系统代理更新失败: {},所有重试均已失败",
|
|
),
|
|
};
|
|
match strategy {
|
|
Strategy::Configured => {}
|
|
Strategy::Clash | Strategy::System => {
|
|
let option = options.get_or_insert_with(PrfOption::default);
|
|
option.self_proxy = Some(matches!(strategy, Strategy::Clash));
|
|
option.with_proxy = Some(matches!(strategy, Strategy::System));
|
|
}
|
|
}
|
|
tracing::Span::current().record("strategy", name);
|
|
let started = std::time::Instant::now();
|
|
let result = PrfItem::from_url(url, None, None, options.as_ref()).await;
|
|
let elapsed_field = match strategy {
|
|
Strategy::Configured => "configured_elapsed",
|
|
Strategy::Clash => "clash_elapsed",
|
|
Strategy::System => "system_elapsed",
|
|
};
|
|
tracing::Span::current().record(elapsed_field, tracing::field::debug(started.elapsed()));
|
|
match &result {
|
|
Ok(_) => logging!(info, Type::Config, "{success}"),
|
|
Err(error) => logging!(
|
|
warn,
|
|
Type::Config,
|
|
"{}",
|
|
failure.replace("{}", &mask_err(&format!("{error:#}")))
|
|
),
|
|
}
|
|
result
|
|
}
|
|
})
|
|
.await;
|
|
let last_err = match result {
|
|
Ok((strategy, mut item)) => {
|
|
// A persistence error must not replay a successful download with another transport.
|
|
profiles_update_item_safe(uid, &mut item).await?;
|
|
if !matches!(strategy, Strategy::Configured) {
|
|
handle::Handle::notice(NoticeStatus::UpdateWithClashProxy, profile_name.as_str());
|
|
}
|
|
return Ok(());
|
|
}
|
|
Err(error) => error,
|
|
};
|
|
|
|
let last_err = mask_err(&last_err.to_string());
|
|
if is_mannual_trigger {
|
|
handle::Handle::notice(
|
|
NoticeStatus::UpdateFailedEvenWithClash,
|
|
format!("{profile_name} - {last_err}"),
|
|
);
|
|
}
|
|
bail!(last_err)
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(uid = %uid, manual = is_mannual_trigger))]
|
|
pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual_trigger: bool) -> Result<()> {
|
|
let url_opt = should_update_profile(uid, is_mannual_trigger).await?;
|
|
|
|
let profile_persisted = match url_opt {
|
|
Some((url, opt)) => {
|
|
perform_profile_update(uid, &url, opt.as_ref(), option, is_mannual_trigger).await?;
|
|
true
|
|
}
|
|
None => false,
|
|
};
|
|
let profiles = Config::profiles().await;
|
|
let is_current = profiles.latest_arc().current.as_ref() == Some(uid);
|
|
let should_refresh = is_current || (!profile_persisted && is_mannual_trigger);
|
|
|
|
if should_refresh {
|
|
logging!(debug, Type::Config, "[订阅更新] 更新内核配置");
|
|
match CoreManager::global().update_config_with_force(is_mannual_trigger).await {
|
|
Ok(outcome) if outcome.is_valid() => announce(Refresh::Clash),
|
|
Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => {
|
|
logging!(info, Type::Config, "[订阅更新] 本次配置刷新已跳过: {}", outcome);
|
|
}
|
|
result => {
|
|
let message = match result {
|
|
Ok(outcome) => outcome.to_string(),
|
|
Err(err) => err.to_string(),
|
|
};
|
|
let message = mask_err(&message);
|
|
let message = if profile_persisted {
|
|
format!("订阅已保存,但未能确认已应用到内核: {message}")
|
|
} else {
|
|
message
|
|
};
|
|
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", message);
|
|
handle::Handle::notice(NoticeStatus::UpdateFailed, message.as_str());
|
|
bail!(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 增强配置
|
|
pub async fn enhance_profiles() -> Result<ValidationOutcome> {
|
|
CoreManager::global().update_config_forced().await
|
|
}
|