diff --git a/CHANGELOG.md b/CHANGELOG.md index 52acbc0..e0e1fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 which is why tests missed it); `scripts/packaged-video-check.sh` now indexes videos with the packaged app before each release. Thanks to the V2EX user who reported it. After updating, videos that failed are re-indexed automatically on the next launch. +- Launching Vixel from a terminal and then closing that terminal no longer pops up a + "JavaScript error occurred in the main process" dialog (`write EPIPE`). ## [0.5.0] - 2026-10-15 diff --git a/src/core/stdioGuard.test.ts b/src/core/stdioGuard.test.ts new file mode 100644 index 0000000..e2aa9db --- /dev/null +++ b/src/core/stdioGuard.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { spawn } from 'child_process' +import { join } from 'path' + +/** 子进程不停 console.log;父进程读到 1 字节就关掉管道 —— 与"关掉启动它的终端"同一情形 */ +function runChild(withGuard: boolean): Promise<{ code: number | null; stderr: string }> { + const guard = join(__dirname, 'stdioGuard.ts') + const script = `${withGuard ? `require(${JSON.stringify(guard)});` : ''} + let i = 0; const t = setInterval(() => { console.log('line ' + i++); if (i > 200) { clearInterval(t); process.exit(0) } }, 2)` + return new Promise((resolve) => { + const child = spawn(process.execPath, ['--experimental-strip-types', '--no-warnings', '-e', script], { stdio: ['ignore', 'pipe', 'pipe'] }) + let stderr = '' + child.stderr.on('data', (d) => { stderr += d }) + child.stdout.once('data', () => child.stdout.destroy()) + child.on('close', (code) => resolve({ code, stderr })) + }) +} + +describe('stdioGuard', () => { + it('a closed stdout pipe (EPIPE) no longer crashes the process', async () => { + const guarded = await runChild(true) + expect(guarded.stderr).not.toMatch(/EPIPE/) + expect(guarded.code).toBe(0) + }) + it('without the guard the same situation is an uncaught EPIPE (the test reproduces the bug)', async () => { + const bare = await runChild(false) + expect(bare.stderr).toMatch(/EPIPE/) + expect(bare.code).not.toBe(0) + }) +}) diff --git a/src/core/stdioGuard.ts b/src/core/stdioGuard.ts new file mode 100644 index 0000000..8191cf7 --- /dev/null +++ b/src/core/stdioGuard.ts @@ -0,0 +1,12 @@ +/** + * stdout / stderr 的读端没了(从终端启动后关掉终端、输出管道给了提前退出的 head……)时, + * 下一次 console.log 会抛 EPIPE。没人监听流的 error 事件,Node 就把它当未捕获异常 —— + * 桌面版弹出"主进程 JavaScript 错误"对话框。日志写不出去不该影响应用本身:EPIPE 吞掉,其他错误照旧。 + */ +for (const stream of [process.stdout, process.stderr]) { + stream?.on?.('error', (err: NodeJS.ErrnoException) => { + if (err.code !== 'EPIPE') throw err + }) +} + +export {} diff --git a/src/main/index.ts b/src/main/index.ts index e038038..f5626e3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,5 @@ import '../core/inference/noTelemetry' // 必须第一个:在加载 onnxruntime 之前关掉它的遥测 +import '../core/stdioGuard' // 输出管道断了(关掉启动它的终端)时 console.log 不再弹错误框 import { app, shell, BrowserWindow, ipcMain, dialog, protocol, net, nativeTheme, session } from 'electron' import { pathToFileURL } from 'url' import { join } from 'path' diff --git a/src/server/index.ts b/src/server/index.ts index 568e450..ab5e95a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,6 +10,7 @@ */ import './bootstrap' // 必须第一个 import:普通 Node 下重定向 better-sqlite3 到 Node ABI 构建 import '../core/inference/noTelemetry' // 在加载 onnxruntime 之前关掉它的遥测 +import '../core/stdioGuard' // 日志管道断了不让进程崩 import { createServer, type IncomingMessage, type ServerResponse } from 'http' import { join } from 'path' import { existsSync, mkdirSync } from 'fs'