From 3ca3a48c383ab641a199c419ee5b25d597181356 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Fri, 11 Jul 2025 05:38:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 196 +++++++++++++++++++++++++++ src-tauri/Cargo.toml | 5 + src-tauri/src/config.rs | 2 + src-tauri/src/errors.rs | 26 ++++ src-tauri/src/events.rs | 19 +++ src-tauri/src/extensions.rs | 15 +++ src-tauri/src/lib.rs | 10 +- src-tauri/src/logger.rs | 221 +++++++++++++++++++++++++++++++ src-tauri/src/types/log_level.rs | 16 +++ src-tauri/src/types/mod.rs | 3 + src/bindings.ts | 10 +- 11 files changed, 521 insertions(+), 2 deletions(-) create mode 100644 src-tauri/src/errors.rs create mode 100644 src-tauri/src/events.rs create mode 100644 src-tauri/src/logger.rs create mode 100644 src-tauri/src/types/log_level.rs create mode 100644 src-tauri/src/types/mod.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 78b6c3f..b370e79 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -268,6 +268,7 @@ name = "bilibili-video-downloader" version = "0.1.0" dependencies = [ "anyhow", + "notify", "parking_lot", "serde", "serde_json", @@ -277,6 +278,10 @@ dependencies = [ "tauri-build", "tauri-plugin-opener", "tauri-specta", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] @@ -1011,6 +1016,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futf" version = "0.1.5" @@ -1721,6 +1735,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -1861,6 +1895,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2032,6 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -2112,6 +2167,40 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97" +dependencies = [ + "bitflags 2.9.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2149,6 +2238,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -2406,6 +2504,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -3288,6 +3392,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3944,6 +4057,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -3952,7 +4074,9 @@ checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -3996,12 +4120,26 @@ dependencies = [ "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "tokio-util" version = "0.7.15" @@ -4134,6 +4272,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -4152,6 +4302,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4306,6 +4496,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0b96406..5fb8847 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,11 @@ specta-typescript = { version = "=0.0.7" } anyhow = { version = "1.0.98" } parking_lot = { version = "0.12.4", features = ["send_guard"] } +tracing = { version = "0.1.41" } +tracing-subscriber = { version = "0.3.19", features = ["json", "time", "local-time"] } +tracing-appender = { version = "0.2.3" } +notify = { version = "8.0.0" } +tokio = { version = "1.46.0", features = ["full"] } [profile.release] strip = true diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index cb080d1..0993dae 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -8,6 +8,7 @@ use tauri::{AppHandle, Manager}; #[serde(rename_all = "camelCase")] pub struct Config { pub download_dir: PathBuf, + pub enable_file_logger: bool, } impl Config { @@ -64,6 +65,7 @@ impl Config { fn default(app_data_dir: &Path) -> Config { Config { download_dir: app_data_dir.join("视频下载"), + enable_file_logger: true, } } } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs new file mode 100644 index 0000000..e82308e --- /dev/null +++ b/src-tauri/src/errors.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::extensions::AnyhowErrorToStringChain; + +pub type CommandResult = Result; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct CommandError { + pub err_title: String, + pub err_message: String, +} + +impl CommandError { + pub fn from(err_title: &str, err: E) -> Self + where + E: Into, + { + let string_chain = err.into().to_string_chain(); + tracing::error!(err_title, message = string_chain); + Self { + err_title: err_title.to_string(), + err_message: string_chain, + } + } +} diff --git a/src-tauri/src/events.rs b/src-tauri/src/events.rs new file mode 100644 index 0000000..3aa35c7 --- /dev/null +++ b/src-tauri/src/events.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event; + +use crate::types::LogLevel; + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[serde(rename_all = "camelCase")] +pub struct LogEvent { + pub timestamp: String, + pub level: LogLevel, + pub fields: HashMap, + pub target: String, + pub filename: String, + #[serde(rename = "line_number")] + pub line_number: i64, +} diff --git a/src-tauri/src/extensions.rs b/src-tauri/src/extensions.rs index 74ad177..eba86ad 100644 --- a/src-tauri/src/extensions.rs +++ b/src-tauri/src/extensions.rs @@ -1,3 +1,8 @@ +use parking_lot::RwLock; +use tauri::{Manager, State}; + +use crate::config::Config; + pub trait AnyhowErrorToStringChain { /// 将 `anyhow::Error` 转换为chain格式 /// # Example @@ -18,3 +23,13 @@ impl AnyhowErrorToStringChain for anyhow::Error { }) } } + +pub trait AppHandleExt { + fn get_config(&self) -> State>; +} + +impl AppHandleExt for tauri::AppHandle { + fn get_config(&self) -> State> { + self.state::>() + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 16df6db..24f32af 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,10 @@ mod commands; mod config; +mod errors; +mod events; mod extensions; +mod logger; +mod types; mod utils; use anyhow::Context; @@ -9,6 +13,8 @@ use config::Config; use parking_lot::RwLock; use tauri::{Manager, Wry}; +use crate::events::LogEvent; + fn generate_context() -> tauri::Context { tauri::generate_context!() } @@ -17,7 +23,7 @@ fn generate_context() -> tauri::Context { pub fn run() { let builder = tauri_specta::Builder::::new() .commands(tauri_specta::collect_commands![greet, get_config]) - .events(tauri_specta::collect_events![]); + .events(tauri_specta::collect_events![LogEvent]); #[cfg(debug_assertions)] builder @@ -47,6 +53,8 @@ pub fn run() { let config = RwLock::new(Config::new(app.handle())?); app.manage(config); + logger::init(app.handle())?; + Ok(()) }) .run(generate_context()) diff --git a/src-tauri/src/logger.rs b/src-tauri/src/logger.rs new file mode 100644 index 0000000..5670178 --- /dev/null +++ b/src-tauri/src/logger.rs @@ -0,0 +1,221 @@ +use std::{io::Write, sync::OnceLock}; + +use anyhow::Context; +use notify::{RecommendedWatcher, Watcher}; +use tauri::{AppHandle, Manager}; +use tauri_specta::Event; +use tracing::{Level, Subscriber}; +use tracing_appender::{ + non_blocking::WorkerGuard, + rolling::{RollingFileAppender, Rotation}, +}; +use tracing_subscriber::{ + filter::{filter_fn, FilterExt, Targets}, + fmt::{layer, time::LocalTime}, + layer::SubscriberExt, + registry::LookupSpan, + util::SubscriberInitExt, + Layer, Registry, +}; + +use crate::{ + events::LogEvent, + extensions::{AnyhowErrorToStringChain, AppHandleExt}, +}; + +struct LogEventWriter { + app: AppHandle, +} + +impl Write for LogEventWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let log_string = String::from_utf8_lossy(buf); + match serde_json::from_str::(&log_string) { + Ok(log_event) => { + let _ = log_event.emit(&self.app); + } + Err(err) => { + let log_string = log_string.to_string(); + let err_msg = err.to_string(); + tracing::error!(log_string, err_msg, "将日志字符串解析为LogEvent失败"); + } + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +static RELOAD_FN: OnceLock anyhow::Result<()> + Send + Sync>> = OnceLock::new(); +static GUARD: OnceLock>> = OnceLock::new(); + +pub fn init(app: &AppHandle) -> anyhow::Result<()> { + let lib_module_path = module_path!(); + let lib_target = lib_module_path.split("::").next().context(format!( + "解析lib_target失败: lib_module_path={lib_module_path}" + ))?; + // 过滤掉来自其他库的日志 + let target_filter = Targets::new().with_target(lib_target, Level::TRACE); + // 输出到文件 + let (file_layer, guard) = create_file_layer(app)?; + let (reloadable_file_layer, reload_handle) = tracing_subscriber::reload::Layer::new(file_layer); + // 输出到控制台 + let console_layer = layer() + .with_writer(std::io::stdout) + .with_timer(LocalTime::rfc_3339()) + .with_file(true) + .with_line_number(true); + // 发送到前端 + let log_event_writer = std::sync::Mutex::new(LogEventWriter { app: app.clone() }); + let log_event_layer = layer() + .with_writer(log_event_writer) + .with_timer(LocalTime::rfc_3339()) + .with_file(true) + .with_line_number(true) + .json() + // 过滤掉来自这个文件的日志(LogEvent解析失败的日志),避免无限递归 + .with_filter(target_filter.clone().and(filter_fn(|metadata| { + metadata.module_path() != Some(lib_module_path) + }))); + + Registry::default() + .with(target_filter) + .with(reloadable_file_layer) + .with(console_layer) + .with(log_event_layer) + .init(); + + GUARD.get_or_init(|| parking_lot::Mutex::new(guard)); + RELOAD_FN.get_or_init(move || { + let app = app.clone(); + Box::new(move || { + let (file_layer, guard) = create_file_layer(&app)?; + reload_handle.reload(file_layer).context("reload失败")?; + *GUARD.get().context("GUARD未初始化")?.lock() = guard; + Ok(()) + }) + }); + tauri::async_runtime::spawn(file_log_watcher(app.clone())); + + Ok(()) +} + +pub fn reload_file_logger() -> anyhow::Result<()> { + RELOAD_FN.get().context("RELOAD_FN未初始化")?() +} + +pub fn disable_file_logger() -> anyhow::Result<()> { + if let Some(guard) = GUARD.get().context("GUARD未初始化")?.lock().take() { + drop(guard); + } + Ok(()) +} + +fn create_file_layer( + app: &AppHandle, +) -> anyhow::Result<(Box + Send + Sync>, Option)> +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + let enable_file_logger = app.get_config().read().enable_file_logger; + // 如果不启用文件日志,则返回一个占位用的sink layer,不创建也不输出日志文件 + if !enable_file_logger { + let sink_layer = layer() + .with_writer(std::io::sink) + .with_timer(LocalTime::rfc_3339()) + .with_ansi(false) + .with_file(true) + .with_line_number(true); + return Ok((Box::new(sink_layer), None)); + } + let logs_dir = logs_dir(app).context("获取日志目录失败")?; + let file_appender = RollingFileAppender::builder() + .filename_prefix("bilibili-video-downloader") + .filename_suffix("log") + .rotation(Rotation::DAILY) + .build(&logs_dir) + .context("创建RollingFileAppender失败")?; + let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender); + let file_layer = layer() + .with_writer(non_blocking_appender) + .with_timer(LocalTime::rfc_3339()) + .with_ansi(false) + .with_file(true) + .with_line_number(true); + Ok((Box::new(file_layer), Some(guard))) +} + +async fn file_log_watcher(app: AppHandle) { + let (sender, mut receiver) = tokio::sync::mpsc::channel(1); + + let event_handler = move |res| { + tauri::async_runtime::block_on(async { + if let Err(err) = sender.send(res).await.map_err(anyhow::Error::from) { + let err_title = "发送日志文件watcher事件失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + } + }); + }; + + let mut watcher = match RecommendedWatcher::new(event_handler, notify::Config::default()) + .map_err(anyhow::Error::from) + { + Ok(watcher) => watcher, + Err(err) => { + let err_title = "创建日志文件watcher失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + return; + } + }; + + let logs_dir = match logs_dir(&app) { + Ok(logs_dir) => logs_dir, + Err(err) => { + let err_title = "日志文件watcher获取日志目录失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + return; + } + }; + + if let Err(err) = watcher + .watch(&logs_dir, notify::RecursiveMode::NonRecursive) + .map_err(anyhow::Error::from) + { + let err_title = "日志文件watcher监听日志目录失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + return; + } + + while let Some(res) = receiver.recv().await { + match res.map_err(anyhow::Error::from) { + Ok(event) => { + if let notify::EventKind::Remove(_) = event.kind { + if let Err(err) = reload_file_logger() { + let err_title = "重置日志文件失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + } + } + } + Err(err) => { + let err_title = "接收日志文件watcher事件失败"; + let string_chain = err.to_string_chain(); + tracing::error!(err_title, message = string_chain); + } + } + } +} + +pub fn logs_dir(app: &AppHandle) -> anyhow::Result { + let app_data_dir = app + .path() + .app_data_dir() + .context("获取app_data_dir目录失败")?; + Ok(app_data_dir.join("日志")) +} diff --git a/src-tauri/src/types/log_level.rs b/src-tauri/src/types/log_level.rs new file mode 100644 index 0000000..c8bc880 --- /dev/null +++ b/src-tauri/src/types/log_level.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub enum LogLevel { + #[serde(rename = "TRACE")] + Trace, + #[serde(rename = "DEBUG")] + Debug, + #[serde(rename = "INFO")] + Info, + #[serde(rename = "WARN")] + Warn, + #[serde(rename = "ERROR")] + Error, +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs new file mode 100644 index 0000000..402dadc --- /dev/null +++ b/src-tauri/src/types/mod.rs @@ -0,0 +1,3 @@ +mod log_level; + +pub use log_level::*; diff --git a/src/bindings.ts b/src/bindings.ts index 8147007..96ce16a 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -16,6 +16,11 @@ async getConfig() : Promise { /** user-defined events **/ +export const events = __makeEvents__<{ +logEvent: LogEvent +}>({ +logEvent: "log-event" +}) /** user-defined constants **/ @@ -23,7 +28,10 @@ async getConfig() : Promise { /** user-defined types **/ -export type Config = { downloadDir: string } +export type Config = { downloadDir: string; enableFileLogger: boolean } +export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue } +export type LogEvent = { timestamp: string; level: LogLevel; fields: { [key in string]: JsonValue }; target: string; filename: string; line_number: number } +export type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR" /** tauri-specta globals **/