diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e1cda6..c7082016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ the frozen-backend fallback mirror it for their toolchains. **Highlights** - A bare 500 report now names the backend error class, so two unrelated faults stop filing the same issue (#1773) +- The first-run install log is kept on disk instead of vanishing with the setup screen (#1847) - `bun run desktop` reclaims port 3900 from a backend the app itself left running, instead of refusing to start (#1974) - A dictation shortcut another app already owns now says so, instead of silently doing nothing (#1858) - Quitting on Windows is no longer reported as a crash on the next launch (#1898) diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index ecbb07d1..43a0e06e 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -963,6 +963,28 @@ repair is required. **Linked issue:** [#1590](https://github.com/debpalash/VoiceStudio/issues/1590) + +## Reading the first-run install log after setup finishes + +The Activity panel on the first-run screen shows the install as it happens, and +that screen closes the moment setup succeeds — so it is not where you go +afterwards to check what was installed, or to attach the log to a bug report. + +The same lines are written to **`bootstrap.log`**, beside the backend logs: + +| Platform | Location | +|---|---| +| macOS | `~/Library/Logs/OmniVoice/bootstrap.log` | +| Windows | `%LOCALAPPDATA%\OmniVoice\Logs\bootstrap.log` | +| Linux | `~/.local/state/OmniVoice/bootstrap.log` | + +`OMNIVOICE_LOG_DIR` moves it, along with the other logs. + +It covers the current run only — it is truncated when a bootstrap starts, so a +retry replaces the previous attempt rather than appending to it. If you need +the log from an attempt that has already been superseded, copy it before +retrying. + ## RTX 50-series (Blackwell, `sm_120`): backend never starts **Symptom.** The desktop app stays on "starting backend", `/health` returns 503, diff --git a/frontend/src-tauri/src/bootstrap.rs b/frontend/src-tauri/src/bootstrap.rs index dd150fdf..12975f89 100644 --- a/frontend/src-tauri/src/bootstrap.rs +++ b/frontend/src-tauri/src/bootstrap.rs @@ -119,6 +119,44 @@ pub struct LogPayload { pub line: String, } +/// Where the first-run log is kept so it outlives the splash. +/// +/// The splash is the ONLY surface with a Show/Copy affordance for these +/// lines, and it unmounts the moment the stage flips to ready — so on a +/// successful first run the whole install log was gone for good, with no +/// pause and nowhere to retrieve it (#1847). A user who wanted to check what +/// had just been installed, or hand it to a bug report, had nothing. +/// +/// Sits beside backend.log so everything about a run is in one directory. +fn bootstrap_log_path() -> PathBuf { + crate::backend::backend_log_path().with_file_name("bootstrap.log") +} + +/// Truncate once per process, then append. +/// +/// A bootstrap is a single episode, and the interesting question is always +/// "what happened THIS time" — an ever-growing file would bury that and grow +/// without bound across retries. Truncating on the first write of the process +/// keeps it to the current run without needing a hook on every restart path. +static BOOTSTRAP_LOG_STARTED: std::sync::OnceLock<()> = std::sync::OnceLock::new(); + +fn append_bootstrap_log(stage: &str, line: &str) { + use std::io::Write; + let path = bootstrap_log_path(); + let fresh = BOOTSTRAP_LOG_STARTED.set(()).is_ok(); + let opened = fs::OpenOptions::new() + .create(true) + .write(true) + .append(!fresh) + .truncate(fresh) + .open(&path); + // Best effort throughout: a log we cannot write must never take the + // bootstrap down with it. + if let Ok(mut file) = opened { + let _ = writeln!(file, "[{stage}] {line}"); + } +} + pub fn emit_log(app: &tauri::AppHandle, stage: &str, line: &str) { let payload = LogPayload { stage: stage.to_string(), line: line.to_string() }; // Buffer the log so the frontend can backfill on mount. @@ -127,6 +165,9 @@ pub fn emit_log(app: &tauri::AppHandle, stage: &str, line: logs.push(payload.clone()); } } + // Persist before emitting: the in-memory buffer and the event both die + // with the splash, the file does not. + append_bootstrap_log(stage, line); let _ = app.emit("bootstrap-log", payload); } @@ -4473,6 +4514,48 @@ mod code_fingerprint_tests { assert_ne!(both, backend_only); } + // #1847: the splash is the only surface with a Show/Copy affordance for + // the first-run log, and it unmounts the moment the stage flips to ready, + // so on a successful install the whole log was gone for good. It is + // written beside backend.log now. + #[test] + fn bootstrap_log_sits_beside_the_backend_log() { + // One directory for everything about a run, so a bug report does not + // have to hunt in two places. + let bootstrap = bootstrap_log_path(); + let backend = crate::backend::backend_log_path(); + assert_eq!(bootstrap.parent(), backend.parent()); + assert_eq!(bootstrap.file_name().unwrap(), "bootstrap.log"); + } + + #[test] + fn append_bootstrap_log_writes_the_stage_and_line() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("bootstrap.log"); + // Exercise the same write shape the helper uses, against a path we + // control: the helper itself resolves a per-OS location, and a test + // that redirected that would be testing the redirection. + use std::io::Write; + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + .unwrap(); + writeln!(file, "[{}] {}", "installing_deps", "Collecting torch").unwrap(); + drop(file); + + let body = fs::read_to_string(&path).unwrap(); + assert!(body.contains("[installing_deps] Collecting torch"), "{body}"); + } + + #[test] + fn append_bootstrap_log_never_panics_on_an_unwritable_path() { + // Best effort by contract: a log we cannot write must not take the + // bootstrap down with it. + append_bootstrap_log("checking", "a line"); + } + // #1898: on Windows the backend is force-terminated with no graceful // phase, so it never clears its own run sentinel and every deliberate // quit was reported as a crash on the next launch. The shell retires the