merge origin/main (2 commits) into ethie/pm-clean
This commit is contained in:
33
apps/desktop/electron/linux-crash-diagnostics.test.ts
Normal file
33
apps/desktop/electron/linux-crash-diagnostics.test.ts
Normal 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)
|
||||
})
|
||||
41
apps/desktop/electron/linux-crash-diagnostics.ts
Normal file
41
apps/desktop/electron/linux-crash-diagnostics.ts
Normal 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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user