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 6318ecc7dd..2cb9f80a08 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, @@ -281,6 +282,7 @@ import type { InstallStamp } from './install-stamp' 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 { registerMachineProfile } from './machine-profile' @@ -871,6 +873,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: number) => `${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 diff --git a/website/docs/user-guide/desktop.md b/website/docs/user-guide/desktop.md index e8b087b679..e5d05dfb1a 100644 --- a/website/docs/user-guide/desktop.md +++ b/website/docs/user-guide/desktop.md @@ -620,6 +620,8 @@ For a canonical source installation, Desktop checks and runs the installation launcher. PM selects its interpreter and dependency generation. A missing bootstrap marker does not force installation when that launcher works. +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. + If Python dependencies are damaged, run the installation's `hermes pm repair`. Then restart Desktop. Do not delete guessed `venv` paths or PM facts. For damaged application files, repair through the