mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-24 04:32:07 +08:00
* refactor(log): migrate logging to tracing-estuary pipeline Swap flexi_logger + clash-verge-logger wiring for the tracing-estuary crate: buffered file sink with explicit panic/exit drains, live level switching, and tokio-trace/chrome-trace behind crate features. AsyncLogger moves in-house (VecDeque ring); file format and rotation stay unchanged. * fix(log): reset sidecar writer in place and re-pin service-ipc v2.6.7 Rebuilding the sidecar FileLogWriter on config changes leaked flexi's unstoppable flusher thread plus an fd each time; the writer is now flushed and reset in place, matching what the service crate does. get_clash_logs_by_service drops the CompactString conversion layer now that service-ipc v2.6.7 returns plain Vec<String>, and AsyncLogger is renamed LogRing to match. * refactor(log): span-based observability and log noise cleanup Instrument spans carry the structure flat lines never could: the core-start spine (mode, readiness/owner generations, pid, config file), proxy apply and clear routes with guard generations, service IPC sessions with refusal codes and outcomes, profile-switch terminal outcomes, subscription fetch strategies, core upgrades with per-proxy version probes, backup flows, timer tasks, config generation uids, and the startup setup flow; running-mode changes emit one guarded transition event each. ~90 hand-written noise lines are gone or demoted (entry+success pairs, polled getter echoes, phase brackets, window sub-steps, platform install chatter); errors keep their anyhow chain, operation name, and item id, and swallowed retry results surface per attempt.
648 lines
21 KiB
Rust
648 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::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?;
|
|
while let Some(entry) = entries.next_entry().await? {
|
|
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?;
|
|
while let Some(entry) = log_read_dir.next_entry().await? {
|
|
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?;
|
|
while let Some(entry) = service_log_read_dir.next_entry().await? {
|
|
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(())
|
|
}
|
|
|
|
/// Raise existing sub-floor update intervals, once per install.
|
|
///
|
|
/// One-off repair, not a standing rule — the UI warns about shorter intervals but still saves
|
|
/// them, so the marker is written even when nothing 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 order as every other writer holding it (lock, then draft permit): save_file runs
|
|
// inside the closure, ahead of the optimistic check, so a racing restore can't be undone.
|
|
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 {
|
|
// `IProfiles::new` normalises items to Some when it read the file, and returns a
|
|
// bare default() when it failed. A failed load also raises zero — without this the
|
|
// marker would burn the one shot on profiles nobody managed to load.
|
|
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("time.*.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![
|
|
Value::String("https://doh.pub/dns-query".into()),
|
|
Value::String("https://dns.alidns.com/dns-query".into()),
|
|
Value::String("tls://223.5.5.5".into()),
|
|
]),
|
|
),
|
|
("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()?;
|
|
|
|
if !app_dir.exists() {
|
|
std::mem::drop(fs::create_dir_all(&app_dir).await);
|
|
}
|
|
if !res_dir.exists() {
|
|
std::mem::drop(fs::create_dir_all(&res_dir).await);
|
|
}
|
|
|
|
let file_list = ["Country.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 resources '{}' to '{:?}', {}",
|
|
file,
|
|
dest,
|
|
err
|
|
);
|
|
}
|
|
};
|
|
}
|