feat: add clash_verge_logger and clash_verge_service_ipc dependencies; refactor logging and process management

This commit is contained in:
Tunglies
2025-10-08 18:05:43 +08:00
parent 0b6681436a
commit 5376d50cfb
9 changed files with 253 additions and 177 deletions

View File

@@ -1,5 +1,8 @@
use crate::AsyncHandler;
use crate::core::logger::Logger;
use crate::process::CommandChildGuard;
use crate::utils::init::sidecar_writer;
use crate::utils::logging::SharedWriter;
use crate::{
config::*,
core::{
@@ -14,16 +17,12 @@ use crate::{
},
};
use anyhow::Result;
use chrono::Local;
use flexi_logger::DeferredNow;
use flexi_logger::writers::LogWriter;
use log::Record;
use parking_lot::Mutex;
use std::{
fmt,
fs::{File, create_dir_all},
io::Write,
path::PathBuf,
sync::Arc,
};
use tauri_plugin_shell::{ShellExt, process::CommandChild};
use std::{fmt, path::PathBuf, sync::Arc};
use tauri_plugin_shell::ShellExt;
// TODO:
// - 重构,提升模式切换速度
@@ -32,7 +31,7 @@ use tauri_plugin_shell::{ShellExt, process::CommandChild};
#[derive(Debug)]
pub struct CoreManager {
running: Arc<Mutex<RunningMode>>,
child_sidecar: Arc<Mutex<Option<CommandChild>>>,
child_sidecar: Arc<Mutex<Option<CommandChildGuard>>>,
}
/// 内核运行模式
@@ -468,7 +467,7 @@ impl CoreManager {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid
&& pid == current
&& Some(pid) == current
{
logging!(
debug,
@@ -741,14 +740,6 @@ impl CoreManager {
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
create_dir_all(&service_log_dir)?;
let now = Local::now();
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
let (mut rx, child) = app_handle
.shell()
.sidecar(&clash_core)?
@@ -768,27 +759,60 @@ impl CoreManager {
"Started core by sidecar pid: {}",
pid
);
*self.child_sidecar.lock() = Some(child);
*self.child_sidecar.lock() = Some(CommandChildGuard::new(child));
self.set_running_mode(RunningMode::Sidecar);
let mut log_file = std::io::BufWriter::new(File::create(log_path)?);
let shared_writer: SharedWriter =
Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
AsyncHandler::spawn(|| async move {
while let Some(event) = rx.recv().await {
let w = shared_writer.lock().await;
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
let mut now = DeferredNow::default();
let line_str = String::from_utf8_lossy(&line);
let arg = format_args!("{}", line_str);
let record = Record::builder()
.args(arg)
.level(log::Level::Error)
.target("sidecar")
.build();
let _ = w.write(&mut now, &record);
let line = String::from_utf8_lossy(&line);
Logger::global().append_log(line.to_string());
if let Err(e) = writeln!(log_file, "{}", line) {
eprintln!("[Sidecar] write stdout failed: {e}");
}
}
tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default();
let line_str = String::from_utf8_lossy(&line);
let arg = format_args!("{}", line_str);
let record = Record::builder()
.args(arg)
.level(log::Level::Error)
.target("sidecar")
.build();
let _ = w.write(&mut now, &record);
let line = String::from_utf8_lossy(&line);
Logger::global().append_log(line.to_string());
let _ = writeln!(log_file, "[stderr] {}", line);
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let _ = writeln!(log_file, "[terminated] {:?}", term);
let mut now = DeferredNow::default();
let output_str = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal)
} else {
"Process terminated".to_string()
};
let arg = format_args!("{}", output_str);
let record = Record::builder()
.args(arg)
.level(log::Level::Info)
.target("sidecar")
.build();
let _ = w.write(&mut now, &record);
break;
}
_ => {}
@@ -803,12 +827,12 @@ impl CoreManager {
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
child.kill()?;
drop(child);
logging!(
trace,
Type::Core,
true,
"Stopped core by sidecar pid: {}",
"Stopped core by sidecar pid: {:?}",
pid
);
}

View File

@@ -381,6 +381,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
"bin_path": dirs::path_to_str(&bin_path)?,
"config_dir": dirs::path_to_str(&dirs::app_home_dir()?)?,
"config_file": dirs::path_to_str(config_file)?,
// TODO 迁移 Service日志后删除
"log_file": dirs::path_to_str(&dirs::service_log_file()?)?,
});

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use tauri_plugin_shell::process::CommandChild;
#[derive(Debug)]
pub struct CommandChildGuard(Option<CommandChild>);
impl Drop for CommandChildGuard {
fn drop(&mut self) {
if let Err(err) = self.kill() {
log::error!(target: "app", "Failed to kill child process: {}", err);
}
}
}
impl CommandChildGuard {
pub fn new(child: CommandChild) -> Self {
Self(Some(child))
}
pub fn kill(&mut self) -> Result<()> {
if let Some(child) = self.0.take() {
let _ = child.kill();
}
Ok(())
}
pub fn pid(&self) -> Option<u32> {
self.0.as_ref().map(|c| c.pid())
}
}

View File

@@ -1,2 +1,4 @@
mod async_handler;
pub use async_handler::AsyncHandler;
mod guard;
pub use guard::CommandChildGuard;

View File

@@ -144,6 +144,7 @@ pub fn service_path() -> Result<PathBuf> {
Ok(res_dir.join("clash-verge-service.exe"))
}
// TODO 迁移 Service日志后删除
pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local;
@@ -158,6 +159,20 @@ pub fn service_log_file() -> Result<PathBuf> {
Ok(log_file)
}
pub fn sidecar_log_dir() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("sidecar");
let _ = std::fs::create_dir_all(&log_dir);
Ok(log_dir)
}
pub fn service_log_dir() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("service");
let _ = std::fs::create_dir_all(&log_dir);
Ok(log_dir)
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
let path_str = path
.as_os_str()

View File

@@ -1,19 +1,23 @@
cfg_if::cfg_if! {
if #[cfg(not(feature = "tauri-dev"))] {
use crate::utils::logging::{console_colored_format, file_format, NoExternModule};
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger};
}
}
#[cfg(not(feature = "tauri-dev"))]
use crate::utils::logging::NoModuleFilter;
use crate::{
config::*,
core::handle,
logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
utils::{
dirs::{self, service_log_dir, sidecar_log_dir},
help,
logging::Type,
},
};
use anyhow::Result;
use chrono::{Local, TimeZone};
use clash_verge_service_ipc::WriterConfig;
use flexi_logger::writers::FileLogWriter;
use flexi_logger::{Cleanup, Criterion, FileSpec};
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::{Duplicate, LogSpecBuilder, Logger};
use std::{path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
@@ -34,11 +38,13 @@ pub async fn init_logger() -> Result<()> {
};
let log_dir = dirs::app_logs_dir()?;
let logger = Logger::with(LogSpecification::from(log_level))
let spec = LogSpecBuilder::new().default(log_level).build();
let logger = Logger::with(spec)
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(console_colored_format)
.format_for_files(file_format)
.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 {
@@ -47,7 +53,7 @@ pub async fn init_logger() -> Result<()> {
},
Cleanup::KeepLogFiles(log_max_count),
)
.filter(Box::new(NoExternModule));
.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
let _handle = logger.start()?;
@@ -59,6 +65,54 @@ pub async fn init_logger() -> Result<()> {
Ok(())
}
pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let sidecar_log_dir = sidecar_log_dir()?;
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()?)
}
// TODO 后续迁移新 service 时使用
#[allow(dead_code)]
pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.to_string();
Ok(WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
})
}
// TODO flexi_logger 提供了最大保留天数或许我们应该用内置删除log文件
/// 删除log文件
pub async fn delete_log() -> Result<()> {

View File

@@ -1,15 +1,12 @@
cfg_if::cfg_if! {
if #[cfg(feature = "tauri-dev")] {
use std::fmt;
} else {
#[cfg(feature = "verge-dev")]
use nu_ansi_term::Color;
use std::{fmt, io::Write, thread};
use flexi_logger::DeferredNow;
use log::{LevelFilter, Record};
use flexi_logger::filter::LogLineFilter;
}
}
use flexi_logger::writers::FileLogWriter;
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::{DeferredNow, filter::LogLineFilter};
#[cfg(not(feature = "tauri-dev"))]
use log::Record;
use std::{fmt, sync::Arc};
use tokio::sync::Mutex;
pub type SharedWriter = Arc<Mutex<FileLogWriter>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
@@ -28,7 +25,6 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
// Ipc,
// Cache,
ClashVergeRev,
}
@@ -51,7 +47,6 @@ impl fmt::Display for Type {
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
// Type::Ipc => write!(f, "[IPC]"),
// Type::Cache => write!(f, "[Cache]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
@@ -173,75 +168,36 @@ macro_rules! logging_error {
}
#[cfg(not(feature = "tauri-dev"))]
static IGNORE_MODULES: &[&str] = &["tauri", "wry"];
pub struct NoModuleFilter<'a>(pub &'a [&'a str]);
#[cfg(not(feature = "tauri-dev"))]
pub struct NoExternModule;
impl<'a> NoModuleFilter<'a> {
#[inline]
pub fn filter(&self, record: &Record) -> bool {
if let Some(module) = record.module_path() {
for blocked in self.0 {
if module.len() >= blocked.len()
&& module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..]
{
return false;
}
}
}
true
}
}
#[cfg(not(feature = "tauri-dev"))]
impl LogLineFilter for NoExternModule {
impl<'a> LogLineFilter for NoModuleFilter<'a> {
fn write(
&self,
now: &mut DeferredNow,
record: &Record,
log_line_writer: &dyn flexi_logger::filter::LogLineWriter,
writer: &dyn flexi_logger::filter::LogLineWriter,
) -> std::io::Result<()> {
let module_path = record.module_path().unwrap_or_default();
if IGNORE_MODULES.iter().any(|m| module_path.starts_with(m)) {
Ok(())
} else {
log_line_writer.write(now, record)
if !self.filter(record) {
return Ok(());
}
writer.write(now, record)
}
}
#[cfg(not(feature = "tauri-dev"))]
pub fn get_log_level(log_level: &LevelFilter) -> String {
#[cfg(feature = "verge-dev")]
match log_level {
LevelFilter::Off => Color::Fixed(8).paint("OFF").to_string(),
LevelFilter::Error => Color::Red.paint("ERROR").to_string(),
LevelFilter::Warn => Color::Yellow.paint("WARN ").to_string(),
LevelFilter::Info => Color::Green.paint("INFO ").to_string(),
LevelFilter::Debug => Color::Blue.paint("DEBUG").to_string(),
LevelFilter::Trace => Color::Purple.paint("TRACE").to_string(),
}
#[cfg(not(feature = "verge-dev"))]
log_level.to_string()
}
#[cfg(not(feature = "tauri-dev"))]
pub fn console_colored_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &log::Record,
) -> std::io::Result<()> {
let current_thread = thread::current();
let thread_name = current_thread.name().unwrap_or("unnamed");
let level = get_log_level(&record.level().to_level_filter());
let line = record.line().unwrap_or(0);
write!(
w,
"[{}] {} [{}:{}] T[{}] {}",
now.format("%H:%M:%S%.3f"),
level,
record.module_path().unwrap_or("<unnamed>"),
line,
thread_name,
record.args(),
)
}
#[cfg(not(feature = "tauri-dev"))]
pub fn file_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &Record,
) -> std::io::Result<()> {
write!(
w,
"[{}] {} {}",
now.format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.args(),
)
}