//! Local diagnostics in `/logs`: `host.log` (this process: start, saved state, sidecar, panics) and //! `webview.log` (uncaught errors of the window, sent through the `log_webview` command). Each file is rotated at //! MAX_BYTES (one older `.1.log` kept). Every line goes through `sanitize`: the home folder shows as `~`, and keys, //! tokens, long secrets and e-mail addresses are replaced, so a log can be attached to a bug report as it is. use std::fs::{self, OpenOptions}; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::{Mutex, OnceLock}; pub const DIR: &str = "logs"; pub const HOST: &str = "host.log"; pub const WEBVIEW: &str = "webview.log"; pub const MAX_BYTES: u64 = 512 * 1024; /// Longest single entry kept (the rest is cut). pub const MAX_ENTRY: usize = 16 * 1024; struct Logs { dir: PathBuf, home: Option, lock: Mutex<()>, } static LOGS: OnceLock = OnceLock::new(); /// Sets the log folder once (the first call wins). pub fn init(data_dir: &Path, home: Option) { let _ = LOGS.set(Logs { dir: data_dir.join(DIR), home, lock: Mutex::new(()) }); } pub fn dir() -> Option { LOGS.get().map(|l| l.dir.clone()) } /// One line in host.log (no-op before `init`). pub fn host(level: &str, message: &str) { write(HOST, "host", level, message); } /// One entry in webview.log. pub fn webview(level: &str, message: &str) { write(WEBVIEW, "webview", level, message); } fn write(file: &str, source: &str, level: &str, message: &str) { let Some(l) = LOGS.get() else { return }; let _g = l.lock.lock(); let level = match level { "error" | "warn" | "info" | "debug" => level, _ => "info", }; let text = sanitize(message, l.home.as_deref()); let line = entry(&crate::store::now_iso(), source, level, &text); let _ = append(&l.dir, file, &line); } /// `