Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions docs/architecture/issue-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Report issue, a tab in Settings' App group below About, and a dialog opened from

## The session log

- The main process keeps the newest 2,000 lines of this run in memory, each stamped with the local time and its source: every sidecar's stdout and stderr, by sidecar name, and its stop or crash; the window's console warnings and errors (`renderer`); electron-updater's own log (`updater`). Nothing is written to disk, so the log ends with the app.
- The main process keeps the newest 2,000 lines of this run in memory, each stamped with the local time and its source: every sidecar's stdout and stderr, by sidecar name, and its stop or crash; the window's console warnings and errors (`renderer`); electron-updater's own log (`updater`); the main process's own warnings (`main`), such as a preference file it could not read or write. Nothing is written to disk, so the log ends with the app.
- Lines are cleaned as they arrive: colour codes stripped, the home directory replaced by `~` (as written, with forward slashes, and escaped as in a Python repr), anything past 2,000 characters cut. uvicorn's access lines for successful `GET`s are dropped, since the app polls the API; failed requests and writes stay. Each kept line is a copy of its own, so a cut line never keeps the whole text it came from in memory.
- Sidecar output is also mirrored to the terminal that started the app, by a write that never holds up the main process ([`supervisor.ts`](../../surfsense_local/electron/src/main/sidecars/supervisor.ts)): on Windows a write to a piped stdout blocks, so a terminal that stopped reading froze the window, and the mirror writes from libuv's pool there instead. Elsewhere it uses Node's own stdout and stderr, which queue a full pipe. Past 1 MB the terminal has not taken, mirrored output is dropped; the session log keeps every line. A sidecar's stop or crash note is written at once, so the last one still prints when the app exits right after it.
- The renderer reads it through `window.surfsense.sessionLog.read()`. `session-log:read` answers only the app's own window. The dialog polls it every second while open, and follows the newest line unless the user has scrolled up.
Expand All @@ -23,7 +23,3 @@ Report issue, a tab in Settings' App group below About, and a dialog opened from
user-facing feature. The report carries the sidecar's internal name and exit
code; requested stops produce no crash event.
- The dialog is an app dialog, like the egress prompt: opened over another dialog, say from Help while Settings is open, it opens as that dialog's nested dialog. Its draft survives a close, and a move to another dialog.

## Known gaps

- The main process's own warnings, such as a preference file it could not read, still go to the terminal only.
6 changes: 3 additions & 3 deletions surfsense_local/electron/src/main/i18n/locale-prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { app } from "electron"

import { isLocale, PSEUDO_LOCALE } from "./locales.ts"
import type { LocalePreference } from "./resolve-locale.ts"
import { warnMain } from "../session-log/main-warn.ts"
import { sessionLog } from "../session-log/session-log.ts"

function prefsPath(): string {
return join(app.getPath("userData"), "locale-prefs.json")
Expand Down Expand Up @@ -36,8 +38,6 @@ export function saveLocalePreference(locale: LocalePreference): void {
renameSync(temporary, path)
} catch (error) {
// Best-effort, as theme-prefs.ts: a lost write falls back to "system".
process.stderr.write(
`[main] failed to save locale preference: ${String(error)}\n`
)
warnMain(sessionLog, `failed to save locale preference: ${String(error)}`)
}
}
5 changes: 3 additions & 2 deletions surfsense_local/electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { loadLocalePreference } from "./i18n/locale-prefs.ts"
import { registerLocaleHandlers } from "./i18n/locale-ipc.ts"
import { registerAboutHandlers } from "./about/about-ipc.ts"
import { sessionLog } from "./session-log/session-log.ts"
import { warnMain } from "./session-log/main-warn.ts"
import { registerSessionLogHandlers } from "./session-log/session-log-ipc.ts"
import { appMenu } from "./menu/app-menu.ts"
import { helpMenu } from "./menu/help-menu.ts"
Expand Down Expand Up @@ -116,7 +117,7 @@ let shuttingDown = false
let stopDocxSnapshots: (() => void) | null = null

function onSidecarCrash(name: string, code: number | null): void {
process.stderr.write(`[main] sidecar ${name} crashed (code=${code})\n`)
warnMain(sessionLog, `sidecar ${name} crashed (code=${code})`)
// best-effort: let the renderer show an error instead of hanging
mainWindow?.webContents.send("sidecar:crashed", {
name,
Expand Down Expand Up @@ -743,7 +744,7 @@ function main(): void {
})
})
.catch((err: unknown) => {
process.stderr.write(`failed to start: ${String(err)}\n`)
warnMain(sessionLog, `failed to start: ${String(err)}`)
void shutdown().finally(() => app.exit(1))
})

Expand Down
11 changes: 11 additions & 0 deletions surfsense_local/electron/src/main/session-log/main-warn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { SessionLog } from "./session-log.ts"

/**
* A warning from the main process itself: the terminal in development, and
* the session log so Report issue carries it. The source is always "main";
* callers pass the text without a prefix.
*/
export function warnMain(log: SessionLog, text: string): void {
process.stderr.write(`[main] ${text}\n`)
log.append("main", text)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,30 @@ import { setFlagsFromString } from "node:v8"
import { runInNewContext } from "node:vm"

import { createSessionLog } from "./session-log.ts"
import { warnMain } from "./main-warn.ts"

const at = () => new Date(2026, 8, 25, 18, 21, 3)

test("a main warning goes to the terminal and the log tagged as main", () => {
const log = createSessionLog({ home: "/home/ada", now: at })
const written: string[] = []
const original = process.stderr.write
process.stderr.write = ((chunk: string) => {
written.push(chunk)
return true
}) as typeof process.stderr.write
try {
warnMain(log, "failed to save theme preference: boom")
} finally {
process.stderr.write = original
}

assert.deepEqual(written, ["[main] failed to save theme preference: boom\n"])
assert.deepEqual(log.lines(), [
"18:21:03 [main] failed to save theme preference: boom",
])
})

test("prefixes every line with the time and the process that wrote it", () => {
const log = createSessionLog({ home: "/home/ada", now: at })

Expand Down
7 changes: 4 additions & 3 deletions surfsense_local/electron/src/main/theme-prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { join } from "node:path"

import { app } from "electron"

import { warnMain } from "./session-log/main-warn.ts"
import { sessionLog } from "./session-log/session-log.ts"

// Mirrors the Theme type in frontend/src/components/theme-provider.tsx.
export type ThemePreference = "dark" | "light" | "system"

Expand Down Expand Up @@ -40,8 +43,6 @@ export function saveThemePreference(theme: ThemePreference): void {
} catch (error) {
// ponytail: best-effort, same as window-state.ts; a stale/missing pref
// just falls back to "system" next launch.
process.stderr.write(
`[main] failed to save theme preference: ${String(error)}\n`,
)
warnMain(sessionLog, `failed to save theme preference: ${String(error)}`)
}
}
7 changes: 4 additions & 3 deletions surfsense_local/electron/src/main/window-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { join } from "node:path"

import { app, type BrowserWindow, type Rectangle, screen } from "electron"

import { warnMain } from "./session-log/main-warn.ts"
import { sessionLog } from "./session-log/session-log.ts"

export type WindowState = {
bounds: Rectangle
maximized: boolean
Expand Down Expand Up @@ -89,8 +92,6 @@ export function saveWindowState(win: BrowserWindow): void {
renameSync(temporary, path)
} catch (error) {
// ponytail: window state is best-effort; failing to save must never block exit.
process.stderr.write(
`[main] failed to save window state: ${String(error)}\n`,
)
warnMain(sessionLog, `failed to save window state: ${String(error)}`)
}
}
Loading