mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-06 00:32:13 +08:00
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:
+48
-223
@@ -1,236 +1,61 @@
|
||||
use std::{
|
||||
str::FromStr as _,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use compact_str::CompactString;
|
||||
use flexi_logger::{
|
||||
Cleanup, Criterion, DeferredNow, FileSpec, LogSpecBuilder, LogSpecification, LoggerHandle,
|
||||
writers::{FileLogWriter, FileLogWriterBuilder, LogWriter as _},
|
||||
};
|
||||
use log::{Level, LevelFilter, Record};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{LoggerConfig, Type, logging};
|
||||
|
||||
use crate::{
|
||||
core::{CoreManager, manager::RunningMode, service},
|
||||
singleton,
|
||||
utils::dirs::{self, sidecar_log_dir},
|
||||
utils::dirs,
|
||||
};
|
||||
|
||||
pub use clash_verge_logging::Logger;
|
||||
|
||||
const fn should_sync_service_writer(running_mode: RunningMode) -> bool {
|
||||
matches!(running_mode, RunningMode::Service)
|
||||
}
|
||||
|
||||
pub struct Logger {
|
||||
handle: Arc<Mutex<Option<LoggerHandle>>>,
|
||||
sidecar_file_writer: Arc<RwLock<Option<FileLogWriter>>>,
|
||||
log_level: Arc<RwLock<LevelFilter>>,
|
||||
log_max_size: AtomicU64,
|
||||
log_max_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl Default for Logger {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handle: Arc::new(Mutex::new(None)),
|
||||
sidecar_file_writer: Arc::new(RwLock::new(None)),
|
||||
log_level: Arc::new(RwLock::new(LevelFilter::Info)),
|
||||
log_max_size: AtomicU64::new(128),
|
||||
log_max_count: AtomicUsize::new(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
singleton!(Logger, LOGGER);
|
||||
|
||||
impl Logger {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let (log_level, log_max_size, log_max_count) = {
|
||||
let verge_guard = crate::config::Config::verge().await;
|
||||
let verge = verge_guard.latest_arc();
|
||||
(
|
||||
verge.get_log_level(),
|
||||
verge.app_log_max_size.unwrap_or(128),
|
||||
verge.app_log_max_count.unwrap_or(8),
|
||||
)
|
||||
};
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|v| log::LevelFilter::from_str(&v).ok())
|
||||
.unwrap_or(log_level);
|
||||
*self.log_level.write() = log_level;
|
||||
self.log_max_size.store(log_max_size, Ordering::SeqCst);
|
||||
self.log_max_count.store(log_max_count, Ordering::SeqCst);
|
||||
|
||||
#[cfg(not(any(feature = "tauri-dev", feature = "tokio-trace")))]
|
||||
{
|
||||
let log_spec = Self::generate_log_spec(log_level);
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
let logger = flexi_logger::Logger::with(log_spec)
|
||||
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
|
||||
.duplicate_to_stdout(log_level.into())
|
||||
.format(clash_verge_logger::console_format)
|
||||
.format_for_files(clash_verge_logger::file_format_with_level)
|
||||
.rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
);
|
||||
|
||||
let mut filter_modules = vec!["wry", "tokio_tungstenite", "tungstenite"];
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
filter_modules.push("tauri");
|
||||
#[cfg(feature = "tracing")]
|
||||
filter_modules.extend(["tauri_plugin_mihomo", "kode_bridge"]);
|
||||
let logger = logger.filter(Box::new(clash_verge_logging::ModuleFilter::new(
|
||||
filter_modules,
|
||||
Some(vec!["tauri_plugin_mihomo"]),
|
||||
)));
|
||||
|
||||
let handle = logger.start()?;
|
||||
*self.handle.lock() = Some(handle);
|
||||
}
|
||||
|
||||
let sidecar_file_writer = self.generate_sidecar_writer()?;
|
||||
*self.sidecar_file_writer.write() = Some(sidecar_file_writer);
|
||||
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
// Capture both common panic payload types instead of logging String payloads as unknown.
|
||||
// This global hook covers panics after logger init; early setup panics are handled separately.
|
||||
let payload = info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| (*s).to_string())
|
||||
.or_else(|| info.payload().downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "Unknown panic payload".to_string());
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
|
||||
.unwrap_or_else(|| "Unknown location".to_string());
|
||||
logging!(error, Type::System, "Panic occurred at {}: {}", location, payload);
|
||||
if let Some(h) = Self::global().handle.lock().as_ref() {
|
||||
h.flush();
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_log_spec(log_level: LevelFilter) -> LogSpecification {
|
||||
let mut spec = LogSpecBuilder::new();
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|v| log::LevelFilter::from_str(&v).ok())
|
||||
.unwrap_or(log_level);
|
||||
spec.default(log_level);
|
||||
#[cfg(feature = "tracing")]
|
||||
spec.module("tauri", log::LevelFilter::Debug)
|
||||
.module("wry", log::LevelFilter::Off)
|
||||
.module("tauri_plugin_mihomo", log::LevelFilter::Off);
|
||||
spec.build()
|
||||
}
|
||||
|
||||
fn generate_file_log_writer(&self) -> Result<FileLogWriterBuilder> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
|
||||
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
|
||||
let flwb = FileLogWriter::builder(FileSpec::default().directory(log_dir).basename("")).rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
);
|
||||
Ok(flwb)
|
||||
}
|
||||
|
||||
/// only update app log level
|
||||
pub fn update_log_level(&self, level: LevelFilter) -> Result<()> {
|
||||
*self.log_level.write() = level;
|
||||
let log_level = self.log_level.read().to_owned();
|
||||
if let Some(handle) = self.handle.lock().as_mut() {
|
||||
let log_spec = Self::generate_log_spec(log_level);
|
||||
handle.set_new_spec(log_spec);
|
||||
handle.adapt_duplication_to_stdout(log_level.into())?;
|
||||
} else {
|
||||
bail!("failed to get logger handle, make sure it init");
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update app and mihomo core log config
|
||||
pub async fn update_log_config(&self, log_max_size: u64, log_max_count: usize) -> Result<()> {
|
||||
self.log_max_size.store(log_max_size, Ordering::SeqCst);
|
||||
self.log_max_count.store(log_max_count, Ordering::SeqCst);
|
||||
if let Some(handle) = self.handle.lock().as_ref() {
|
||||
let log_file_writer = self.generate_file_log_writer()?;
|
||||
handle.reset_flw(&log_file_writer)?;
|
||||
} else {
|
||||
bail!("failed to get logger handle, make sure it init");
|
||||
};
|
||||
let sidecar_writer = self.generate_sidecar_writer()?;
|
||||
*self.sidecar_file_writer.write() = Some(sidecar_writer);
|
||||
|
||||
// The service writer is auxiliary to the local logger. Synchronize it only
|
||||
// for an active service session and do not roll back local settings on failure.
|
||||
if should_sync_service_writer(*CoreManager::global().get_running_mode())
|
||||
&& let Err(error) = service::update_writer_by_service(&clash_verge_service_ipc::WriterConfig {
|
||||
directory: String::new(),
|
||||
max_log_size: log_max_size * 1024,
|
||||
max_log_files: log_max_count,
|
||||
})
|
||||
.await
|
||||
{
|
||||
logging!(warn, Type::Service, "failed to update service writer config: {error:#}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_sidecar_writer(&self) -> Result<FileLogWriter> {
|
||||
let sidecar_log_dir = sidecar_log_dir()?;
|
||||
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
|
||||
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
|
||||
Ok(FileLogWriter::builder(
|
||||
FileSpec::default()
|
||||
.directory(sidecar_log_dir)
|
||||
.basename("sidecar")
|
||||
.suppress_timestamp(),
|
||||
async fn logger_config() -> Result<LoggerConfig> {
|
||||
let (level, max_size, max_count) = {
|
||||
let verge_guard = crate::config::Config::verge().await;
|
||||
let verge = verge_guard.latest_arc();
|
||||
(
|
||||
verge.get_log_level(),
|
||||
verge.app_log_max_size.unwrap_or(128),
|
||||
verge.app_log_max_count.unwrap_or(8),
|
||||
)
|
||||
.format(clash_verge_logger::file_format_without_level)
|
||||
.rotate(
|
||||
Criterion::Size(log_max_size * 1024),
|
||||
flexi_logger::Naming::TimestampsCustomFormat {
|
||||
current_infix: Some("latest"),
|
||||
format: "%Y-%m-%d_%H-%M-%S",
|
||||
},
|
||||
Cleanup::KeepLogFiles(log_max_count),
|
||||
)
|
||||
.try_build()?)
|
||||
};
|
||||
Ok(LoggerConfig {
|
||||
level,
|
||||
max_size_kb: max_size,
|
||||
max_count,
|
||||
log_dir: dirs::app_logs_dir()?,
|
||||
sidecar_log_dir: dirs::sidecar_log_dir()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Initializes the logging stack. The devtools plugin installs its own global
|
||||
/// subscriber; in that mode only the sidecar writer and panic hook are set up.
|
||||
pub async fn init() -> Result<()> {
|
||||
let cfg = logger_config().await?;
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
Logger::global().init(&cfg)?;
|
||||
Logger::global().init_sidecar(&cfg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_log_config(log_max_size: u64, log_max_count: usize) -> Result<()> {
|
||||
Logger::global().update_log_config(log_max_size, log_max_count)?;
|
||||
|
||||
// The service writer is auxiliary to the local logger. Synchronize it only
|
||||
// for an active service session and do not roll back local settings on failure.
|
||||
if should_sync_service_writer(*CoreManager::global().get_running_mode())
|
||||
&& let Err(error) = service::update_writer_by_service(&clash_verge_service_ipc::WriterConfig {
|
||||
directory: String::new(),
|
||||
max_log_size: log_max_size * 1024,
|
||||
max_log_files: log_max_count,
|
||||
})
|
||||
.await
|
||||
{
|
||||
logging!(warn, Type::Service, "failed to update service writer config: {error:#}");
|
||||
}
|
||||
|
||||
pub fn writer_sidecar_log(&self, level: Level, message: &CompactString) {
|
||||
if let Some(writer) = self.sidecar_file_writer.read().as_ref() {
|
||||
let mut now = DeferredNow::default();
|
||||
let args = format_args!("{}", message);
|
||||
let record = Record::builder().args(args).level(level).target("sidecar").build();
|
||||
let _ = writer.write(&mut now, &record);
|
||||
} else {
|
||||
logging!(error, Type::System, "failed to get sidecar file log writer");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
|
||||
match crate::core::service::stage_runtime_by_service(path).await {
|
||||
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
|
||||
Ok(StageRequest::Refused { code, message }) => {
|
||||
let message = message.to_string().into();
|
||||
let message = message.into();
|
||||
if StageRequest::is_about_the_bundle(code) {
|
||||
StageAttempt::RefusedTheBundle(message)
|
||||
} else {
|
||||
@@ -103,6 +103,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
|
||||
|
||||
/// Confirms one silent request because the service may commit before its reply is lost.
|
||||
/// Re-staging an already committed bundle is idempotent and bounded by `confirm_within`.
|
||||
#[tracing::instrument(skip_all, level = "debug", fields(confirm_within = ?confirm_within, asks = 1))]
|
||||
async fn stage_with_confirmation<Ask, Fut>(confirm_within: Duration, ask: Ask) -> StageAttempt
|
||||
where
|
||||
Ask: Fn() -> Fut,
|
||||
@@ -117,6 +118,7 @@ where
|
||||
Type::Core,
|
||||
"Staging did not answer ({first}); asking once more before replacing the core"
|
||||
);
|
||||
tracing::Span::current().record("asks", 2);
|
||||
match tokio::time::timeout(confirm_within, ask()).await {
|
||||
Ok(StageAttempt::Unanswered(again)) => {
|
||||
StageAttempt::Unanswered(format!("{first}; asked again: {again}").into())
|
||||
@@ -150,6 +152,7 @@ impl CoreManager {
|
||||
self.update_config_with_force(true).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(force))]
|
||||
pub async fn update_config_with_force(&self, force: bool) -> Result<ValidationOutcome> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
return Ok(ValidationOutcome::Skipped {
|
||||
@@ -158,7 +161,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
if !self.try_start_config_update() {
|
||||
logging!(info, Type::Core, "Configuration update is already running");
|
||||
logging!(debug, Type::Core, "Configuration update is already running");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
@@ -179,6 +182,7 @@ impl CoreManager {
|
||||
self.perform_config_update(None).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(profile = ?candidate.current, outcome = tracing::field::Empty))]
|
||||
pub(crate) async fn update_config_forced_with_profiles(
|
||||
&self,
|
||||
candidate: &IProfiles,
|
||||
@@ -202,17 +206,23 @@ impl CoreManager {
|
||||
{
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
tracing::Span::current().record("outcome", "rolled_back");
|
||||
self.restore_profile_config(rollback).await?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if !outcome.is_valid() {
|
||||
tracing::Span::current().record("outcome", "invalid");
|
||||
crate::config::profiles::restore_selected_nodes().await;
|
||||
return Ok(Err(outcome));
|
||||
}
|
||||
match candidate.save_file().await {
|
||||
Ok(()) => Ok(Ok(guard)),
|
||||
Ok(()) => {
|
||||
tracing::Span::current().record("outcome", "committed");
|
||||
Ok(Ok(guard))
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::Span::current().record("outcome", "save_rolled_back");
|
||||
self.restore_profile_config(rollback).await?;
|
||||
Err(error)
|
||||
}
|
||||
@@ -273,7 +283,7 @@ impl CoreManager {
|
||||
F: FnOnce(&mut IRuntime),
|
||||
{
|
||||
if !self.try_start_config_update() {
|
||||
logging!(info, Type::Core, "Configuration update is already running");
|
||||
logging!(debug, Type::Core, "Configuration update is already running");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
@@ -315,6 +325,7 @@ impl CoreManager {
|
||||
|
||||
/// Applies through the service, replacing the core when in-place staging is unavailable.
|
||||
/// Caller must hold `lifecycle_lock`.
|
||||
#[tracing::instrument(skip_all, level = "info", fields(path = %path.display(), outcome = tracing::field::Empty))]
|
||||
async fn apply_config_by_service(&self, path: &std::path::Path) -> Result<()> {
|
||||
match plan_config_application(&self.attempt_staging(path).await) {
|
||||
ConfigApplication::Fail(message) => {
|
||||
@@ -328,7 +339,7 @@ impl CoreManager {
|
||||
}
|
||||
ConfigApplication::ReloadFrom(staged) => match self.reload_config(&staged).await {
|
||||
Ok(()) => {
|
||||
logging!(info, Type::Core, "Configuration staged and applied by service");
|
||||
tracing::Span::current().record("outcome", "staged");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => logging!(
|
||||
@@ -341,7 +352,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
self.replace_service_core_with_config(path).await?;
|
||||
logging!(info, Type::Core, "Configuration materialized and applied by service");
|
||||
tracing::Span::current().record("outcome", "replaced");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -353,9 +364,10 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
/// Reload the Core from `path`, and replace the Core if it will not take it.
|
||||
#[tracing::instrument(skip_all, level = "info", fields(outcome = tracing::field::Empty))]
|
||||
async fn reload_or_restart(&self, path: &str) -> Result<()> {
|
||||
let Err(err) = self.reload_config(path).await else {
|
||||
logging!(info, Type::Core, "Configuration applied");
|
||||
tracing::Span::current().record("outcome", "reloaded");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -366,7 +378,7 @@ impl CoreManager {
|
||||
);
|
||||
match self.restart_core_during_config_update().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, "Configuration applied after restart");
|
||||
tracing::Span::current().record("outcome", "restarted");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -317,6 +317,7 @@ impl CoreManager {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(status = tracing::field::Empty, tun_disabled = false, readiness_generation = tracing::field::Empty))]
|
||||
pub async fn continue_with_sidecar(&self) -> Result<()> {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
@@ -329,6 +330,7 @@ impl CoreManager {
|
||||
let config_write = Config::lock_config_write().await;
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
let status = SERVICE_MANAGER.current().await;
|
||||
tracing::Span::current().record("status", tracing::field::debug(&status));
|
||||
let mode = self.get_running_mode();
|
||||
if !can_allow_sidecar_for_session(&mode, &status) {
|
||||
anyhow::bail!("Sidecar continuation is not allowed from {mode:?} / {status:?}");
|
||||
@@ -342,6 +344,7 @@ impl CoreManager {
|
||||
.state()
|
||||
.tun_should_be_disabled(tun_enabled)
|
||||
{
|
||||
tracing::Span::current().record("tun_disabled", true);
|
||||
Config::disable_tun_and_persist().await?;
|
||||
}
|
||||
Config::generate().await
|
||||
@@ -361,6 +364,7 @@ impl CoreManager {
|
||||
{
|
||||
anyhow::bail!("Sidecar did not become ready");
|
||||
}
|
||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||
self.apply_proxy_after_start().await
|
||||
}
|
||||
.await;
|
||||
@@ -376,6 +380,7 @@ impl CoreManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(readiness_generation = tracing::field::Empty))]
|
||||
pub async fn uninstall_service_and_start_sidecar(&self) -> Result<()> {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
@@ -413,6 +418,7 @@ impl CoreManager {
|
||||
{
|
||||
anyhow::bail!("Sidecar did not become ready after service uninstall");
|
||||
}
|
||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||
self.apply_proxy_after_start().await
|
||||
},
|
||||
)
|
||||
@@ -456,6 +462,7 @@ impl CoreManager {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(mode = ?*self.get_running_mode(), readiness_generation = self.current_core_readiness_generation(), owner_generation = crate::core::service::owner_monitor_generation()))]
|
||||
pub(crate) async fn apply_proxy_after_start(&self) -> Result<()> {
|
||||
let expectation = ProxyRestoreExpectation::capture(
|
||||
*self.get_running_mode(),
|
||||
@@ -519,6 +526,7 @@ impl CoreManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(decision = tracing::field::Empty))]
|
||||
async fn start_core_inner(&self) -> Result<()> {
|
||||
if Handle::global().is_exiting() {
|
||||
return Ok(());
|
||||
@@ -526,7 +534,7 @@ impl CoreManager {
|
||||
|
||||
if !matches!(*self.get_running_mode(), RunningMode::NotRunning) {
|
||||
logging!(
|
||||
info,
|
||||
debug,
|
||||
Type::Core,
|
||||
"start_core called while a core is running; treated as no-op"
|
||||
);
|
||||
@@ -534,6 +542,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
let startup = self.prepare_startup().await;
|
||||
tracing::Span::current().record("decision", tracing::field::debug(&startup));
|
||||
if matches!(startup, StartupDecision::Wait) {
|
||||
self.rollback_failed_start().await;
|
||||
return Ok(());
|
||||
@@ -563,6 +572,7 @@ impl CoreManager {
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(mode = ?*self.get_running_mode()))]
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
self.controlled_stop_core_inner().await
|
||||
@@ -594,7 +604,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
async fn stop_core_unprepared_inner(&self) -> Result<()> {
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
CLASH_LOGGER.clear_logs();
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => self.stop_core_by_service().await,
|
||||
RunningMode::Sidecar => {
|
||||
@@ -605,6 +615,7 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info")]
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
if !self.try_start_config_update() {
|
||||
anyhow::bail!("configuration update is already running");
|
||||
@@ -617,7 +628,6 @@ impl CoreManager {
|
||||
|
||||
pub(crate) async fn restart_core_during_config_update(&self) -> Result<()> {
|
||||
let _life = self.lifecycle_lock.lock().await;
|
||||
logging!(info, Type::Core, "Restarting core");
|
||||
let proxy_intent = self.proxy_stop_intent().await;
|
||||
run_core_replacement_transition(
|
||||
|| self.controlled_stop_core_with_intent(proxy_intent),
|
||||
@@ -635,6 +645,7 @@ impl CoreManager {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(core = %clash_core))]
|
||||
pub async fn change_core(&self, clash_core: &String) -> Result<()> {
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
|
||||
anyhow::bail!("invalid clash core: {clash_core}");
|
||||
@@ -757,6 +768,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tracing::instrument(skip_all, level = "debug", fields(outcome = tracing::field::Empty))]
|
||||
async fn try_handoff_sidecar_to_service(&self) -> HandoffOutcome {
|
||||
// Before probing: a probe reply records an observation, which clears sidecar_allowed.
|
||||
if crate::core::runstate::RUN_STATE.state().sidecar_allowed {
|
||||
|
||||
@@ -4,8 +4,7 @@ mod state;
|
||||
|
||||
use anyhow::Result;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use clash_verge_logger::AsyncLogger;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_logging::{LogRing, Type, logging};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -22,7 +21,7 @@ use crate::singleton;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::io::OwnedHandle;
|
||||
|
||||
pub(crate) static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = Lazy::new(|| Arc::new(AsyncLogger::new()));
|
||||
pub(crate) static CLASH_LOGGER: Lazy<Arc<LogRing>> = Lazy::new(|| Arc::new(LogRing::new()));
|
||||
|
||||
tokio::task_local! {
|
||||
static PROFILE_SELECTIONS_PENDING_COMMIT: bool;
|
||||
@@ -161,6 +160,10 @@ impl CoreManager {
|
||||
/// Run State derives PAC availability and the outward mode mirror from this; callers must
|
||||
/// not set those alongside.
|
||||
pub fn core_started(&self, mode: RunningMode) {
|
||||
let previous = *self.get_running_mode();
|
||||
if previous != mode {
|
||||
logging!(info, Type::Core, "Core running mode changed: {previous} -> {mode}");
|
||||
}
|
||||
self.run_state.core_started(mode);
|
||||
}
|
||||
|
||||
@@ -169,6 +172,10 @@ impl CoreManager {
|
||||
/// Core readiness is invalidated here rather than inside Run State because readiness
|
||||
/// belongs to the process this manager supervises.
|
||||
pub fn core_stopped(&self) {
|
||||
let previous = *self.get_running_mode();
|
||||
if !matches!(previous, RunningMode::NotRunning) {
|
||||
logging!(info, Type::Core, "Core running mode changed: {previous} -> NotRunning");
|
||||
}
|
||||
self.invalidate_core_readiness();
|
||||
self.run_state.core_stopped();
|
||||
}
|
||||
@@ -244,6 +251,7 @@ impl CoreManager {
|
||||
self.config_update_in_progress.store(false, Ordering::Release);
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(retries = 0))]
|
||||
pub async fn init(&self) -> Result<bool> {
|
||||
const MAX_PORT_FALLBACK_RETRIES: usize = 3;
|
||||
|
||||
@@ -266,10 +274,11 @@ impl CoreManager {
|
||||
match crate::config::Config::resolve_startup_mixed_port().await {
|
||||
Ok(true) => {
|
||||
retries += 1;
|
||||
tracing::Span::current().record("retries", retries);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Retrying core startup after mixed proxy port fallback ({}/{})",
|
||||
"Retrying core startup after mixed proxy port fallback ({}/{}): {start_error:#}",
|
||||
retries,
|
||||
MAX_PORT_FALLBACK_RETRIES
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use clash_verge_logging::Type;
|
||||
use compact_str::CompactString;
|
||||
use log::Level;
|
||||
use scopeguard::defer;
|
||||
use std::path::Path;
|
||||
@@ -49,7 +48,16 @@ where
|
||||
for attempt in 0..max_attempts {
|
||||
match probe().await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => last_error = Some(error),
|
||||
Err(error) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
"sidecar readiness probe {}/{} failed: {error:#}",
|
||||
attempt + 1,
|
||||
max_attempts
|
||||
);
|
||||
last_error = Some(error);
|
||||
}
|
||||
}
|
||||
if attempt + 1 < max_attempts {
|
||||
tokio::time::sleep(retry_delay).await;
|
||||
@@ -80,16 +88,16 @@ use {
|
||||
};
|
||||
|
||||
impl CoreManager {
|
||||
pub async fn get_clash_logs(&self) -> Result<Vec<CompactString>> {
|
||||
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => service::get_clash_logs_by_service().await,
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().await),
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
|
||||
RunningMode::NotRunning => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(pid = tracing::field::Empty))]
|
||||
pub(super) async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Starting core in sidecar mode");
|
||||
self.core_stopped();
|
||||
|
||||
let sidecar_ipc = dirs::sidecar_ipc_path()?;
|
||||
@@ -158,7 +166,7 @@ impl CoreManager {
|
||||
};
|
||||
|
||||
let pid = child.pid();
|
||||
logging!(trace, Type::Core, "Sidecar started with PID: {}", pid);
|
||||
tracing::Span::current().record("pid", pid);
|
||||
|
||||
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
|
||||
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
|
||||
@@ -192,22 +200,22 @@ impl CoreManager {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(line)
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let message = CompactString::from(&*String::from_utf8_lossy(&line));
|
||||
let message = String::from_utf8_lossy(&line).into_owned();
|
||||
Logger::global().writer_sidecar_log(Level::Error, &message);
|
||||
CLASH_LOGGER.append_log(message).await;
|
||||
CLASH_LOGGER.append_log(message);
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
|
||||
let manager = Self::global();
|
||||
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
|
||||
let message = if let Some(code) = term.code {
|
||||
CompactString::from(format!("Process terminated with code: {}", code))
|
||||
format!("Process terminated with code: {}", code)
|
||||
} else if let Some(signal) = term.signal {
|
||||
CompactString::from(format!("Process terminated by signal: {}", signal))
|
||||
format!("Process terminated by signal: {}", signal)
|
||||
} else {
|
||||
CompactString::from("Process terminated")
|
||||
String::from("Process terminated")
|
||||
};
|
||||
Logger::global().writer_sidecar_log(Level::Info, &message);
|
||||
CLASH_LOGGER.clear_logs().await;
|
||||
CLASH_LOGGER.clear_logs();
|
||||
manager.clear_terminated_sidecar(pid).await;
|
||||
break;
|
||||
}
|
||||
@@ -222,7 +230,6 @@ impl CoreManager {
|
||||
/// Terminates the sidecar after its caller has successfully cleared the
|
||||
/// system proxy.
|
||||
pub(super) fn stop_core_by_sidecar_unprepared(&self) {
|
||||
logging!(info, Type::Core, "Stopping sidecar");
|
||||
defer! {
|
||||
self.core_stopped();
|
||||
}
|
||||
@@ -234,27 +241,16 @@ impl CoreManager {
|
||||
// Setting the job handle to None clears the stored handle and
|
||||
// closes the previous Windows job handle in `set_job_handle`.
|
||||
self.set_job_handle(None);
|
||||
logging!(
|
||||
trace,
|
||||
Type::Core,
|
||||
"Closed job handle for sidecar process (PID: {})",
|
||||
pid
|
||||
);
|
||||
let _ = pid;
|
||||
}
|
||||
|
||||
let result = child.kill();
|
||||
logging!(
|
||||
trace,
|
||||
Type::Core,
|
||||
"Sidecar stopped (PID: {:?}, Result: {:?})",
|
||||
pid,
|
||||
result
|
||||
);
|
||||
if let Err(error) = child.kill() {
|
||||
logging!(warn, Type::Core, "failed to terminate sidecar PID {pid}: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn start_core_by_service(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Starting core in service mode");
|
||||
self.core_starting();
|
||||
let service_ipc = dirs::ipc_path()?;
|
||||
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
|
||||
@@ -265,6 +261,7 @@ impl CoreManager {
|
||||
self.start_core_by_service_with_config(&config_file).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(config_file = %config_file.display()))]
|
||||
pub(super) async fn start_core_by_service_with_config(&self, config_file: &Path) -> Result<()> {
|
||||
// 交接时等待 sidecar 释放 ext-controller 通道。
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -283,10 +280,9 @@ impl CoreManager {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"service start attempt {}/{} failed: {}",
|
||||
"service start attempt {}/{} failed: {e:#}",
|
||||
attempt + 1,
|
||||
timing::SERVICE_START_RETRIES,
|
||||
e
|
||||
timing::SERVICE_START_RETRIES
|
||||
);
|
||||
last_err = Some(e);
|
||||
tokio::time::sleep(timing::SERVICE_START_RETRY_DELAY).await;
|
||||
@@ -307,7 +303,6 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
pub(super) async fn stop_core_by_service(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, "Stopping service");
|
||||
service::stop_core_by_service().await?;
|
||||
self.core_stopped();
|
||||
Ok(())
|
||||
|
||||
@@ -43,6 +43,11 @@ pub fn start() {
|
||||
};
|
||||
CFRunLoop::get_main().add_source(&source, unsafe { kCFRunLoopCommonModes });
|
||||
ARMED.store(true, Ordering::Release);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
"network watch armed; the system proxy is re-applied on network changes"
|
||||
);
|
||||
}
|
||||
|
||||
fn on_change(_: SCDynamicStore, _: CFArray<CFString>, (): &mut ()) {
|
||||
|
||||
@@ -471,10 +471,13 @@ fn table_effect(result: &Result<()>) -> TableEffect<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(route = tracing::field::Empty))]
|
||||
pub async fn apply() -> Result<()> {
|
||||
let running_mode = CoreManager::global().get_running_mode();
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let result = match proxy_backend_route(cfg!(target_os = "macos"), &running_mode) {
|
||||
let route = proxy_backend_route(cfg!(target_os = "macos"), &running_mode);
|
||||
tracing::Span::current().record("route", tracing::field::debug(&route));
|
||||
let result = match route {
|
||||
ProxyBackendRoute::Local => match Sysopt::global().update_sysproxy().await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => Err(classify_local_apply_failure(error).await),
|
||||
@@ -520,7 +523,7 @@ pub async fn clear() -> Result<()> {
|
||||
|
||||
/// A failed system call is usually a transient RPC hiccup, so give it a few tries.
|
||||
async fn clear_with_retry() -> Result<()> {
|
||||
for _ in 1..CLEAR_ATTEMPTS {
|
||||
for attempt in 1..CLEAR_ATTEMPTS {
|
||||
match clear_inner().await {
|
||||
Err(error)
|
||||
if matches!(
|
||||
@@ -531,7 +534,7 @@ async fn clear_with_retry() -> Result<()> {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"clearing the system proxy failed; retrying: {error:#}"
|
||||
"clearing the system proxy failed (attempt {attempt}/{CLEAR_ATTEMPTS}); retrying: {error:#}"
|
||||
);
|
||||
tokio::time::sleep(CLEAR_RETRY_DELAY).await;
|
||||
}
|
||||
@@ -541,9 +544,12 @@ async fn clear_with_retry() -> Result<()> {
|
||||
clear_inner().await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(route = tracing::field::Empty))]
|
||||
async fn clear_inner() -> Result<()> {
|
||||
let running_mode = CoreManager::global().get_running_mode();
|
||||
match proxy_backend_route(cfg!(target_os = "macos"), &running_mode) {
|
||||
let route = proxy_backend_route(cfg!(target_os = "macos"), &running_mode);
|
||||
tracing::Span::current().record("route", tracing::field::debug(&route));
|
||||
match route {
|
||||
ProxyBackendRoute::Local => Sysopt::global().reset_sysproxy().await.map_err(classify_local_failure),
|
||||
ProxyBackendRoute::Service => {
|
||||
SERVICE_PROXY_OPERATIONS
|
||||
@@ -599,13 +605,16 @@ pub async fn refresh_guard() -> Result<()> {
|
||||
.await
|
||||
{
|
||||
Ok(true) => consecutive_failures = 0,
|
||||
Ok(false) => break,
|
||||
Ok(false) => {
|
||||
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
consecutive_failures += 1;
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"failed to refresh system proxy through Service ({}/{}): {:#}",
|
||||
"failed to refresh system proxy through Service (generation {generation}, {}/{}): {:#}",
|
||||
consecutive_failures,
|
||||
GUARD_FAILURES_BEFORE_STOPPING,
|
||||
error
|
||||
|
||||
@@ -23,7 +23,6 @@ use clash_verge_service_ipc::{
|
||||
MacosProxyConfig, OwnerSessionProof, ProxyApplyOutcome, RuntimeBundle, ServiceErrorCode, StageRuntimeOutcome,
|
||||
StartClashRequest, WriterConfig,
|
||||
};
|
||||
use compact_str::CompactString;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -74,13 +73,19 @@ pub(crate) fn clear_active_service_session() {
|
||||
}
|
||||
|
||||
/// Probes staging support without failing startup when the fast path is unavailable.
|
||||
#[tracing::instrument(skip_all, level = "info", fields(supported = tracing::field::Empty))]
|
||||
async fn probe_runtime_staging_support() -> bool {
|
||||
match clash_verge_service_ipc::get_version().await {
|
||||
Ok(response) if response.code == 0 => response
|
||||
.data
|
||||
.as_ref()
|
||||
.is_some_and(clash_verge_service_ipc::ProtocolInfo::supports_runtime_staging),
|
||||
Ok(response) if response.code == 0 => {
|
||||
let supported = response
|
||||
.data
|
||||
.as_ref()
|
||||
.is_some_and(clash_verge_service_ipc::ProtocolInfo::supports_runtime_staging);
|
||||
tracing::Span::current().record("supported", supported);
|
||||
supported
|
||||
}
|
||||
Ok(response) => {
|
||||
tracing::Span::current().record("supported", false);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -91,6 +96,7 @@ async fn probe_runtime_staging_support() -> bool {
|
||||
false
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::Span::current().record("supported", false);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -818,7 +824,7 @@ async fn collect_service_runtime_bundle(config_file: &Path) -> Result<RuntimeBun
|
||||
|
||||
/// A staging response whose refusal code tells callers whether a fresh start can help.
|
||||
pub(super) enum StageRequest {
|
||||
Refused { code: u16, message: CompactString },
|
||||
Refused { code: u16, message: String },
|
||||
Answered(StageRuntimeOutcome),
|
||||
}
|
||||
|
||||
@@ -841,7 +847,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
|
||||
if response.code > 0 {
|
||||
return Ok(StageRequest::Refused {
|
||||
code: response.code,
|
||||
message: response.message.into(),
|
||||
message: response.message,
|
||||
});
|
||||
}
|
||||
response
|
||||
@@ -851,8 +857,8 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
|
||||
}
|
||||
|
||||
/// 尝试使用服务启动core
|
||||
#[tracing::instrument(skip_all, level = "info", fields(generation = tracing::field::Empty, staging = tracing::field::Empty, code = tracing::field::Empty, outcome = tracing::field::Empty))]
|
||||
pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()> {
|
||||
logging!(info, Type::Service, "尝试使用现有服务启动核心");
|
||||
clear_active_service_session();
|
||||
|
||||
let credentials = current_owner_credentials()?;
|
||||
@@ -867,14 +873,23 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
let response = match clash_verge_service_ipc::start_clash(&credentials, &request).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
tracing::Span::current().record("outcome", "ipc-unreachable");
|
||||
start_owner_monitor();
|
||||
return Err(error).context("无法连接到Clash Verge Service");
|
||||
}
|
||||
};
|
||||
|
||||
if response.code > 0 {
|
||||
tracing::Span::current().record("code", response.code);
|
||||
tracing::Span::current().record("outcome", "refused");
|
||||
let err_msg = response.message;
|
||||
logging!(error, Type::Service, "启动核心失败: {}", err_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
"启动核心失败 (code {}): {}",
|
||||
response.code,
|
||||
err_msg
|
||||
);
|
||||
start_owner_monitor();
|
||||
bail!(
|
||||
"failed to start Service core at {}: {err_msg}",
|
||||
@@ -883,7 +898,9 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
}
|
||||
|
||||
let result = response.data.context("Clash Verge Service 未返回会话信息")?;
|
||||
tracing::Span::current().record("generation", result.session.generation);
|
||||
let supports_runtime_staging = probe_runtime_staging_support().await;
|
||||
tracing::Span::current().record("staging", supports_runtime_staging);
|
||||
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
|
||||
proof: OwnerSessionProof {
|
||||
generation: result.session.generation,
|
||||
@@ -894,14 +911,18 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
|
||||
// PAC follows the Running Mode; the caller opens it via `core_started(Service)`.
|
||||
start_owner_monitor();
|
||||
logging!(info, Type::Service, "服务成功启动核心");
|
||||
tracing::Span::current().record("outcome", "started");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
"服务成功启动核心 (session generation {})",
|
||||
result.session.generation
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 以服务启动core
|
||||
pub(super) async fn run_core_by_service(config_file: &Path) -> Result<()> {
|
||||
logging!(info, Type::Service, "正在尝试通过服务启动核心");
|
||||
|
||||
SERVICE_MANAGER.refresh().await?;
|
||||
|
||||
let status = SERVICE_MANAGER.current().await;
|
||||
@@ -922,9 +943,8 @@ where
|
||||
(captured, operation().await)
|
||||
}
|
||||
|
||||
pub(super) async fn get_clash_logs_by_service() -> Result<Vec<CompactString>> {
|
||||
logging!(info, Type::Service, "正在获取服务模式下的 Clash 日志");
|
||||
|
||||
pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
|
||||
// Frontend-polled: no per-call logging here.
|
||||
let credentials = current_owner_credentials()?;
|
||||
let (generation, response) = capture_generation_before(&OWNER_MONITOR_GENERATION, || {
|
||||
clash_verge_service_ipc::get_clash_logs(&credentials)
|
||||
@@ -937,11 +957,9 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<CompactString>> {
|
||||
recover_after_owner_loss(generation, OwnerRecoveryReason::Displaced).await;
|
||||
}
|
||||
let err_msg = response.message;
|
||||
logging!(error, Type::Service, "获取服务模式下的 Clash 日志失败: {}", err_msg);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
logging!(info, Type::Service, "成功获取服务模式下的 Clash 日志");
|
||||
Ok(response.data.unwrap_or_default())
|
||||
}
|
||||
|
||||
@@ -970,8 +988,8 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
|
||||
}
|
||||
|
||||
/// 通过服务停止core
|
||||
#[tracing::instrument(skip_all, level = "info", fields(code = tracing::field::Empty, outcome = tracing::field::Empty))]
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "通过服务停止核心 (IPC)");
|
||||
cancel_owner_monitors();
|
||||
|
||||
let credentials = match current_owner_credentials() {
|
||||
@@ -1007,11 +1025,20 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
start_owner_monitor();
|
||||
}
|
||||
let err_msg = response.message;
|
||||
logging!(error, Type::Service, "停止核心失败: {}", err_msg);
|
||||
tracing::Span::current().record("code", response.code);
|
||||
tracing::Span::current().record("outcome", "refused");
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
"停止核心失败 (code {}): {}",
|
||||
response.code,
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
clear_active_service_session();
|
||||
tracing::Span::current().record("outcome", "stopped");
|
||||
logging!(info, Type::Service, "服务成功停止核心");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1023,6 +1050,13 @@ pub(crate) async fn update_writer_by_service(writer: &WriterConfig) -> Result<()
|
||||
.await
|
||||
.context("无法连接到Clash Verge Service")?;
|
||||
if response.code > 0 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
"update writer rejected by service: code={}, {}",
|
||||
response.code,
|
||||
response.message
|
||||
);
|
||||
bail!(response.message);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1042,6 +1076,13 @@ pub(super) async fn set_system_proxy_by_service_with_session(
|
||||
.await
|
||||
.context("无法连接到Clash Verge Service")?;
|
||||
if response.code > 0 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
"set system proxy rejected by service: code={}, {}",
|
||||
response.code,
|
||||
response.message
|
||||
);
|
||||
bail!(response.message);
|
||||
}
|
||||
response.data.context("Clash Verge Service 未返回系统代理结果")
|
||||
@@ -1074,13 +1115,24 @@ const SUSTAINED_OWNER_SAMPLES: u8 = 3;
|
||||
fn start_owner_monitor() {
|
||||
let generation = OWNER_MONITOR_GENERATION.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(debug, Type::Service, "owner monitor started (generation {generation})");
|
||||
let mut watch = OwnerWatch::new();
|
||||
loop {
|
||||
tokio::time::sleep(OWNER_MONITOR_INTERVAL).await;
|
||||
if OWNER_MONITOR_GENERATION.load(Ordering::Acquire) != generation {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Service,
|
||||
"owner monitor superseded (generation {generation})"
|
||||
);
|
||||
break;
|
||||
}
|
||||
if !matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Service,
|
||||
"owner monitor stopped; core no longer in service mode (generation {generation})"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1091,7 +1143,7 @@ fn start_owner_monitor() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
"service owner status unavailable for {SUSTAINED_OWNER_SAMPLES} samples; \
|
||||
"service owner status unavailable for {SUSTAINED_OWNER_SAMPLES} samples (generation {generation}); \
|
||||
preserving local proxy state while the core endpoint still answers"
|
||||
);
|
||||
}
|
||||
@@ -1199,6 +1251,7 @@ fn claim_owner_recovery_generation(generation: &AtomicU64, captured_generation:
|
||||
.map(|_| recovery_generation)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(reason = ?reason))]
|
||||
async fn recover_after_owner_loss_while_locked(reason: OwnerRecoveryReason) {
|
||||
logging!(
|
||||
warn,
|
||||
@@ -1215,10 +1268,15 @@ async fn recover_after_owner_loss_while_locked(reason: OwnerRecoveryReason) {
|
||||
}
|
||||
|
||||
let mut last_error = None;
|
||||
for _ in 0..3 {
|
||||
for attempt in 1..=3 {
|
||||
match proxy_control::clear().await {
|
||||
Ok(()) => return,
|
||||
Err(error) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
"proxy clear attempt {attempt}/3 after owner loss failed: {error:#}"
|
||||
);
|
||||
last_error = Some(error);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
@@ -1228,24 +1286,35 @@ async fn recover_after_owner_loss_while_locked(reason: OwnerRecoveryReason) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
"failed to clear local proxy after owner loss: {error}"
|
||||
"failed to clear local proxy after owner loss: {error:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for a repaired service, preserving readable rejection details but classifying sustained
|
||||
/// silence as unavailable.
|
||||
#[tracing::instrument(skip_all, level = "info", fields(attempts = tracing::field::Empty, interval_ms = tracing::field::Empty, outcome = tracing::field::Empty))]
|
||||
async fn wait_for_service_ipc() -> Result<()> {
|
||||
const CONTEXT: &str = "service IPC did not become available";
|
||||
let config = ServiceManager::config();
|
||||
let span = tracing::Span::current();
|
||||
span.record("attempts", config.max_retries);
|
||||
span.record("interval_ms", config.retry_delay.as_millis() as u64);
|
||||
|
||||
match RUN_STATE.await_ready(config.max_retries, config.retry_delay).await {
|
||||
Ok(_) => Ok(()),
|
||||
Ok(_) => {
|
||||
tracing::Span::current().record("outcome", "ready");
|
||||
Ok(())
|
||||
}
|
||||
Err(ReadyWaitError::Unreachable(error)) => {
|
||||
tracing::Span::current().record("outcome", "unreachable");
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
|
||||
Err(error).context(CONTEXT)
|
||||
}
|
||||
Err(ReadyWaitError::Rejected(error)) => Err(error).context(CONTEXT),
|
||||
Err(ReadyWaitError::Rejected(error)) => {
|
||||
tracing::Span::current().record("outcome", "rejected");
|
||||
Err(error).context(CONTEXT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ impl Timer {
|
||||
|
||||
if let Err(e) = self.refresh().await {
|
||||
self.initialized.store(false, Ordering::SeqCst);
|
||||
logging_error!(Type::Timer, "Failed to initialize timer: {}", e);
|
||||
logging!(error, Type::Timer, "Failed to initialize timer: {e:#}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ impl Timer {
|
||||
}
|
||||
|
||||
fn spawn_update_task(uid: String, command_tx: mpsc::UnboundedSender<TimerCommand>) {
|
||||
logging!(info, Type::Timer, "Starting timer task: uid={}", uid);
|
||||
logging!(debug, Type::Timer, "Starting timer task: uid={}", uid);
|
||||
AsyncHandler::spawn(move || async move {
|
||||
Self::wait_until_resolve_done(Duration::from_millis(5000)).await;
|
||||
Self::async_task(&uid).await;
|
||||
@@ -466,8 +466,8 @@ impl Timer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(uid = %uid))]
|
||||
async fn async_task(uid: &String) {
|
||||
let task_start = std::time::Instant::now();
|
||||
logging!(debug, Type::Timer, "Running timer task for profile: {}", uid);
|
||||
|
||||
run_timer_profile_update_transition(
|
||||
@@ -475,15 +475,9 @@ impl Timer {
|
||||
|| feat::update_profile(uid, None, false),
|
||||
|result| match result {
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Timer task completed for uid: {} (took {}ms)",
|
||||
uid,
|
||||
task_start.elapsed().as_millis()
|
||||
);
|
||||
logging!(debug, Type::Timer, "timer task completed for uid: {}", uid);
|
||||
}
|
||||
Err(e) => logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e),
|
||||
Err(e) => logging_error!(Type::Timer, "Failed to update profile uid {}: {e:#}", uid),
|
||||
},
|
||||
|| Self::emit_update_event(uid, false),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user