Files
clash-verge-rev/src-tauri/src/core/lightweight.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

267 lines
7.8 KiB
Rust

use crate::{
config::Config,
core::{timer::Timer, tray::Tray},
process::AsyncHandler,
};
use clash_verge_logging::{Type, logging};
use crate::utils::window_manager::WindowManager;
use anyhow::Result;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use tauri::Listener as _;
use tokio::sync::oneshot;
use tokio::time::{Duration, sleep};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LightweightState {
Normal = 0,
In = 1,
Exiting = 2,
}
impl From<u8> for LightweightState {
fn from(v: u8) -> Self {
match v {
1 => Self::In,
2 => Self::Exiting,
_ => Self::Normal,
}
}
}
impl LightweightState {
const fn as_u8(self) -> u8 {
self as u8
}
}
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
struct WindowListener {
handler_id: AtomicU32,
event: &'static str,
label: &'static str,
}
impl WindowListener {
fn setup(&self, on_event: impl Fn() + Send + 'static) {
if let Some(window) = WindowManager::get_main_window() {
let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel);
if previous_handler_id != 0 {
window.unlisten(previous_handler_id);
logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label);
}
let handler_id = window.listen(self.event, move |_event| on_event());
self.handler_id.store(handler_id, Ordering::Release);
}
}
fn cancel(&self) {
let id = self.handler_id.swap(0, Ordering::AcqRel);
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了{}监听", self.label);
}
}
}
static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener {
handler_id: AtomicU32::new(0),
event: "tauri://close-requested",
label: "窗口关闭",
};
static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener {
handler_id: AtomicU32::new(0),
event: "tauri://focus",
label: "窗口焦点",
};
static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
#[inline]
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
#[inline]
fn try_transition(from: LightweightState, to: LightweightState) -> bool {
LIGHTWEIGHT_STATE
.compare_exchange(from.as_u8(), to.as_u8(), Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
}
#[inline]
fn record_state_and_log(state: LightweightState) {
LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release);
match state {
LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"),
LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"),
LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"),
}
}
#[inline]
pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_menu().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
}
}
pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await;
let is_enable_auto = verge_config.data_arc().enable_auto_light_weight_mode.unwrap_or(false);
let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
if is_enable_auto {
enable_auto_light_weight_mode().await;
}
if is_silent_start {
entry_lightweight_mode().await;
}
Ok(())
}
pub async fn enable_auto_light_weight_mode() {
if let Err(e) = Timer::global().init().await {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
return;
}
logging!(info, Type::Lightweight, "开启自动轻量模式");
setup_window_close_listener();
setup_webview_focus_listener();
}
pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, "关闭自动轻量模式");
cancel_light_weight_timer();
cancel_window_close_listener();
cancel_webview_focus_listener();
}
pub async fn entry_lightweight_mode() -> bool {
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::In);
WindowManager::destroy_main_window();
cancel_light_weight_timer();
refresh_lightweight_tray_state().await;
true
}
pub async fn exit_lightweight_mode() -> bool {
if !try_transition(LightweightState::In, LightweightState::Exiting) {
logging!(
debug,
Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::Exiting);
WindowManager::show_main_window().await;
let enable_auto_light_weight_mode = Config::verge()
.await
.data_arc()
.enable_auto_light_weight_mode
.unwrap_or(false);
if enable_auto_light_weight_mode {
setup_window_close_listener();
setup_webview_focus_listener();
}
cancel_light_weight_timer();
record_state_and_log(LightweightState::Normal);
refresh_lightweight_tray_state().await;
true
}
#[cfg(target_os = "macos")]
pub async fn add_light_weight_timer() {
setup_light_weight_timer().await;
}
fn setup_window_close_listener() {
WINDOW_CLOSE_LISTENER.setup(|| {
std::mem::drop(AsyncHandler::spawn(|| async {
setup_light_weight_timer().await;
}));
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
}
fn cancel_window_close_listener() {
WINDOW_CLOSE_LISTENER.cancel();
}
fn setup_webview_focus_listener() {
WEBVIEW_FOCUS_LISTENER.setup(|| {
cancel_light_weight_timer();
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
});
}
fn cancel_webview_focus_listener() {
WEBVIEW_FOCUS_LISTENER.cancel();
}
async fn setup_light_weight_timer() {
let once_by_minutes = Config::verge().await.data_arc().auto_light_weight_minutes.unwrap_or(10);
let mut cancel_tx_guard = CANCEL_TX.lock();
if cancel_tx_guard.is_some() {
logging!(
debug,
Type::Timer,
"Lightweight mode timer already exists, skipping setup"
);
return;
}
let (tx, rx) = oneshot::channel::<()>();
*cancel_tx_guard = Some(tx);
drop(cancel_tx_guard);
AsyncHandler::spawn(move || async move {
tokio::select! {
_ = sleep(Duration::from_secs(once_by_minutes.saturating_mul(60))) => {
logging!(info, Type::Timer, "Lightweight mode timer expired, entering lightweight mode");
{
let mut guard = CANCEL_TX.lock();
*guard = None;
}
entry_lightweight_mode().await;
}
_ = rx => {
logging!(debug, Type::Timer, "Received cancel signal, stopping lightweight mode timer");
}
}
});
logging!(
info,
Type::Timer,
"Lightweight mode timer set, entering lightweight mode in {} minutes",
once_by_minutes
);
}
fn cancel_light_weight_timer() {
let mut cancel_tx_guard = CANCEL_TX.lock();
if let Some(tx) = cancel_tx_guard.take() {
let _ = tx.send(());
logging!(debug, Type::Timer, "Timer cancelled");
}
}