From 795342724c87605fb4f9fd2f73240a06b393d576 Mon Sep 17 00:00:00 2001 From: RGB-loop <91311486+RGB-loop@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:22:16 +0800 Subject: [PATCH 1/2] fix: closed stdout/stderr pipe (EPIPE) no longer raises a main-process error dialog Launching Vixel from a terminal and then closing it (or piping its output to a reader that exits) made the next console.log throw EPIPE as an uncaught exception. Swallow EPIPE on stdout/stderr in the desktop main process and the server; regression test reproduces the crash in a child process. --- src/core/stdioGuard.test.ts | 30 ++++++++++++++++++++++++++++++ src/core/stdioGuard.ts | 12 ++++++++++++ src/main/index.ts | 1 + src/server/index.ts | 1 + 4 files changed, 44 insertions(+) create mode 100644 src/core/stdioGuard.test.ts create mode 100644 src/core/stdioGuard.ts 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' From 38884f2594ca0335208e0ba5a8843ec697dd6f93 Mon Sep 17 00:00:00 2001 From: RGB-loop <91311486+RGB-loop@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:44:15 +0800 Subject: [PATCH 2/2] docs(changelog): EPIPE fix in 0.5.1 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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