Files
clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs
T
Tunglies 2f370d6490 refactor(log): in-place writer lifecycle and span-based core observability (#7889)
* 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.
2026-09-08 05:51:04 +08:00

160 lines
5.1 KiB
Rust

use anyhow::Result;
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use crate::{
config::{Config, PrfItem, profiles},
core::{CoreManager, handle, timer::Timer},
utils::help,
};
use clash_verge_logging::{Type, logging, logging_error};
pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param
};
let masked_deep_link = help::mask_url(param_str);
logging!(debug, Type::Config, "received deep link: {masked_deep_link}");
let link_parsed = Url::parse(param_str)
.map_err(|e| anyhow::anyhow!("failed to parse deep link: {e:?}, param: {masked_deep_link}"))?;
let Some((url, name)) = extract_subscription_info(&link_parsed) else {
logging!(
warn,
Type::Config,
"missing url parameter in deep link: {masked_deep_link}"
);
return Ok(());
};
import_subscription(&url, name.as_ref()).await;
Ok(())
}
fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option<String>)> {
if !matches!(link_parsed.scheme(), "clash" | "clash-verge") {
return None;
}
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned().into());
let url = extract_subscription_url(link_parsed)?;
Some((url, name))
}
fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
let query = link_parsed.query()?;
let prefix = "url=";
let pos = query.find(prefix)?;
let raw_url = query[pos + prefix.len()..].trim();
Some(decode_subscription_url(raw_url))
}
fn decode_subscription_url(raw_url: &str) -> std::string::String {
// Avoid double-decoding nested subscription URLs; decode only when needed.
if Url::parse(raw_url).is_ok() {
return raw_url.to_string();
}
let mut candidate = raw_url.to_string();
for _ in 0..2 {
let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string();
if next == candidate {
break;
}
candidate = next;
if Url::parse(&candidate).is_ok() {
break;
}
}
candidate
}
async fn import_subscription(url: &str, name: Option<&String>) {
let had_current_profile = {
let profiles = Config::profiles().await;
profiles.latest_arc().current.is_some()
};
let Some(mut item) = fetch_profile_item(url, name).await else {
return;
};
let uid = item.uid.clone().unwrap_or_default();
if let Err(e) = profiles::profiles_append_item_safe(&mut item).await {
logging!(error, Type::Config, "failed to import subscription url: {:?}", e);
Config::profiles().await.discard();
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return;
}
if let Err(e) = profiles::profiles_save_file_safe().await {
logging!(error, Type::Config, "failed to save imported subscription: {e:#}");
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return;
}
logging_error!(Type::Timer, Timer::global().refresh().await);
handle::Handle::notice_message(
"import_sub_url::ok",
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
);
post_import_updates(&uid, had_current_profile).await;
}
async fn fetch_profile_item(url: &str, name: Option<&String>) -> Option<PrfItem> {
match PrfItem::from_url(url, name, None, None).await {
Ok(item) => Some(item),
Err(e) => {
logging!(error, Type::Config, "failed to parse profile from url: {:?}", e);
handle::Handle::notice_message("import_sub_url::error", e.to_string());
None
}
}
}
async fn post_import_updates(uid: &String, had_current_profile: bool) {
handle::Handle::refresh_verge();
handle::Handle::notify_profile_changed(uid);
let should_update_core = if uid.is_empty() || had_current_profile {
false
} else {
let profiles = Config::profiles().await;
profiles.latest_arc().current.as_ref() == Some(uid)
};
if should_update_core {
refresh_core_config().await;
}
}
async fn refresh_core_config() {
logging!(
info,
Type::Config,
"Deep link import set current profile; refreshing core config"
);
match CoreManager::global().update_config_forced().await {
Ok(outcome) if outcome.is_valid() => handle::Handle::refresh_clash(),
Ok(outcome) => {
let message = outcome.to_string();
logging!(warn, Type::Config, "Apply config failed: {}", message);
handle::Handle::notice_message("config_validate::error", message);
}
Err(err) => {
logging!(error, Type::Config, "Apply config error: {err:#}");
handle::Handle::notice_message("update_failed", format!("{err}"));
}
}
}