mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-04 14:22:11 +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.
267 lines
7.8 KiB
Rust
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");
|
|
}
|
|
}
|