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.
This commit is contained in:
Tunglies
2026-09-08 05:51:04 +08:00
committed by GitHub
parent f8b69b5d62
commit 2f370d6490
43 changed files with 807 additions and 797 deletions
+37 -17
View File
@@ -69,7 +69,7 @@ async fn finalize_restored_verge_config(
}
.await;
if let Err(err) = result {
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#?}");
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
if SystemProxyStateUnknown::is_in(&err) {
return Err(err);
}
@@ -79,7 +79,7 @@ async fn finalize_restored_verge_config(
// Run configuration side effects without rewriting the already-restored file.
if let Err(err) = super::patch_verge(&restored, true).await {
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#?}");
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
// Propagate unknown proxy state; ordinary side-effect failures stay logged.
if SystemProxyStateUnknown::is_in(&err) {
return Err(err);
@@ -88,9 +88,10 @@ async fn finalize_restored_verge_config(
Ok(())
}
#[tracing::instrument(skip_all, level = "info")]
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
logging!(error, Type::Backup, "Failed to create backup: {err:#?}");
logging!(error, Type::Backup, "Failed to create backup: {err:#}");
err
})?;
@@ -98,13 +99,13 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
.upload(temp_file_path.clone(), file_name)
.await
{
logging!(error, Type::Backup, "Failed to upload to WebDAV: {err:#?}");
logging!(error, Type::Backup, "Failed to upload to WebDAV: {err:#}");
backup::WebDavClient::global().reset();
return Err(err);
}
if let Err(err) = temp_file_path.remove_if_exists().await {
logging!(warn, Type::Backup, "Failed to remove temp file: {err:#?}");
logging!(warn, Type::Backup, "Failed to remove temp file: {err:#}");
}
Ok(())
@@ -112,18 +113,24 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global().list().await.map_err(|err| {
logging!(error, Type::Backup, "Failed to list WebDAV backup files: {err:#?}");
logging!(error, Type::Backup, "Failed to list WebDAV backup files: {err:#}");
err
})
}
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
let name = filename.to_string();
backup::WebDavClient::global().delete(filename).await.map_err(|err| {
logging!(error, Type::Backup, "Failed to delete WebDAV backup file: {err:#?}");
logging!(
error,
Type::Backup,
"Failed to delete WebDAV backup file {name}: {err:#}"
);
err
})
}
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge().await;
let verge_data = verge.latest_arc();
@@ -134,11 +141,16 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let backup_storage_path = app_home_dir()
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
.join(filename.as_str());
let name = filename.to_string();
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
.await
.map_err(|err| {
logging!(error, Type::Backup, "Failed to download WebDAV backup file: {err:#?}");
logging!(
error,
Type::Backup,
"Failed to download WebDAV backup file {name}: {err:#}"
);
err
})?;
@@ -163,7 +175,7 @@ where
F: FnOnce(&str) -> String,
{
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
logging!(error, Type::Backup, "Failed to create local backup: {err:#?}");
logging!(error, Type::Backup, "Failed to create local backup: {err:#}");
err
})?;
@@ -172,12 +184,17 @@ where
let target_path = backup_dir.join(final_name.as_str());
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
logging!(error, Type::Backup, "Failed to move local backup file: {err:#?}");
logging!(
error,
Type::Backup,
"Failed to move local backup file to {}: {err:#}",
target_path.display()
);
if let Err(clean_err) = temp_file_path.remove_if_exists().await {
logging!(
warn,
Type::Backup,
"Failed to remove temp backup file after move error: {clean_err:#?}"
"Failed to remove temp backup file after move error: {clean_err:#}"
);
}
return Err(err);
@@ -186,6 +203,7 @@ where
Ok(final_name)
}
#[tracing::instrument(skip_all, level = "info", fields(source = %source))]
pub async fn import_local_backup(source: String) -> Result<String> {
let source_path = PathBuf::from(source.as_str());
if !source_path.exists() {
@@ -226,7 +244,7 @@ pub async fn import_local_backup(source: String) -> Result<String> {
fs::copy(&source_path, &target_path)
.await
.map_err(|err| anyhow!("Failed to import backup file: {err:#?}"))?;
.map_err(|err| anyhow!("Failed to import backup file: {err:#}"))?;
Ok(file_name.to_string().into())
}
@@ -243,14 +261,14 @@ async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
logging!(
warn,
Type::Backup,
"Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}"
"Failed to rename backup file directly, fallback to copy/remove: {rename_err}"
);
fs::copy(&from, &to)
.await
.map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
.map_err(|err| anyhow!("Failed to copy backup file: {err:#}"))?;
fs::remove_file(&from)
.await
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?;
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#}"))?;
Ok(())
}
}
@@ -295,13 +313,14 @@ pub async fn delete_local_backup(filename: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let target_path = backup_dir.join(filename.as_str());
if !target_path.exists() {
logging!(warn, Type::Backup, "Local backup file not found: {}", filename);
logging!(debug, Type::Backup, "Local backup file not found: {}", filename);
return Ok(());
}
target_path.remove_if_exists().await?;
Ok(())
}
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
pub async fn restore_local_backup(filename: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let target_path = backup_dir.join(filename.as_str());
@@ -327,6 +346,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
Ok(())
}
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let source_path = backup_dir.join(filename.as_str());
@@ -342,6 +362,6 @@ pub async fn export_local_backup(filename: String, destination: String) -> Resul
fs::copy(&source_path, &dest_path)
.await
.map(|_| ())
.map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?;
.map_err(|err| anyhow!("Failed to export backup file: {err:#}"))?;
Ok(())
}
+3 -4
View File
@@ -31,7 +31,7 @@ pub async fn restart_clash_core() {
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err:#}"));
logging!(error, Type::Core, "{err:#}");
logging!(error, Type::Core, "restart core failed: {err:#}");
}
}
}
@@ -42,7 +42,6 @@ pub async fn restart_app() {
Config::apply_all_and_save_file().await;
logging!(info, Type::System, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
@@ -85,9 +84,8 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
let json_value = serde_json::json!({
"mode": mode
});
logging!(debug, Type::Core, "change clash mode to {mode}");
if let Err(err) = handle::Handle::mihomo().patch_base_config(&json_value).await {
logging!(error, Type::Core, "{err}");
logging!(error, Type::Core, "change clash mode failed: {err}");
return Err(err.to_string().into());
}
@@ -111,6 +109,7 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
/// Test delay to a URL through proxy.
/// HTTPS: measures TLS handshake time. HTTP: measures HEAD round-trip time.
#[tracing::instrument(skip_all, level = "trace", fields(url = %url))]
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
use std::sync::Arc;
use std::time::Duration;
+3 -4
View File
@@ -1,6 +1,6 @@
use crate::{
config::{Config, IVerge},
core::{CoreManager, autostart, handle, hotkey, logger::Logger, proxy_control, tray},
core::{CoreManager, autostart, handle, hotkey, logger, proxy_control, tray},
module::{auto_backup::AutoBackupManager, lightweight},
};
use anyhow::Result;
@@ -267,12 +267,12 @@ async fn process_terminated_flags(update_flags: UpdateFlags, patch: &IVerge) ->
}
}
if update_flags.contains(UpdateFlags::LOG_LEVEL) {
Logger::global().update_log_level(patch.get_log_level())?;
logger::Logger::global().update_log_level(patch.get_log_level())?;
}
if update_flags.contains(UpdateFlags::LOG_FILE) {
let log_max_size = patch.app_log_max_size.unwrap_or(128);
let log_max_count = patch.app_log_max_count.unwrap_or(8);
Logger::global().update_log_config(log_max_size, log_max_count).await?;
logger::update_log_config(log_max_size, log_max_count).await?;
}
Ok(())
}
@@ -317,7 +317,6 @@ pub(super) async fn apply_verge_patch_locked(
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = verge.data_arc();
logging!(debug, Type::Setup, "Saving Verge configuration to file...");
verge_data.save_file().await?;
}
Ok(())
+28 -8
View File
@@ -45,6 +45,7 @@ pub struct CoreUpgradeReport {
pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
let _serialized = UPGRADE_LOCK.lock().await;
let core = Config::verge().await.latest_arc().get_valid_clash_core();
tracing::Span::current().record("core", tracing::field::display(&core));
let alpha = core.ends_with("-alpha");
let target = managed_core_path(&core)?;
@@ -55,11 +56,10 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
});
let (proxy, latest) = resolve_latest_version(alpha).await?;
logging!(
info,
Type::Core,
"core upgrade: {core} installed={installed:?} latest={latest:?} via {proxy:?}"
);
let span = tracing::Span::current();
span.record("from", tracing::field::display(&installed));
span.record("to", tracing::field::display(&latest));
logging!(debug, Type::Core, "core upgrade: latest version resolved via {proxy:?}");
if !force && installed == latest {
return Ok(CoreUpgradeReport {
@@ -111,7 +111,6 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
#[cfg(windows)]
let _ = std::fs::remove_file(target.with_extension("old"));
logging!(info, Type::Core, "core upgrade: {core} now at {latest}");
Ok(CoreUpgradeReport {
upgraded: true,
from: installed,
@@ -175,6 +174,7 @@ fn asset_base_name(alpha: bool) -> Result<&'static str> {
}
/// Returns the proxy that reached GitHub so the package download reuses it.
#[tracing::instrument(skip_all, level = "debug", fields(alpha))]
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::String)> {
let url = if alpha {
format!("{ALPHA_BASE_URL}/version.txt")
@@ -193,13 +193,33 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::
// An error page answered with 200 must not become a version, nor reach the URL.
let version = response.text().trim().to_owned();
if !is_usable_version(&version) {
logging!(
warn,
Type::Core,
"core upgrade: {url} returned an unusable version: {version:?}"
);
last_error = Some(anyhow!("{url} returned an unusable version"));
continue;
}
return Ok((proxy, version));
}
Ok(response) => last_error = Some(anyhow!("{url} returned status {}", response.status())),
Err(error) => last_error = Some(error.context(format!("{proxy:?} could not reach {url}"))),
Ok(response) => {
logging!(
debug,
Type::Core,
"core upgrade: version probe via {proxy:?}: status {}",
response.status()
);
last_error = Some(anyhow!("{url} returned status {}", response.status()));
}
Err(error) => {
logging!(
debug,
Type::Core,
"core upgrade: version probe via {proxy:?} failed: {error:#}"
);
last_error = Some(error.context(format!("{proxy:?} could not reach {url}")));
}
}
}
+1 -7
View File
@@ -157,13 +157,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
previous_ico.remove_if_exists().await.unwrap_or_default();
}
logging!(
info,
Type::Cmd,
"Copying icon file path: {:?} -> file dist: {:?}",
path,
dest_path
);
logging!(debug, Type::Cmd, "copying icon file: {path} -> {}", dest_path.display());
match fs::copy(file_path, &dest_path).await {
Ok(_) => Ok(dest_path.to_string_lossy().into()),
+24 -38
View File
@@ -5,15 +5,18 @@ use crate::{
utils::help::{mask_err, mask_url},
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
logging_error!(
Type::Config,
cmd::patch_profiles_config_by_profile_index(profile_index).await
);
if let Err(err) = cmd::patch_profiles_config_by_profile_index(profile_index.clone()).await {
logging!(
error,
Type::Config,
"toggle proxy profile {profile_index} failed: {err}"
);
}
}
/// Tell the profile which node this group is on now.
@@ -25,7 +28,7 @@ async fn record_switched_node(group_name: &str, proxy_name: &str) {
if let Err(error) = crate::config::profiles::record_selected_node(group_name, proxy_name).await {
logging!(
warn,
Type::Tray,
Type::Config,
"切换代理成功但未能记录到配置: {} -> {}, 错误: {error:#}",
group_name,
proxy_name
@@ -39,21 +42,13 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
.await
{
Ok(_) => {
logging!(info, Type::Tray, "切换代理成功: {} -> {}", group_name, proxy_name);
record_switched_node(group_name, proxy_name).await;
handle::Handle::refresh_proxy_config();
let _ = tray::Tray::global().update_menu().await;
return;
}
Err(err) => {
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
logging!(error, Type::Tray, "切换代理失败: {err:?}");
}
}
@@ -67,14 +62,7 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(
error,
Type::Tray,
"代理切换最终失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
logging!(error, Type::Tray, "代理切换最终失败: {err:?}");
}
}
}
@@ -86,20 +74,19 @@ async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
logging!(info, Type::Config, "[订阅更新] {uid} 不是远程订阅,跳过更新");
logging!(info, Type::Config, "[订阅更新] 不是远程订阅,跳过更新");
Ok(None)
} else if item.url.is_none() {
logging!(warn, Type::Config, "Warning: [订阅更新] {uid} 缺少URL,无法更新");
logging!(warn, Type::Config, "[订阅更新] 缺少URL,无法更新");
bail!("failed to get the profile item url");
} else if !ignore_auto_update && !item.option.as_ref().and_then(|o| o.allow_auto_update).unwrap_or(true) {
logging!(info, Type::Config, "[订阅更新] {} 禁止自动更新,跳过更新", uid);
logging!(info, Type::Config, "[订阅更新] 禁止自动更新,跳过更新");
Ok(None)
} else {
logging!(
info,
Type::Config,
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
"[订阅更新] 远程订阅,URL: {}",
mask_url(
item.url
.as_ref()
@@ -113,6 +100,7 @@ async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result
}
}
#[tracing::instrument(skip_all, level = "info", fields(uid = %uid, strategy = tracing::field::Empty))]
async fn perform_profile_update(
uid: &String,
url: &String,
@@ -120,7 +108,6 @@ async fn perform_profile_update(
option: Option<&PrfOption>,
is_mannual_trigger: bool,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
let mut merged_opt = PrfOption::merge(opt, option);
let profiles = Config::profiles().await;
let profiles_arc = profiles.latest_arc();
@@ -140,8 +127,8 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {},尝试使用Clash代理更新",
mask_err(&err.to_string())
"[订阅更新] 直接更新失败: {},尝试使用Clash代理更新",
mask_err(&format!("{err:#}"))
);
}
}
@@ -160,8 +147,8 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] Clash代理更新失败: {},尝试使用系统代理更新",
mask_err(&err.to_string())
"[订阅更新] Clash代理更新失败: {},尝试使用系统代理更新",
mask_err(&format!("{err:#}"))
);
}
}
@@ -180,8 +167,8 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 系统代理更新失败: {},所有重试均已失败",
mask_err(&err.to_string())
"[订阅更新] 系统代理更新失败: {},所有重试均已失败",
mask_err(&format!("{err:#}"))
);
err
}
@@ -194,8 +181,8 @@ async fn perform_profile_update(
bail!(last_err)
}
#[tracing::instrument(skip_all, level = "info", fields(uid = %uid, manual = is_mannual_trigger))]
pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual_trigger: bool) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let url_opt = should_update_profile(uid, is_mannual_trigger).await?;
let profile_persisted = match url_opt {
@@ -210,10 +197,9 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
let should_refresh = is_current || (!profile_persisted && is_mannual_trigger);
if should_refresh {
logging!(info, Type::Config, "[订阅更新] 更新内核配置");
logging!(debug, Type::Config, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config_with_force(is_mannual_trigger).await {
Ok(outcome) if outcome.is_valid() => {
logging!(info, Type::Config, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => {
+9 -5
View File
@@ -23,7 +23,11 @@ pub async fn toggle_system_proxy() -> Option<bool> {
&& auto_close_connection
&& let Err(err) = handle::Handle::mihomo().close_all_connections().await
{
logging!(error, Type::ProxyMode, "Failed to close all connections: {err}");
logging!(
error,
Type::ProxyMode,
"toggle system proxy: failed to close all connections: {err}"
);
}
let requested = !current;
@@ -42,7 +46,7 @@ pub async fn toggle_system_proxy() -> Option<bool> {
match patch_result {
Ok(_) => Some(requested),
Err(err) => {
logging!(error, Type::ProxyMode, "{err:#}");
logging!(error, Type::ProxyMode, "toggle system proxy failed: {err:#}");
report_toggle_failure(&err).await;
None
}
@@ -84,7 +88,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) -> bool {
Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false)
}
Err(err) => {
logging!(error, Type::ProxyMode, "{err:#}");
logging!(error, Type::ProxyMode, "toggle tun mode failed: {err:#}");
current
}
}
@@ -133,7 +137,7 @@ pub async fn copy_clash_env() {
}
};
if clipboard.write_text(&export_text).is_err() {
logging!(error, Type::ProxyMode, "Failed to write to clipboard");
if let Err(err) = clipboard.write_text(&export_text) {
logging!(error, Type::ProxyMode, "Failed to write to clipboard: {err}");
}
}
+7 -20
View File
@@ -88,11 +88,11 @@ async fn restore_dns_after_core_stop() -> bool {
.await
{
Ok(_) => {
logging!(info, Type::Window, "DNS设置已恢复");
logging!(debug, Type::Window, "DNS设置已恢复");
true
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 恢复DNS设置超时");
logging!(warn, Type::Window, "恢复DNS设置超时");
false
}
}
@@ -117,7 +117,6 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
Config::apply_all_and_save_file().await;
logging!(info, Type::System, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
@@ -143,21 +142,12 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
}
pub async fn clean_async() -> CleanupResult {
logging!(
info,
Type::System,
"Starting interactive cleanup; controlled core stop will be awaited to completion"
);
let stop_error = Mutex::new(None);
let mut result = run_interactive_cleanup_transition(
|| async {
logging!(info, Type::System, "Stopping core for interactive quit or restart");
logging!(debug, Type::System, "Stopping core for interactive quit or restart");
match CoreManager::global().stop_core().await {
Ok(()) => {
logging!(info, Type::Window, "Core stopped for interactive quit or restart");
true
}
Ok(()) => true,
Err(error) => {
logging!(
warn,
@@ -199,13 +189,10 @@ pub async fn clean_session_ending_best_effort() -> CleanupResult {
let result = run_session_ending_cleanup_transition(
|| async {
logging!(info, Type::System, "Clearing all WebSocket connections before stopping core");
let _ = handle::Handle::mihomo().clear_all_ws_connections();
logging!(info, Type::System, "Stopping core during session-ending best-effort cleanup");
match CoreManager::global().stop_core().await {
let _ = handle::Handle::mihomo().clear_all_ws_connections();
match CoreManager::global().stop_core().await {
Ok(()) => {
logging!(info, Type::Window, "Core stopped during session-ending best-effort cleanup");
true
true
}
Err(error) => {
logging!(