From 6a077ac7a1938e3faa3d2dd555bde1729ff694e3 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Fri, 15 May 2026 23:18:57 +0800 Subject: [PATCH] feat(bridge): tee tracing output to platform log file + log_file_path_str API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge already had a tracing fmt layer writing to stderr but a Flutter desktop app launched from Explorer / RDP has no terminal attached so those records vanish. Add a non-ANSI file appender (best-effort, 4 MiB rotation) at the platform-conventional log path so developers and beta testers can hand-inspect output: * Linux: $XDG_STATE_HOME/app.chanora/chanora_flutter/chanora.log (fallback ~/.local/state/...) * macOS: ~/Library/Logs/app.chanora.chanora_flutter/chanora.log * Windows: %LOCALAPPDATA%\app.chanora\chanora_flutter\logs\chanora.log Expose log_file_path_str() over FRB so the UI can show the path in a 'Save diagnostics' affordance later. Mobile (Android/iOS) returns an empty string — those platforms still rely on logcat / Console.app. No DEC-016 conflict: this is local-only, append-only, never auto-uploaded. The in-memory log sink and export_diagnostics() path are unchanged. The redacting layer still wraps the in-memory sink; the new file appender consumes the same tracing events post-filter. Trigger for this change: a ko-KR Windows 11 tester saw 'BridgeError.invalidCommand(audio not started)' with no way to find the upstream warn record that documents which cpal call failed. Log file is now discoverable without a terminal launch. --- crates/chanora_bridge/src/api.rs | 116 +++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 7231457..c63ccb1 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -92,14 +92,120 @@ pub fn bridge_init() { let fmt_layer = tracing_subscriber::fmt::layer().with_target(true); let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER)); - let _ = tracing_subscriber::registry() - .with(filter) - .with(fmt_layer) - .with(redact_layer) - .try_init(); + + // Also tee tracing output to a rotating file in the OS's + // standard log directory so developers and beta testers can + // hand-inspect output without launching from a terminal. + // The path is platform-conventional: + // * Linux: ~/.local/state/app.chanora/chanora_flutter/chanora.log + // * macOS: ~/Library/Logs/app.chanora.chanora_flutter/chanora.log + // * Windows: %LOCALAPPDATA%\app.chanora\chanora_flutter\logs\chanora.log + // Best-effort: if the directory cannot be created or the + // file cannot be opened, the layer is silently dropped and + // stderr remains the only sink. + let file_writer = open_log_file(); + if let Some(file) = file_writer { + let file_layer = tracing_subscriber::fmt::layer() + .with_target(true) + .with_ansi(false) + .with_writer(std::sync::Mutex::new(file)); + let _ = tracing_subscriber::registry() + .with(filter) + .with(fmt_layer) + .with(file_layer) + .with(redact_layer) + .try_init(); + } else { + let _ = tracing_subscriber::registry() + .with(filter) + .with(fmt_layer) + .with(redact_layer) + .try_init(); + } } info!(target: "chanora_bridge", "bridge initialised"); + if let Some(p) = log_file_path() { + info!(target: "chanora_bridge", path = %p.display(), "log file path"); + } +} + +/// Return the platform-conventional log-file path as a string, or +/// an empty string if the platform does not have one (mobile). +#[frb(sync)] +pub fn log_file_path_str() -> String { + log_file_path() + .map(|p| p.display().to_string()) + .unwrap_or_default() +} + +/// Resolve the platform-conventional log-file path. Returns `None` +/// if the platform conventions can't be honoured (e.g. neither +/// `LOCALAPPDATA` nor `HOME` is set). +fn log_file_path() -> Option { + #[cfg(target_os = "windows")] + { + let base = std::env::var_os("LOCALAPPDATA") + .or_else(|| std::env::var_os("APPDATA"))?; + Some( + std::path::PathBuf::from(base) + .join("app.chanora") + .join("chanora_flutter") + .join("logs") + .join("chanora.log"), + ) + } + #[cfg(target_os = "macos")] + { + let home = std::env::var_os("HOME")?; + Some( + std::path::PathBuf::from(home) + .join("Library") + .join("Logs") + .join("app.chanora.chanora_flutter") + .join("chanora.log"), + ) + } + #[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))] + { + let base = std::env::var_os("XDG_STATE_HOME") + .map(std::path::PathBuf::from) + .or_else(|| { + std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local").join("state")) + })?; + Some( + base.join("app.chanora") + .join("chanora_flutter") + .join("chanora.log"), + ) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + None + } +} + +/// Open the platform log file in append mode, rotating once on +/// startup so each launch begins with a fresh file. Best-effort: +/// returns `None` on any I/O error. +fn open_log_file() -> Option { + let path = log_file_path()?; + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + // Rotate: if the existing file exceeds 4 MiB rename it to .1 so + // we never grow unbounded. One generation is enough for + // debugging; we are not building a real log-rotation system. + if let Ok(meta) = std::fs::metadata(&path) { + if meta.len() > 4 * 1024 * 1024 { + let _ = std::fs::rename(&path, path.with_extension("log.1")); + } + } + std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .ok() } // ---------- DTOs ----------