Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions src/core/stdioGuard.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
12 changes: 12 additions & 0 deletions src/core/stdioGuard.ts
Original file line number Diff line number Diff line change
@@ -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 {}
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
1 change: 1 addition & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading