mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-04 06:02: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.
651 lines
21 KiB
Rust
651 lines
21 KiB
Rust
use crate::{
|
|
config::{Config, IClashTemp, IProfiles, IVerge},
|
|
constants,
|
|
core::handle,
|
|
logging,
|
|
process::AsyncHandler,
|
|
utils::{
|
|
dirs::{self, PathBufExec as _},
|
|
help,
|
|
},
|
|
};
|
|
use anyhow::{Context as _, Result};
|
|
use chrono::{Local, TimeZone as _};
|
|
use clash_verge_logging::Type;
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
use std::path::Path;
|
|
use std::{path::PathBuf, str::FromStr as _};
|
|
use tauri_plugin_shell::ShellExt as _;
|
|
use tokio::fs;
|
|
use tokio::fs::DirEntry;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
async fn delete_snapshot_logs(log_dir: &Path) -> Result<()> {
|
|
let temp_dirs = [
|
|
log_dir.join("temp"),
|
|
log_dir.join("service").join("temp"),
|
|
log_dir.join("sidecar").join("temp"),
|
|
];
|
|
|
|
for temp_dir in temp_dirs.iter().filter(|d| d.exists()) {
|
|
let mut entries = fs::read_dir(temp_dir)
|
|
.await
|
|
.with_context(|| format!("failed to read log snapshot directory {}", temp_dir.display()))?;
|
|
while let Some(entry) = entries.next_entry().await.with_context(|| {
|
|
format!(
|
|
"failed to read next entry in log snapshot directory {}",
|
|
temp_dir.display()
|
|
)
|
|
})? {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|s| s.to_str()) == Some("log") {
|
|
let _ = path.remove_if_exists().await;
|
|
logging!(debug, Type::Setup, "delete snapshot log file: {}", path.display());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
|
|
pub async fn delete_log() -> Result<()> {
|
|
let log_dir = dirs::app_logs_dir()?;
|
|
let service_log_dir = dirs::service_log_dir()?;
|
|
|
|
if !log_dir.exists() && !service_log_dir.exists() {
|
|
return Ok(());
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
if log_dir.exists() {
|
|
delete_snapshot_logs(&log_dir).await?;
|
|
}
|
|
|
|
let auto_log_clean = {
|
|
let verge = Config::verge().await;
|
|
let verge = verge.data_arc();
|
|
verge.auto_log_clean.unwrap_or(0)
|
|
};
|
|
|
|
let day = match auto_log_clean {
|
|
1 => 1,
|
|
2 => 7,
|
|
3 => 30,
|
|
4 => 90,
|
|
_ => return Ok(()),
|
|
};
|
|
|
|
logging!(debug, Type::Setup, "try to delete log files, day: {}", day);
|
|
|
|
let parse_time_str = |s: &str| {
|
|
let sa: Vec<&str> = s.split('-').collect();
|
|
if sa.len() != 4 {
|
|
return Err(anyhow::anyhow!("invalid time str"));
|
|
}
|
|
|
|
let year = i32::from_str(sa[0])?;
|
|
let month = u32::from_str(sa[1])?;
|
|
let day = u32::from_str(sa[2])?;
|
|
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
|
.ok_or_else(|| anyhow::anyhow!("invalid time str"))?
|
|
.and_hms_opt(0, 0, 0)
|
|
.ok_or_else(|| anyhow::anyhow!("invalid time str"))?;
|
|
Ok(time)
|
|
};
|
|
|
|
let process_file = async move |file: DirEntry| -> Result<()> {
|
|
let file_name = file.file_name();
|
|
let file_name = file_name.to_str().unwrap_or_default();
|
|
|
|
if file_name.ends_with(".log") {
|
|
let now = Local::now();
|
|
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
|
let file_time = Local
|
|
.from_local_datetime(&created_time)
|
|
.single()
|
|
.ok_or_else(|| anyhow::anyhow!("invalid local datetime"))?;
|
|
|
|
let duration = now.signed_duration_since(file_time);
|
|
if duration.num_days() > day {
|
|
let _ = file.path().remove_if_exists().await;
|
|
logging!(debug, Type::Setup, "delete log file: {}", file_name);
|
|
}
|
|
}
|
|
Ok(())
|
|
};
|
|
|
|
if log_dir.exists() {
|
|
let mut log_read_dir = fs::read_dir(&log_dir)
|
|
.await
|
|
.with_context(|| format!("failed to read log directory {}", log_dir.display()))?;
|
|
while let Some(entry) = log_read_dir
|
|
.next_entry()
|
|
.await
|
|
.with_context(|| format!("failed to read next entry in log directory {}", log_dir.display()))?
|
|
{
|
|
std::mem::drop(process_file(entry).await);
|
|
}
|
|
}
|
|
|
|
if service_log_dir.exists() {
|
|
let mut service_log_read_dir = fs::read_dir(&service_log_dir)
|
|
.await
|
|
.with_context(|| format!("failed to read service log directory {}", service_log_dir.display()))?;
|
|
while let Some(entry) = service_log_read_dir.next_entry().await.with_context(|| {
|
|
format!(
|
|
"failed to read next entry in service log directory {}",
|
|
service_log_dir.display()
|
|
)
|
|
})? {
|
|
std::mem::drop(process_file(entry).await);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
async fn is_logs_dir_writable(log_dir: &Path) -> bool {
|
|
if !log_dir.is_dir() {
|
|
logging!(warn, Type::Setup, "macOS logs path is not a directory: {:?}", log_dir);
|
|
return false;
|
|
}
|
|
|
|
let probe_path = log_dir.join(format!(
|
|
".clash-verge-write-test-{}-{}",
|
|
std::process::id(),
|
|
Local::now().timestamp_nanos_opt().unwrap_or_default()
|
|
));
|
|
|
|
match fs::OpenOptions::new()
|
|
.create_new(true)
|
|
.write(true)
|
|
.open(&probe_path)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
if let Err(e) = fs::remove_file(&probe_path).await {
|
|
logging!(
|
|
warn,
|
|
Type::Setup,
|
|
"failed to remove macOS logs write probe {:?}: {}",
|
|
probe_path,
|
|
e
|
|
);
|
|
}
|
|
true
|
|
}
|
|
Err(e) => {
|
|
logging!(
|
|
warn,
|
|
Type::Setup,
|
|
"macOS logs directory is not writable {:?}: {}",
|
|
log_dir,
|
|
e
|
|
);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn available_legacy_path(parent: &Path, prefix: &str) -> Result<PathBuf> {
|
|
let timestamp = Local::now().format("%Y%m%d%H%M%S");
|
|
let base_name = format!("{prefix}-{timestamp}");
|
|
let candidate = parent.join(&base_name);
|
|
|
|
if !candidate.exists() {
|
|
return Ok(candidate);
|
|
}
|
|
|
|
for index in 1..100 {
|
|
let candidate = parent.join(format!("{base_name}-{index}"));
|
|
if !candidate.exists() {
|
|
return Ok(candidate);
|
|
}
|
|
}
|
|
|
|
Err(anyhow::anyhow!(
|
|
"failed to allocate legacy path under {:?} with prefix {}",
|
|
parent,
|
|
prefix
|
|
))
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
async fn migrate_legacy_macos_service_logs(log_dir: &Path) -> Result<()> {
|
|
let legacy_service_dir = log_dir.join("service");
|
|
if !legacy_service_dir.exists() {
|
|
return Ok(());
|
|
}
|
|
|
|
let service_logs_root = dirs::service_logs_root_dir()?;
|
|
fs::create_dir_all(&service_logs_root)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create service logs root {:?}: {}", service_logs_root, e))?;
|
|
|
|
let archived_service_dir = available_legacy_path(&service_logs_root, "service.legacy")?;
|
|
fs::rename(&legacy_service_dir, &archived_service_dir)
|
|
.await
|
|
.map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"Failed to archive legacy macOS service logs {:?} to {:?}: {}",
|
|
legacy_service_dir,
|
|
archived_service_dir,
|
|
e
|
|
)
|
|
})?;
|
|
|
|
logging!(
|
|
info,
|
|
Type::Setup,
|
|
"Archived legacy macOS service logs: {:?} -> {:?}",
|
|
legacy_service_dir,
|
|
archived_service_dir
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Once per install; the marker is written even when no interval needed raising.
|
|
pub async fn migrate_short_update_intervals() -> Result<()> {
|
|
let marker = dirs::update_interval_migrated_path()?;
|
|
if fs::try_exists(&marker).await? {
|
|
return Ok(());
|
|
}
|
|
|
|
// Same lock order as other writers; saving inside the closure keeps a racing restore intact.
|
|
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
|
|
|
let min = constants::profile::MIN_UPDATE_INTERVAL;
|
|
let raised = Config::profiles()
|
|
.await
|
|
.with_data_modify(|mut profiles: IProfiles| async move {
|
|
// A failed profiles load (bare default) must not burn the one-shot marker.
|
|
if profiles.items.is_none() {
|
|
anyhow::bail!("profiles.yaml was not loaded; refusing to record the migration as done");
|
|
}
|
|
let raised = profiles.raise_short_update_intervals(min);
|
|
if raised > 0 {
|
|
profiles.save_file().await?;
|
|
}
|
|
Ok((profiles, raised))
|
|
})
|
|
.await?;
|
|
|
|
if raised > 0 {
|
|
logging!(
|
|
info,
|
|
Type::Setup,
|
|
"已将 {} 个订阅的自动更新间隔提升到 {} 分钟",
|
|
raised,
|
|
min
|
|
);
|
|
}
|
|
|
|
fs::write(&marker, b"").await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
async fn migrate_legacy_macos_logs() -> Result<()> {
|
|
let log_dir = dirs::app_logs_dir()?;
|
|
|
|
if !log_dir.exists() {
|
|
return Ok(());
|
|
}
|
|
|
|
if is_logs_dir_writable(&log_dir).await {
|
|
if let Err(e) = migrate_legacy_macos_service_logs(&log_dir).await {
|
|
logging!(warn, Type::Setup, "Failed to migrate legacy macOS service logs: {e:#}");
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let app_home = dirs::app_home_dir()?;
|
|
let archived_log_dir = available_legacy_path(&app_home, "logs.legacy-root")?;
|
|
fs::rename(&log_dir, &archived_log_dir).await.map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"Failed to archive unwritable macOS logs directory {:?} to {:?}: {}",
|
|
log_dir,
|
|
archived_log_dir,
|
|
e
|
|
)
|
|
})?;
|
|
|
|
logging!(
|
|
warn,
|
|
Type::Setup,
|
|
"Archived unwritable macOS logs directory: {:?} -> {:?}",
|
|
log_dir,
|
|
archived_log_dir
|
|
);
|
|
|
|
fs::create_dir_all(&log_dir)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to recreate macOS logs directory {:?}: {}", log_dir, e))?;
|
|
logging!(info, Type::Setup, "Recreated macOS logs directory: {:?}", log_dir);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn init_dns_config() -> Result<()> {
|
|
use serde_yaml_ng::Value;
|
|
|
|
let dns_config = serde_yaml_ng::Mapping::from_iter([
|
|
("enable".into(), Value::Bool(true)),
|
|
// Must match the frontend default used for authoritative `dns.ipv6`.
|
|
("ipv6".into(), Value::Bool(true)),
|
|
("listen".into(), Value::String(":53".into())),
|
|
("enhanced-mode".into(), Value::String("fake-ip".into())),
|
|
("fake-ip-range".into(), Value::String("198.18.0.1/16".into())),
|
|
("fake-ip-range6".into(), Value::String("fdfe:dcba:9876::1/64".into())),
|
|
("fake-ip-filter-mode".into(), Value::String("blacklist".into())),
|
|
("prefer-h3".into(), Value::Bool(false)),
|
|
("respect-rules".into(), Value::Bool(false)),
|
|
("use-hosts".into(), Value::Bool(false)),
|
|
("use-system-hosts".into(), Value::Bool(false)),
|
|
(
|
|
"fake-ip-filter".into(),
|
|
Value::Sequence(vec![
|
|
Value::String("*.lan".into()),
|
|
Value::String("*.local".into()),
|
|
Value::String("*.arpa".into()),
|
|
Value::String("time.*.com".into()),
|
|
Value::String("ntp.*.com".into()),
|
|
Value::String("+.market.xiaomi.com".into()),
|
|
Value::String("localhost.ptlogin2.qq.com".into()),
|
|
Value::String("*.msftncsi.com".into()),
|
|
Value::String("www.msftconnecttest.com".into()),
|
|
]),
|
|
),
|
|
(
|
|
"default-nameserver".into(),
|
|
Value::Sequence(vec![
|
|
Value::String("system".into()),
|
|
Value::String("223.6.6.6".into()),
|
|
Value::String("8.8.8.8".into()),
|
|
Value::String("2400:3200::1".into()),
|
|
Value::String("2001:4860:4860::8888".into()),
|
|
]),
|
|
),
|
|
(
|
|
"nameserver".into(),
|
|
Value::Sequence(vec![
|
|
Value::String("8.8.8.8".into()),
|
|
Value::String("https://doh.pub/dns-query".into()),
|
|
Value::String("https://dns.alidns.com/dns-query".into()),
|
|
]),
|
|
),
|
|
("fallback".into(), Value::Sequence(vec![])),
|
|
(
|
|
"nameserver-policy".into(),
|
|
Value::Mapping(serde_yaml_ng::Mapping::new()),
|
|
),
|
|
("proxy-server-nameserver".into(), Value::Sequence(vec![])),
|
|
("direct-nameserver".into(), Value::Sequence(vec![])),
|
|
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
|
|
(
|
|
"fallback-filter".into(),
|
|
Value::Mapping(serde_yaml_ng::Mapping::from_iter([
|
|
("geoip".into(), Value::Bool(true)),
|
|
("geoip-code".into(), Value::String("CN".into())),
|
|
(
|
|
"ipcidr".into(),
|
|
Value::Sequence(vec![
|
|
Value::String("240.0.0.0/4".into()),
|
|
Value::String("0.0.0.0/32".into()),
|
|
]),
|
|
),
|
|
(
|
|
"domain".into(),
|
|
Value::Sequence(vec![
|
|
Value::String("+.google.com".into()),
|
|
Value::String("+.facebook.com".into()),
|
|
Value::String("+.youtube.com".into()),
|
|
]),
|
|
),
|
|
])),
|
|
),
|
|
]);
|
|
|
|
let default_dns_config = serde_yaml_ng::Mapping::from_iter([
|
|
("dns".into(), Value::Mapping(dns_config)),
|
|
("hosts".into(), Value::Mapping(serde_yaml_ng::Mapping::new())),
|
|
]);
|
|
|
|
let app_dir = dirs::app_home_dir()?;
|
|
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
|
|
|
if !dns_path.exists() {
|
|
logging!(info, Type::Setup, "Creating default DNS config file");
|
|
help::save_yaml(&dns_path, &default_dns_config, Some("# Clash Verge DNS Config")).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ensure_directories() -> Result<()> {
|
|
let directories = [
|
|
("app_home", dirs::app_home_dir()?),
|
|
("app_profiles", dirs::app_profiles_dir()?),
|
|
("app_logs", dirs::app_logs_dir()?),
|
|
("service_logs", dirs::service_log_dir()?),
|
|
];
|
|
|
|
for (name, dir) in directories {
|
|
if !dir.exists() {
|
|
fs::create_dir_all(&dir)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e))?;
|
|
logging!(info, Type::Setup, "Created {} directory: {:?}", name, dir);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn initialize_config_files() -> Result<()> {
|
|
if let Ok(path) = dirs::clash_path()
|
|
&& !path.exists()
|
|
{
|
|
let template = IClashTemp::template().0;
|
|
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
|
|
logging!(info, Type::Setup, "Created clash config at {:?}", path);
|
|
}
|
|
|
|
if let Ok(path) = dirs::verge_path()
|
|
&& !path.exists()
|
|
{
|
|
let template = IVerge::template();
|
|
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
|
|
logging!(info, Type::Setup, "Created verge config at {:?}", path);
|
|
}
|
|
|
|
if let Ok(path) = dirs::profiles_path()
|
|
&& !path.exists()
|
|
{
|
|
let template = IProfiles::default();
|
|
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
|
|
logging!(info, Type::Setup, "Created profiles config at {:?}", path);
|
|
}
|
|
|
|
IVerge::validate_and_fix_config()
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to validate verge config: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Initializes configuration required before Tauri setup.
|
|
pub async fn init_config() -> Result<()> {
|
|
#[cfg(target_os = "macos")]
|
|
migrate_legacy_macos_logs().await?;
|
|
|
|
ensure_directories().await?;
|
|
|
|
initialize_config_files().await?;
|
|
|
|
AsyncHandler::spawn(|| async {
|
|
if let Err(e) = delete_log().await {
|
|
logging!(warn, Type::Setup, "Failed to clean old logs: {e:#}");
|
|
}
|
|
logging!(debug, Type::Setup, "后台日志清理任务完成");
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Initializes application resources after Tauri setup.
|
|
pub async fn init_resources() -> Result<()> {
|
|
let app_dir = dirs::app_home_dir()?;
|
|
let res_dir = dirs::app_resources_dir()?;
|
|
|
|
// app_home is created by `ensure_directories` in `init_config`, which always runs first.
|
|
if !res_dir.exists() {
|
|
fs::create_dir_all(&res_dir).await?;
|
|
}
|
|
|
|
let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
|
|
|
|
for file in file_list.iter() {
|
|
let src_path = res_dir.join(file);
|
|
let dest_path = app_dir.join(file);
|
|
|
|
if src_path.exists() && !dest_path.exists() {
|
|
handle_copy(&src_path, &dest_path, file).await;
|
|
continue;
|
|
}
|
|
|
|
let src_modified = fs::metadata(&src_path).await.and_then(|m| m.modified());
|
|
let dest_modified = fs::metadata(&dest_path).await.and_then(|m| m.modified());
|
|
|
|
match (src_modified, dest_modified) {
|
|
(Ok(src_modified), Ok(dest_modified)) => {
|
|
if src_modified > dest_modified {
|
|
handle_copy(&src_path, &dest_path, file).await;
|
|
}
|
|
}
|
|
_ => {
|
|
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
|
|
handle_copy(&src_path, &dest_path, file).await;
|
|
}
|
|
};
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub fn init_scheme() -> Result<()> {
|
|
use tauri::utils::platform::current_exe;
|
|
use winreg::{RegKey, enums::HKEY_CURRENT_USER};
|
|
|
|
let app_exe = current_exe()?;
|
|
let app_exe = dunce::canonicalize(app_exe)?;
|
|
let app_exe = app_exe.to_string_lossy().into_owned();
|
|
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
|
|
clash.set_value("", &"Clash Verge")?;
|
|
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
|
|
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
|
|
default_icon.set_value("", &app_exe)?;
|
|
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
|
|
command.set_value("", &format!("{app_exe} \"%1\""))?;
|
|
|
|
Ok(())
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
pub fn init_scheme() -> Result<()> {
|
|
const DESKTOP_FILE: &str = "clash-verge.desktop";
|
|
|
|
for scheme in DEEP_LINK_SCHEMES {
|
|
let handler = format!("x-scheme-handler/{scheme}");
|
|
let output = std::process::Command::new("xdg-mime")
|
|
.arg("default")
|
|
.arg(DESKTOP_FILE)
|
|
.arg(&handler)
|
|
.output()?;
|
|
if !output.status.success() {
|
|
return Err(anyhow::anyhow!(
|
|
"failed to set {handler}, {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
));
|
|
}
|
|
}
|
|
|
|
crate::utils::linux::mime::ensure_mimeapps_entries(DESKTOP_FILE, DEEP_LINK_SCHEMES)?;
|
|
Ok(())
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
pub const fn init_scheme() -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
const DEEP_LINK_SCHEMES: &[&str] = &["clash", "clash-verge"];
|
|
|
|
pub async fn startup_script() -> Result<()> {
|
|
let app_handle = handle::Handle::app_handle();
|
|
let script_path = {
|
|
let verge = Config::verge().await;
|
|
let verge = verge.data_arc();
|
|
verge.startup_script.clone().unwrap_or_else(|| "".into())
|
|
};
|
|
|
|
if script_path.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let shell_type = if script_path.ends_with(".sh") {
|
|
"bash"
|
|
} else if script_path.ends_with(".ps1") || script_path.ends_with(".bat") {
|
|
"powershell"
|
|
} else {
|
|
return Err(anyhow::anyhow!("unsupported script extension: {}", script_path));
|
|
};
|
|
|
|
let script_dir = PathBuf::from(script_path.as_str());
|
|
if !script_dir.exists() {
|
|
return Err(anyhow::anyhow!("script not found: {}", script_path));
|
|
}
|
|
|
|
let parent_dir = script_dir.parent();
|
|
let working_dir = parent_dir.unwrap_or_else(|| script_dir.as_ref());
|
|
|
|
app_handle
|
|
.shell()
|
|
.command(shell_type)
|
|
.current_dir(working_dir)
|
|
.args([script_path.as_str()])
|
|
.output()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) {
|
|
match fs::copy(src, dest).await {
|
|
Ok(_) => {
|
|
logging!(debug, Type::Setup, "resources copied '{}'", file);
|
|
}
|
|
Err(err) => {
|
|
logging!(
|
|
error,
|
|
Type::Setup,
|
|
"failed to copy resource {} to {}: {err}",
|
|
src.display(),
|
|
dest.display()
|
|
);
|
|
}
|
|
};
|
|
}
|