From 598c5a23919e1fed2e66eb335397db3f64ab8b1a Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sat, 30 May 2026 10:41:40 +0200 Subject: [PATCH] feat: record-daemon logs to a file --- record-daemon/Cargo.lock | 21 +++++++++++ record-daemon/Cargo.toml | 1 + record-daemon/src/main.rs | 52 +++++++++++++++++++++++---- tauri-app/src-tauri/src/daemon_ipc.rs | 39 ++++++++++++++++++++ tauri-app/src-tauri/src/lib.rs | 2 ++ 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/record-daemon/Cargo.lock b/record-daemon/Cargo.lock index 0b87715..da5dc34 100644 --- a/record-daemon/Cargo.lock +++ b/record-daemon/Cargo.lock @@ -2418,6 +2418,7 @@ dependencies = [ "tokio-util", "toml", "tracing", + "tracing-appender", "tracing-subscriber", "uuid", "winapi 0.3.9", @@ -3074,6 +3075,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.117" @@ -3255,6 +3262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde_core", @@ -3520,6 +3528,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" diff --git a/record-daemon/Cargo.toml b/record-daemon/Cargo.toml index 3a43ad5..05f7657 100644 --- a/record-daemon/Cargo.toml +++ b/record-daemon/Cargo.toml @@ -34,6 +34,7 @@ anyhow = "1" # Logging tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" # UUID for recording IDs uuid = { version = "1", features = ["v4", "serde"] } diff --git a/record-daemon/src/main.rs b/record-daemon/src/main.rs index d96df49..386b3e1 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -42,6 +42,10 @@ struct Args { /// Socket path for IPC. #[arg(short, long)] socket: Option, + + /// Path to log file. If not specified, logs to stdout/stderr. + #[arg(short, long, value_name = "PATH")] + log_file: Option, } /// Main daemon structure. @@ -579,15 +583,49 @@ impl Daemon { } } -/// Initialize logging. -fn init_logging(level: &str) { +/// Initialize logging with optional file output. +fn init_logging(level: &str, log_file: Option<&std::path::Path>) { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level)); - tracing_subscriber::registry() - .with(filter) - .with(tracing_subscriber::fmt::layer()) - .init(); + if let Some(log_path) = log_file { + // Ensure parent directory exists + if let Some(parent) = log_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + // Use file appender for logging + let prefix = log_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("daemon"); + + let file_appender = tracing_appender::rolling::RollingFileAppender::builder() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .filename_prefix(prefix) + .filename_suffix("log") + .build(log_path.parent().unwrap_or(std::path::Path::new("."))) + .expect("Failed to create log file appender"); + + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + // Store the guard to prevent it from being dropped + // We leak it intentionally to keep logging working for the daemon's lifetime + std::mem::forget(_guard); + + tracing_subscriber::registry() + .with(filter) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking)) + .init(); + + eprintln!("Logging to file: {}", log_path.display()); + } else { + // Log to stdout/stderr + tracing_subscriber::registry() + .with(filter) + .with(tracing_subscriber::fmt::layer()) + .init(); + } // Set up panic hook to log panics std::panic::set_hook(Box::new(|panic_info| { @@ -614,7 +652,7 @@ async fn main() -> Result<()> { let args = Args::parse(); // Initialize logging - init_logging(&args.log_level); + init_logging(&args.log_level, args.log_file.as_deref()); info!("Record Daemon v{} starting", record_daemon::VERSION); diff --git a/tauri-app/src-tauri/src/daemon_ipc.rs b/tauri-app/src-tauri/src/daemon_ipc.rs index 979957c..ae9da43 100644 --- a/tauri-app/src-tauri/src/daemon_ipc.rs +++ b/tauri-app/src-tauri/src/daemon_ipc.rs @@ -314,6 +314,20 @@ fn find_daemon_binary() -> Option { None } +/// Get the log file path for the daemon. +/// +/// Returns a path in the app's data directory. +fn get_log_file_path() -> Result { + let data_dir = directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon") + .ok_or_else(|| "Failed to get app data directory".to_string())?; + + let log_dir = data_dir.data_dir().join("logs"); + std::fs::create_dir_all(&log_dir) + .map_err(|e| format!("Failed to create log directory: {}", e))?; + + Ok(log_dir.join("daemon.log")) +} + /// Spawn the record-daemon as a detached background process. /// /// Returns `Ok(())` if the process was successfully spawned. @@ -321,10 +335,13 @@ fn spawn_daemon() -> Result<(), String> { let binary = find_daemon_binary() .ok_or_else(|| "record-daemon binary not found".to_string())?; + let log_file = get_log_file_path()?; eprintln!("[daemon] Spawning record-daemon: {}", binary.display()); + eprintln!("[daemon] Log file: {}", log_file.display()); let mut cmd = std::process::Command::new(&binary); cmd.arg("--foreground"); + cmd.arg("--log-file").arg(&log_file); // Set the working directory to the daemon binary's directory so it can // find its bundled resources (OBS plugins, etc.) relative to itself. @@ -536,3 +553,25 @@ pub async fn daemon_shutdown() -> Result { Err(response.error.unwrap_or_else(|| "Unknown error".to_string())) } } + +/// Get the daemon log file path. +#[tauri::command] +pub fn daemon_get_log_path() -> Result { + get_log_file_path() + .map(|p| p.to_string_lossy().to_string()) +} + +/// Read the daemon log file. +/// +/// Returns the last `lines` lines of the log file (default 100, max 1000). +#[tauri::command] +pub fn daemon_read_logs(lines: Option) -> Result { + let log_path = get_log_file_path()?; + let max_lines = lines.unwrap_or(100).min(1000); + + let content = std::fs::read_to_string(&log_path) + .map_err(|e| format!("Failed to read log file: {}", e))?; + + let lines: Vec<&str> = content.lines().rev().take(max_lines).collect(); + Ok(lines.into_iter().rev().collect::>().join("\n")) +} diff --git a/tauri-app/src-tauri/src/lib.rs b/tauri-app/src-tauri/src/lib.rs index 1e7a013..62c4fac 100644 --- a/tauri-app/src-tauri/src/lib.rs +++ b/tauri-app/src-tauri/src/lib.rs @@ -439,6 +439,8 @@ pub fn run() { daemon_ipc::daemon_start_recording, daemon_ipc::daemon_stop_recording, daemon_ipc::daemon_shutdown, + daemon_ipc::daemon_get_log_path, + daemon_ipc::daemon_read_logs, ]) .run(tauri::generate_context!()) .expect("error while running tauri application");