From 00e08cec3087025b9b777ceb869c00c4fbbf1ea0 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Mon, 21 Sep 2026 00:09:56 -0400 Subject: [PATCH 1/2] fix(desktop): a Linux SIGTRAP leaves its FATAL line and a minidump behind Every Chromium CHECK/LOG(FATAL) traps at the same instruction (the ImmediateCrash tail of logging::LogMessage::HandleFatal), so the cores collected for #100573 all share one address and none say which check fired. The message goes to stderr, which the .desktop entry and the Omarchy wrapper both discard, and the app never started Crashpad. On Linux, route Chromium's log (ERROR and above) to HERMES_HOME/logs/desktop-chromium.log and keep local minidumps; nothing is uploaded. Child processes inherit the switches, so zygote/GPU checks land in the same file. Refs #100573 --- .../electron/linux-crash-diagnostics.test.ts | 33 +++++++++++++++ .../electron/linux-crash-diagnostics.ts | 41 +++++++++++++++++++ apps/desktop/electron/main.ts | 18 ++++++++ 3 files changed, 92 insertions(+) create mode 100644 apps/desktop/electron/linux-crash-diagnostics.test.ts create mode 100644 apps/desktop/electron/linux-crash-diagnostics.ts diff --git a/apps/desktop/electron/linux-crash-diagnostics.test.ts b/apps/desktop/electron/linux-crash-diagnostics.test.ts new file mode 100644 index 0000000000..68d13b4511 --- /dev/null +++ b/apps/desktop/electron/linux-crash-diagnostics.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import path from 'node:path' + +import { test } from 'vitest' + +import { linuxCrashDiagnostics } from './linux-crash-diagnostics' + +// Regression for #100573: the Linux shell died with SIGTRAP at Chromium's +// shared fatal-handler address and no launcher kept the FATAL message. The +// fix is not a guess at the cause; it is making the next crash legible. + +test('on linux, fatal Chromium output lands in a file under the Hermes logs dir', () => { + const plan = linuxCrashDiagnostics('/home/u/.hermes/logs', 'linux') + + assert.ok(plan) + + const switches = new Map(plan.switches) + + assert.equal(switches.get('enable-logging'), 'file') + + const logFile = switches.get('log-file') + + assert.ok(logFile) + assert.equal(path.dirname(logFile), '/home/u/.hermes/logs') + // FATAL (3) must survive the level filter; anything stricter would drop it. + assert.ok(Number(switches.get('log-level')) <= 3) + assert.equal(plan.crashReporter.uploadToServer, false) +}) + +test('other platforms get no Chromium logging switches and no crash reporter', () => { + assert.equal(linuxCrashDiagnostics('/Users/u/.hermes/logs', 'darwin'), null) + assert.equal(linuxCrashDiagnostics('C:\\Users\\u\\.hermes\\logs', 'win32'), null) +}) diff --git a/apps/desktop/electron/linux-crash-diagnostics.ts b/apps/desktop/electron/linux-crash-diagnostics.ts new file mode 100644 index 0000000000..abd80ee6a6 --- /dev/null +++ b/apps/desktop/electron/linux-crash-diagnostics.ts @@ -0,0 +1,41 @@ +import path from 'node:path' + +// Every Chromium CHECK/LOG(FATAL) in the shell ends at the same instruction — +// base::ImmediateCrash at the tail of logging::LogMessage::HandleFatal — so a +// core dump alone says "something fatal happened" and nothing about what +// (#100573: ten Linux reports, one shared trap address, zero fatal messages). +// The message itself goes to stderr a moment before the trap, and every Linux +// launcher (.desktop entry, Omarchy's hermes-desktop wrapper) discards stderr. +// Route Chromium's own log to a file next to desktop.log and let Crashpad keep +// local minidumps, so the next crash carries its FATAL line with it. + +export interface LinuxCrashDiagnostics { + /** Chromium command-line switches, applied before `app` is ready. */ + switches: ReadonlyArray + /** `crashReporter.start` options; local database only, nothing leaves the machine. */ + crashReporter: { uploadToServer: false; compress: false } +} + +// Chromium log severities: 0 INFO, 1 WARNING, 2 ERROR, 3 FATAL. ERROR keeps the +// file quiet during normal use (INFO would mirror every renderer console line). +const CHROMIUM_LOG_LEVEL_ERROR = '2' + +export const CHROMIUM_LOG_FILENAME = 'desktop-chromium.log' + +export function linuxCrashDiagnostics( + logsDir: string, + platform: NodeJS.Platform = process.platform +): LinuxCrashDiagnostics | null { + if (platform !== 'linux') { + return null + } + + return { + switches: [ + ['enable-logging', 'file'], + ['log-file', path.join(logsDir, CHROMIUM_LOG_FILENAME)], + ['log-level', CHROMIUM_LOG_LEVEL_ERROR] + ], + crashReporter: { uploadToServer: false, compress: false } + } +} diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index ea3c70a6b3..8eb0ba6544 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -12,6 +12,7 @@ import { app, BrowserWindow, clipboard, + crashReporter, dialog, net as electronNet, webContents as electronWebContents, @@ -268,6 +269,7 @@ import { resolveHudWindowing } from './hud-windowing' import { createIntroRevealWindowController } from './intro-reveal-window' import { isAuthWall, resolveLinkTitle } from './link-title-wall' import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window' +import { linuxCrashDiagnostics } from './linux-crash-diagnostics' import { notifyLauncherWindowRevealed } from './linux-launcher-ready' import { createLocalBackendLifecycle, waitForTeardown } from './local-backend-lifecycle' import { ensureMainWindow } from './main-window-lifecycle' @@ -986,6 +988,22 @@ const DESKTOP_LOG_MAX_BYTES = 10 * 1024 * 1024 const DESKTOP_LOG_BACKUP_COUNT = 3 const DESKTOP_LOG_DISCARD_BYTES = DESKTOP_LOG_MAX_BYTES * 4 const desktopLogBackupPath = n => `${DESKTOP_LOG_PATH}.${n}` + +// #100573: keep the FATAL line and a local minidump for the next Linux SIGTRAP. +// Both must be wired before `app` is ready; the log-file switch is inherited by +// every child process, so a zygote or GPU CHECK lands in the same file. +const CRASH_DIAGNOSTICS = linuxCrashDiagnostics(path.dirname(DESKTOP_LOG_PATH)) + +if (CRASH_DIAGNOSTICS) { + fs.mkdirSync(path.dirname(DESKTOP_LOG_PATH), { recursive: true }) + + for (const [name, value] of CRASH_DIAGNOSTICS.switches) { + app.commandLine.appendSwitch(name, value) + } + + crashReporter.start(CRASH_DIAGNOSTICS.crashReporter) +} + const BOOT_FAKE_MODE = process.env.HERMES_DESKTOP_BOOT_FAKE === '1' const BOOT_FAKE_ERROR = process.env.HERMES_DESKTOP_BOOT_FAKE_ERROR || '' // Automated teardown (Playwright's app.close(), harness scripts) quits with From 274bc7b8f613c299b4f59160bacf8a19010f7003 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Mon, 21 Sep 2026 00:09:56 -0400 Subject: [PATCH 2/2] docs(desktop): where the Linux Chromium log and minidumps live --- website/docs/user-guide/desktop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/user-guide/desktop.md b/website/docs/user-guide/desktop.md index 6d8bcf8b40..07f3e08ac0 100644 --- a/website/docs/user-guide/desktop.md +++ b/website/docs/user-guide/desktop.md @@ -614,6 +614,8 @@ Boot logs land in `HERMES_HOME/logs/desktop.log` (it includes backend output and hermes logs gui -f ``` +On Linux, Chromium's own errors go to `HERMES_HOME/logs/desktop-chromium.log`, and a crash of the shell itself leaves a minidump under the app's `Crashpad/` directory (inside Electron's user-data directory, next to `connection.json`). If the window vanishes with `SIGTRAP` in the journal, the `FATAL:` line in that log names the check that fired; attach it to the bug report. Nothing is uploaded. + Common resets: ```bash