Files
clash-verge-rev/src-tauri/src/utils/resolve/mod.rs
T
Tunglies 9a904af3aa refactor: backend cleanup (#8436)
* 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.
2026-11-03 10:48:44 +00:00

245 lines
7.1 KiB
Rust

use crate::core::notify::NoticeStatus;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::{
config::Config,
core::{
CoreManager, Timer,
handle::Handle,
hotkey::Hotkey,
logger,
manager::CoreFailure,
service::{SERVICE_MANAGER, ServiceManager},
tray::Tray,
},
core::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot},
feat,
process::AsyncHandler,
utils::{init, server, window_manager::WindowManager},
};
use clash_verge_logging::{Type, logging, logging_error};
#[cfg(target_os = "macos")]
pub mod dns;
mod scheme;
pub(crate) mod window;
mod window_script;
static RESOLVE_DONE: AtomicBool = AtomicBool::new(false);
pub(crate) fn init_work_dir_and_logger() -> anyhow::Result<()> {
AsyncHandler::block_on(async {
init_work_config().await;
logging!(info, Type::Setup, "Initializing logger");
logger::init().await?;
Ok(())
})
}
pub(crate) fn resolve_setup_sync() {
AsyncHandler::spawn(|| async {
AsyncHandler::spawn_blocking(init_scheme);
AsyncHandler::spawn_blocking(server::embed_server);
#[cfg(target_os = "linux")]
AsyncHandler::spawn_blocking(watch_linux_theme_changed);
});
}
pub(crate) fn resolve_setup_async() {
AsyncHandler::spawn(resolve_setup);
}
#[tracing::instrument(skip_all, level = "info")]
async fn resolve_setup() {
logging!(info, Type::ClashVergeRev, "Version: {}", env!("CARGO_PKG_VERSION"));
// Migrate before windows or timers can change the loaded config.
logging_error!(Type::Setup, init::migrate_short_update_intervals().await);
#[cfg(target_os = "macos")]
resolve_dock_show().await;
init_startup_script().await;
init_service_manager().await;
let config_initialized = init_verge_config_before_window().await;
init_window().await;
feat::reconcile_startup_tun_availability().await;
init_resources().await;
if let Err(e) = init::init_dns_config().await {
logging!(warn, Type::Setup, "DNS config initialization failed: {}", e);
}
if config_initialized {
init_verge_config().await;
}
Config::verify_config_initialization().await;
// Live before the core starts, so a service appearing mid-start is not missed.
#[cfg(target_os = "macos")]
crate::core::network_watch::start();
let core_init = AsyncHandler::spawn(|| async {
init_core_manager().await;
});
let _ = futures::join!(
core_init,
init_tray(),
init_timer(),
init_hotkey(),
init_auto_lightweight_boot(),
init_auto_backup(),
init_silent_updater(),
);
Handle::refresh_clash();
refresh_tray_menu().await;
resolve_done();
}
fn init_scheme() {
logging_error!(Type::Setup, init::init_scheme());
}
pub(crate) async fn resolve_scheme(param: &str) {
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
}
#[cfg(target_os = "linux")]
fn watch_linux_theme_changed() {
match dark_light::subscribe() {
Ok(watcher) => {
for mode in watcher.iter() {
use crate::core::notification;
let theme = match mode {
dark_light::Mode::Light => tauri::Theme::Light,
dark_light::Mode::Dark => tauri::Theme::Dark,
dark_light::Mode::Unspecified => tauri::Theme::Light, // fallback to light
};
notification::NotificationSystem::send_event(
Handle::app_handle().clone(),
notification::FrontendEvent::ThemeChanged { theme },
);
}
}
Err(e) => {
logging_error!(Type::Setup, "Fail to watch linux theme changed: {}", e);
}
}
}
async fn init_resources() {
logging_error!(Type::Setup, init::init_resources().await);
}
async fn init_startup_script() {
logging_error!(Type::Setup, init::startup_script().await);
}
async fn init_timer() {
logging_error!(Type::Setup, Timer::global().init().await);
}
async fn init_hotkey() {
let skip_register_hotkeys = !Config::verge().await.latest_arc().enable_global_hotkey.unwrap_or(true);
logging_error!(Type::Setup, Hotkey::global().init(skip_register_hotkeys).await);
}
async fn init_auto_lightweight_boot() {
logging_error!(Type::Setup, auto_lightweight_boot().await);
}
async fn init_auto_backup() {
logging_error!(Type::Setup, AutoBackupManager::global().init().await);
}
async fn init_silent_updater() {
use crate::core::SilentUpdater;
use crate::core::handle::Handle;
logging!(debug, Type::Setup, "Initializing silent updater...");
let app_handle = Handle::app_handle();
// Install cached updates before starting background checks.
if SilentUpdater::global().try_install_on_startup(app_handle).await {
logging!(info, Type::Setup, "Update installed at startup, restarting...");
feat::restart_app().await;
}
let app_handle = app_handle.clone();
tokio::spawn(async move {
SilentUpdater::global().start_background_check(app_handle).await;
});
logging!(info, Type::Setup, "Silent updater initialized");
}
// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
#[cfg(target_os = "linux")]
pub(crate) fn init_signal() {
logging!(debug, Type::Setup, "Initializing signal handlers...");
clash_verge_signal::register(feat::quit);
}
async fn init_work_config() {
logging_error!(Type::Setup, init::init_config().await);
}
async fn init_tray() {
logging_error!(Type::Setup, Tray::global().init().await);
}
async fn init_verge_config() {
logging_error!(Type::Setup, Config::init_runtime_config().await);
}
async fn init_verge_config_before_window() -> bool {
let result = Config::init_config_before_window().await;
let success = result.is_ok();
logging_error!(Type::Setup, result);
success
}
async fn init_service_manager() {
clash_verge_service_ipc::set_config(Some(ServiceManager::config())).await;
SERVICE_MANAGER.detect_startup_status().await;
}
async fn init_core_manager() -> bool {
match CoreManager::global().init().await {
Ok(initialized) => initialized,
Err(error) => {
logging!(error, Type::Setup, "core manager initialization failed: {error:#}");
CoreManager::global().record_startup_error(CoreFailure::StartFailed(format!("{error:#}")));
Handle::notice(NoticeStatus::CoreStartError, "");
false
}
}
}
async fn refresh_tray_menu() {
logging_error!(Type::Setup, Tray::global().update_part().await);
}
async fn init_window() {
let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false);
WindowManager::create_window(!is_silent_start).await;
}
#[cfg(target_os = "macos")]
async fn resolve_dock_show() {
let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false);
if is_silent_start {
Handle::global().set_activation_policy_accessory();
}
}
fn resolve_done() {
RESOLVE_DONE.store(true, Ordering::Release);
}
pub(crate) fn is_resolve_done() -> bool {
RESOLVE_DONE.load(Ordering::Acquire)
}