use std::{ str::FromStr as _, sync::{ Arc, atomic::{AtomicU64, AtomicUsize, Ordering}, }, }; use anyhow::{Result, bail}; use clash_verge_logging::{Type, logging}; use clash_verge_service_ipc::WriterConfig; 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 crate::{ core::service, singleton, utils::dirs::{self, service_log_dir, sidecar_log_dir}, }; pub struct Logger { handle: Arc>>, sidecar_file_writer: Arc>>, log_level: Arc>, 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::NoModuleFilter(filter_modules))); 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| { let payload = info .payload() .downcast_ref::<&str>() .unwrap_or(&"Unknown panic payload"); 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 { 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); // update service writer config if service::is_service_ipc_path_exists() && service::is_service_available().await.is_ok() { let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into(); clash_verge_service_ipc::update_writer(&WriterConfig { directory: service_log_dir, max_log_size: log_max_size * 1024, max_log_files: log_max_count, }) .await?; } Ok(()) } fn generate_sidecar_writer(&self) -> Result { 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(), ) .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()?) } 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"); } } pub fn service_writer_config(&self) -> Result { let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into(); let log_max_size = self.log_max_size.load(Ordering::SeqCst); let log_max_count = self.log_max_count.load(Ordering::SeqCst); let writer_config = WriterConfig { directory: service_log_dir, max_log_size: log_max_size * 1024, max_log_files: log_max_count, }; Ok(writer_config) } }