merge origin/main (2 commits) into ethie/pm-clean

This commit is contained in:
ethernet
2026-09-21 09:25:04 -04:00
4 changed files with 94 additions and 0 deletions

View File

@@ -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)
})

View File

@@ -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<readonly [name: string, value: string]>
/** `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 }
}
}

View File

@@ -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

View File

@@ -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