mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
fix(desktop): stop a failed log write from crashing the main process
Launching from Finder or the Dock leaves the main process with stdio that has no reader. Writing there fails asynchronously with EPIPE from inside the stream machinery, and with no `error` listener Node escalates it to an uncaught exception — which Electron shows as "A JavaScript error occurred in the main process". This is reachable in ordinary use: the sidecar exit handler logs a line every time a sidecar dies, so any crashed or killed sidecar could raise that dialog. Guarding that one call site would not help, since the main process has ~25 console call sites and all of them write to the same two streams. A try/catch at the call site cannot help either, because the throw happens off-stack. Install the guard on stdout/stderr before anything logs. Losing a diagnostic line is acceptable; killing the user's session over one is not — real diagnostics already persist to a file through appendHostDiagnostic.
This commit is contained in:
parent
eaeab84b16
commit
ec5094fb57
@ -35,6 +35,7 @@ import {
|
||||
setAppMode,
|
||||
} from './services/appMode'
|
||||
import { installMacOsChromiumKeychainPromptGuard } from './services/keychain'
|
||||
import { installStdioWriteFailureGuards } from './services/stdioGuards'
|
||||
import { applyWindowsAppUserModelId } from './services/appIdentity'
|
||||
import { installMainWindowNavigationGuards, installPreviewNavigationGuards } from './services/navigationGuards'
|
||||
import { installPreviewCleanupOnRendererNavigation } from './services/previewLifecycle'
|
||||
@ -90,6 +91,9 @@ const traceWindows = new Map<string, BrowserWindow>()
|
||||
let isQuitting = false
|
||||
let trayController: TrayController | null = null
|
||||
|
||||
// Must run before anything logs: a Finder/Dock launch inherits unreadable
|
||||
// stdio, and an unguarded write failure there surfaces as a crash dialog.
|
||||
installStdioWriteFailureGuards()
|
||||
installMacOsChromiumKeychainPromptGuard(app)
|
||||
|
||||
function appRoot() {
|
||||
|
||||
44
desktop/electron/services/stdioGuards.test.ts
Normal file
44
desktop/electron/services/stdioGuards.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { installStdioWriteFailureGuards } from './stdioGuards'
|
||||
|
||||
const makeStream = () => new EventEmitter()
|
||||
|
||||
describe('Electron stdio write failure guards', () => {
|
||||
it('keeps an EPIPE write failure from escalating to an uncaught exception', () => {
|
||||
const stdout = makeStream()
|
||||
installStdioWriteFailureGuards([stdout])
|
||||
|
||||
const epipe: NodeJS.ErrnoException = Object.assign(new Error('write EPIPE'), {
|
||||
code: 'EPIPE',
|
||||
})
|
||||
|
||||
// Without a listener, EventEmitter rethrows 'error' — exactly how Node
|
||||
// turns a failed console.log into the Electron crash dialog.
|
||||
expect(() => stdout.emit('error', epipe)).not.toThrow()
|
||||
})
|
||||
|
||||
it('guards stderr as well as stdout', () => {
|
||||
const stdout = makeStream()
|
||||
const stderr = makeStream()
|
||||
|
||||
expect(installStdioWriteFailureGuards([stdout, stderr])).toBe(2)
|
||||
expect(() => stderr.emit('error', new Error('write EPIPE'))).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not stack duplicate listeners when installed twice', () => {
|
||||
const stdout = makeStream()
|
||||
|
||||
expect(installStdioWriteFailureGuards([stdout])).toBe(1)
|
||||
expect(installStdioWriteFailureGuards([stdout])).toBe(0)
|
||||
expect(stdout.listenerCount('error')).toBe(1)
|
||||
})
|
||||
|
||||
it('defaults to the real process stdio streams', () => {
|
||||
// Idempotent by design, so calling it here cannot disturb the test runner's
|
||||
// own streams beyond the single no-op listener the app installs at startup.
|
||||
expect(installStdioWriteFailureGuards()).toBeGreaterThanOrEqual(0)
|
||||
expect(process.stdout.listenerCount('error')).toBeGreaterThan(0)
|
||||
expect(process.stderr.listenerCount('error')).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
38
desktop/electron/services/stdioGuards.ts
Normal file
38
desktop/electron/services/stdioGuards.ts
Normal file
@ -0,0 +1,38 @@
|
||||
type ErrorEmittingStream = {
|
||||
on(event: 'error', listener: (error: NodeJS.ErrnoException) => void): unknown
|
||||
}
|
||||
|
||||
const guarded = new WeakSet<ErrorEmittingStream>()
|
||||
|
||||
/**
|
||||
* Keep a failed log write from taking the whole app down.
|
||||
*
|
||||
* When the app is launched from Finder or the Dock, the main process inherits
|
||||
* stdio that has no reader. Writing to it fails asynchronously with EPIPE from
|
||||
* inside the stream machinery, so a plain `console.log` cannot be defended with
|
||||
* try/catch at the call site — and with no `error` listener on the stream, Node
|
||||
* escalates it to an uncaught exception. Electron then shows the user a
|
||||
* "A JavaScript error occurred in the main process" dialog.
|
||||
*
|
||||
* This is reachable from ordinary operation: the sidecar exit handler logs a
|
||||
* line whenever a sidecar dies (`serverRuntime.ts`), so any crashed or killed
|
||||
* sidecar could surface that dialog. Guarding the single noisiest call site
|
||||
* would not help — the main process has ~25 console call sites, and every one
|
||||
* of them writes to these two streams.
|
||||
*
|
||||
* Losing a diagnostic line is an acceptable outcome; killing the user's session
|
||||
* over one never is. Real diagnostics are persisted separately through
|
||||
* `appendHostDiagnostic`, which writes to a file rather than to stdio.
|
||||
*/
|
||||
export function installStdioWriteFailureGuards(
|
||||
streams: ErrorEmittingStream[] = [process.stdout, process.stderr],
|
||||
): number {
|
||||
let installed = 0
|
||||
for (const stream of streams) {
|
||||
if (guarded.has(stream)) continue
|
||||
stream.on('error', () => {})
|
||||
guarded.add(stream)
|
||||
installed += 1
|
||||
}
|
||||
return installed
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user