mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
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.
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
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)
|
|
})
|
|
})
|