From b5db04f5bdee4233783582b0c5862a13c348febd Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Tue, 3 Mar 2026 04:08:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=9A=84=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 11 +++ src-tauri/Cargo.toml | 1 + src-tauri/src/bili_client.rs | 6 +- src-tauri/src/downloader/download_manager.rs | 38 +++++------ src-tauri/src/downloader/download_task.rs | 38 +++++------ src-tauri/src/downloader/tasks/audio_task.rs | 6 +- src-tauri/src/downloader/tasks/video_task.rs | 6 +- src-tauri/src/errors.rs | 70 ++++++++++++++++++-- src-tauri/src/extensions.rs | 21 ++---- src-tauri/src/lib.rs | 3 + src-tauri/src/logger.rs | 32 ++++----- src/bindings.ts | 2 +- 12 files changed, 149 insertions(+), 85 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cf901fb..56fb7f6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -317,6 +317,7 @@ dependencies = [ "tokio", "tracing", "tracing-appender", + "tracing-error", "tracing-subscriber", "uuid", "yaserde", @@ -4890,6 +4891,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1f283e2..100f28c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ 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" } +tracing-error = { version = "0.2.1" } notify = { version = "8.0.0" } tokio = { version = "1.46.0", features = ["full"] } byteorder = { version = "1.5.0" } diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 4092901..4a5d8c1 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -18,7 +18,7 @@ use tokio::task::JoinSet; use crate::{ config::ProxyMode, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, protobuf::DmSegMobileReply, types::{ bangumi_follow_info::BangumiFollowInfo, bangumi_info::BangumiInfo, @@ -1132,8 +1132,8 @@ impl ClientBuilderExt for reqwest::ClientBuilder { Ok(proxy) => self.proxy(proxy), Err(err) => { let err_title = format!("{client_name}将`{proxy_url}`设为代理失败,将直连"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); self.no_proxy() } } diff --git a/src-tauri/src/downloader/download_manager.rs b/src-tauri/src/downloader/download_manager.rs index 4daff3d..ad42d1c 100644 --- a/src-tauri/src/downloader/download_manager.rs +++ b/src-tauri/src/downloader/download_manager.rs @@ -16,7 +16,7 @@ use tokio::sync::Semaphore; use crate::{ events::DownloadEvent, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, types::{ create_download_task_params::CreateDownloadTaskParams, restart_download_task_params::RestartDownloadTaskParams, @@ -109,8 +109,8 @@ impl DownloadManager { for task_id in task_ids { let Some(task) = tasks.get(task_id) else { let err_title = "暂停下载任务失败"; - let err_msg = format!("未找到ID为`{task_id}`的下载任务"); - tracing::error!(err_title, message = err_msg); + let message = format!("未找到ID为`{task_id}`的下载任务"); + tracing::error!(err_title, message); continue; }; task.set_state(DownloadTaskState::Paused); @@ -123,8 +123,8 @@ impl DownloadManager { for task_id in task_ids { let Some(task) = tasks.get(task_id) else { let err_title = "继续下载任务失败"; - let err_msg = format!("未找到ID为`{task_id}`的下载任务"); - tracing::error!(err_title, message = err_msg); + let message = format!("未找到ID为`{task_id}`的下载任务"); + tracing::error!(err_title, message); continue; }; task.set_state(DownloadTaskState::Pending); @@ -137,15 +137,15 @@ impl DownloadManager { for task_id in task_ids { let Some(task) = tasks.remove(task_id) else { let err_title = "删除下载任务失败"; - let err_msg = format!("未找到ID为`{task_id}`的下载任务"); - tracing::error!(err_title, message = err_msg); + let message = format!("未找到ID为`{task_id}`的下载任务"); + tracing::error!(err_title, message); continue; }; if let Err(err) = self.delete_progress_file(task_id) { let err_title = "删除下载任务失败"; - let err_msg = format!("删除ID为`{task_id}`的下载任务文件失败: {err}"); - tracing::error!(err_title, message = err_msg); + let message = format!("删除ID为`{task_id}`的下载任务文件失败: {err}"); + tracing::error!(err_title, message); tasks.insert(task_id.clone(), task); continue; } @@ -153,8 +153,8 @@ impl DownloadManager { if let Err(err) = task.delete_sender.send(()).map_err(eyre::Report::from) { let err_title = "删除下载任务失败"; let err = err.wrap_err(format!("通知ID为`{task_id}`的下载任务删除失败")); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); tasks.insert(task_id.clone(), task); continue; } @@ -168,16 +168,16 @@ impl DownloadManager { for task_id in task_ids { let Some(task) = tasks.get(task_id) else { let err_title = "重来下载任务失败"; - let err_msg = format!("未找到ID为`{task_id}`的下载任务"); - tracing::error!(err_title, message = err_msg); + let message = format!("未找到ID为`{task_id}`的下载任务"); + tracing::error!(err_title, message); continue; }; if let Err(err) = task.restart_sender.send(()).map_err(eyre::Report::from) { let err_title = "重来下载任务失败"; let err = err.wrap_err(format!("通知ID为`{task_id}`的下载任务重来失败")); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); continue; } @@ -191,8 +191,8 @@ impl DownloadManager { let tasks = self.download_tasks.read(); let Some(task) = tasks.get(task_id) else { let err_title = "重来下载任务失败"; - let err_msg = format!("未找到ID为`{task_id}`的下载任务"); - tracing::error!(err_title, message = err_msg); + let message = format!("未找到ID为`{task_id}`的下载任务"); + tracing::error!(err_title, message); return; }; @@ -220,8 +220,8 @@ impl DownloadManager { if let Err(err) = task.restart_sender.send(()).map_err(eyre::Report::from) { let err_title = "重来下载任务失败"; let err = err.wrap_err(format!("通知ID为`{task_id}`的下载任务重来失败")); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); return; } diff --git a/src-tauri/src/downloader/download_task.rs b/src-tauri/src/downloader/download_task.rs index 40576d5..0100fb6 100644 --- a/src-tauri/src/downloader/download_task.rs +++ b/src-tauri/src/downloader/download_task.rs @@ -11,7 +11,7 @@ use tokio::{ use crate::{ events::DownloadEvent, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, types::create_download_task_params::CreateDownloadTaskParams, }; @@ -42,8 +42,8 @@ impl DownloadTask { let cid = cid.map_or("None".to_string(), |id| id.to_string()); let ids_string = format!("aid: {aid}, cid: {cid}"); let err_title = format!("{ids_string} 创建普通视频的下载进度失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); continue; } }; @@ -58,8 +58,8 @@ impl DownloadTask { Err(err) => { let ids_string = format!("ep_id: {ep_id}"); let err_title = format!("{ids_string} 创建番剧的下载进度失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); continue; } }; @@ -74,8 +74,8 @@ impl DownloadTask { Err(err) => { let ids_string = format!("ep_id: {ep_id}"); let err_title = format!("{ids_string} 创建课程的下载进度失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); continue; } }; @@ -91,8 +91,8 @@ impl DownloadTask { let ids_string = progress.get_ids_string(); let episode_title = &progress.episode_title; let err_title = format!("{ids_string} `{episode_title}`保存下载进度到文件失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } let auto_start = app.get_config().read().auto_start_download_task; @@ -237,8 +237,8 @@ impl DownloadTask { .wrap_err("[继续]失败的任务可以断点续传") { let err_title = format!("{ids_string} `{episode_title}`下载失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); self.set_state(DownloadTaskState::Failed); @@ -289,8 +289,8 @@ impl DownloadTask { Err(err) => { let err_title = format!("{ids_string} `{episode_title}`获取下载任务的permit失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); self.set_state(DownloadTaskState::Failed); @@ -309,8 +309,8 @@ impl DownloadTask { .map_err(eyre::Report::from) { let err_title = format!("{ids_string} `{episode_title}`发送状态`Downloading`失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); self.set_state(DownloadTaskState::Failed); } @@ -352,8 +352,8 @@ impl DownloadTask { if let Err(err) = self.state_sender.send(state).map_err(eyre::Report::from) { let err_title = format!("{ids_string} `{episode_title}`发送状态`{state:?}`失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } @@ -374,8 +374,8 @@ impl DownloadTask { let ids_string = updated_progress.get_ids_string(); let episode_title = &updated_progress.episode_title; let err_title = format!("{ids_string} `{episode_title}`保存下载进度到文件失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } } diff --git a/src-tauri/src/downloader/tasks/audio_task.rs b/src-tauri/src/downloader/tasks/audio_task.rs index ffc1b27..38840c4 100644 --- a/src-tauri/src/downloader/tasks/audio_task.rs +++ b/src-tauri/src/downloader/tasks/audio_task.rs @@ -18,7 +18,7 @@ use crate::{ download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress, download_task::DownloadTask, media_chunk::MediaChunk, }, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, types::{ audio_quality::AudioQuality, bangumi_media_url::BangumiMediaUrl, cheese_media_url::CheeseMediaUrl, normal_media_url::NormalMediaUrl, @@ -393,8 +393,8 @@ impl AudioTask { Ok(i) => download_task.update_progress(|p| p.audio_task.chunks[i].completed = true), Err(err) => { let err_title = format!("{ids_string} `{episode_title}`音频的一个分片下载失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } } diff --git a/src-tauri/src/downloader/tasks/video_task.rs b/src-tauri/src/downloader/tasks/video_task.rs index e2a0b5d..e532476 100644 --- a/src-tauri/src/downloader/tasks/video_task.rs +++ b/src-tauri/src/downloader/tasks/video_task.rs @@ -18,7 +18,7 @@ use crate::{ download_chunk_task::DownloadChunkTask, download_progress::DownloadProgress, download_task::DownloadTask, media_chunk::MediaChunk, }, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, types::{ bangumi_media_url::BangumiMediaUrl, cheese_media_url::CheeseMediaUrl, codec_type::CodecType, normal_media_url::NormalMediaUrl, video_quality::VideoQuality, @@ -404,8 +404,8 @@ impl VideoTask { Ok(i) => download_task.update_progress(|p| p.video_task.chunks[i].completed = true), Err(err) => { let err_title = format!("{ids_string} `{episode_title}`视频的一个分片下载失败"); - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 9b0d2a7..1b22c3c 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -1,14 +1,16 @@ +use std::panic::Location; + +use eyre::EyreHandler; use serde::{Deserialize, Serialize}; use specta::Type; - -use crate::extensions::EyreToStringChain; +use tracing_error::SpanTrace; pub type CommandResult = Result; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] pub struct CommandError { pub err_title: String, - pub err_message: String, + pub message: String, } impl CommandError { @@ -16,11 +18,67 @@ impl CommandError { where E: Into, { - let string_chain = err.into().to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = format!("{:?}", err.into()); + tracing::error!(err_title, message); Self { err_title: err_title.to_string(), - err_message: string_chain, + message, } } } + +struct CustomEyreHandler { + span_trace: SpanTrace, + location: Option<&'static Location<'static>>, +} + +impl EyreHandler for CustomEyreHandler { + fn debug( + &self, + error: &(dyn std::error::Error + 'static), + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + use std::fmt::Write; + + let mut buf = String::new(); + + writeln!(&mut buf, "Error:")?; + writeln!(&mut buf, " 0: {error}")?; + + let mut current = error.source(); + let mut i = 1; + while let Some(cause) = current { + writeln!(&mut buf, " {i}: {cause}")?; + current = cause.source(); + i += 1; + } + + if let Some(loc) = self.location { + writeln!(&mut buf, "Location:")?; + writeln!(&mut buf, " at {}:{}", loc.file(), loc.line())?; + } + + let span_trace = format!("{}", self.span_trace); + if !span_trace.is_empty() { + writeln!(&mut buf, "SpanTrace:")?; + writeln!(&mut buf, "{span_trace}")?; + } + + write!(f, "{}", buf.trim_end())?; + + Ok(()) + } + fn track_caller(&mut self, location: &'static Location<'static>) { + self.location = Some(location); + } +} + +pub fn install_custom_eyre_handler() -> eyre::Result<()> { + eyre::set_hook(Box::new(|_error| { + Box::new(CustomEyreHandler { + span_trace: SpanTrace::capture(), + location: None, + }) + }))?; + Ok(()) +} diff --git a/src-tauri/src/extensions.rs b/src-tauri/src/extensions.rs index cd9a576..f7701b9 100644 --- a/src-tauri/src/extensions.rs +++ b/src-tauri/src/extensions.rs @@ -9,24 +9,13 @@ use crate::{ types::player_info::PlayerInfo, }; -pub trait EyreToStringChain { - /// 将 `eyre::Report` 转换为chain格式 - /// # Example - /// 0: error message\ - /// 1: error message\ - /// 2: error message - fn to_string_chain(&self) -> String; +pub trait EyreReportToMessage { + fn to_message(&self) -> String; } -impl EyreToStringChain for eyre::Report { - fn to_string_chain(&self) -> String { - use std::fmt::Write; - self.chain() - .enumerate() - .fold(String::new(), |mut output, (i, e)| { - let _ = writeln!(output, "{i}: {e}"); - output - }) +impl EyreReportToMessage for eyre::Report { + fn to_message(&self) -> String { + format!("{self:?}") } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 92c29b7..3ab3e57 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -31,6 +31,7 @@ use tauri::{Manager, Wry}; use crate::{ bili_client::BiliClient, downloader::download_manager::DownloadManager, + errors::install_custom_eyre_handler, events::{DownloadEvent, LogEvent}, }; @@ -41,6 +42,8 @@ fn generate_context() -> tauri::Context { #[allow(clippy::missing_panics_doc)] #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + install_custom_eyre_handler().unwrap(); + let builder = tauri_specta::Builder::::new() .commands(tauri_specta::collect_commands![ get_config, diff --git a/src-tauri/src/logger.rs b/src-tauri/src/logger.rs index 322326e..c75482a 100644 --- a/src-tauri/src/logger.rs +++ b/src-tauri/src/logger.rs @@ -9,6 +9,7 @@ use tracing_appender::{ non_blocking::WorkerGuard, rolling::{RollingFileAppender, Rotation}, }; +use tracing_error::ErrorLayer; use tracing_subscriber::{ Layer, Registry, filter::{FilterExt, Targets, filter_fn}, @@ -20,7 +21,7 @@ use tracing_subscriber::{ use crate::{ events::LogEvent, - extensions::{AppHandleExt, EyreToStringChain}, + extensions::{AppHandleExt, EyreReportToMessage}, }; struct LogEventWriter { @@ -36,8 +37,8 @@ impl Write for LogEventWriter { } Err(err) => { let log_string = log_string.to_string(); - let err_msg = err.to_string(); - tracing::error!(log_string, err_msg, "将日志字符串解析为LogEvent失败"); + let message = err.to_string(); + tracing::error!(log_string, message, "将日志字符串解析为LogEvent失败"); } } Ok(buf.len()) @@ -85,6 +86,7 @@ pub fn init(app: &AppHandle) -> eyre::Result<()> { .with(reloadable_file_layer) .with(console_layer) .with(log_event_layer) + .with(ErrorLayer::default()) .init(); GUARD.get_or_init(|| parking_lot::Mutex::new(guard)); @@ -154,8 +156,8 @@ async fn file_log_watcher(app: AppHandle) { tauri::async_runtime::block_on(async { if let Err(err) = sender.send(res).await.map_err(eyre::Report::from) { let err_title = "发送日志文件watcher事件失败"; - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } }); }; @@ -166,8 +168,8 @@ async fn file_log_watcher(app: AppHandle) { Ok(watcher) => watcher, Err(err) => { let err_title = "创建日志文件watcher失败"; - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); return; } }; @@ -176,8 +178,8 @@ async fn file_log_watcher(app: AppHandle) { 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); + let message = err.to_message(); + tracing::error!(err_title, message); return; } }; @@ -187,8 +189,8 @@ async fn file_log_watcher(app: AppHandle) { .map_err(eyre::Report::from) { let err_title = "日志文件watcher监听日志目录失败"; - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); return; } @@ -199,14 +201,14 @@ async fn file_log_watcher(app: AppHandle) { && let Err(err) = reload_file_logger() { let err_title = "重置日志文件失败"; - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } Err(err) => { let err_title = "接收日志文件watcher事件失败"; - let string_chain = err.to_string_chain(); - tracing::error!(err_title, message = string_chain); + let message = err.to_message(); + tracing::error!(err_title, message); } } } diff --git a/src/bindings.ts b/src/bindings.ts index 0db7058..73a4113 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -266,7 +266,7 @@ export type CheeseSearchResult = { ep: EpInCheese | null; info: CheeseInfo } export type CntInfo = { collect: number; play: number; thumb_up: number; share: number } export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string } export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1" -export type CommandError = { err_title: string; err_message: string } +export type CommandError = { err_title: string; message: string } export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig; file_exist_action: FileExistAction; auto_start_download_task: boolean } export type Consulting = { consulting_flag: boolean; consulting_url: string } export type ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }