Skip to content

Commit e38d454

Browse files
fix: 修复 Windows Node.js 构建产物因 stdin.ref() 泄漏导致进程挂起 (claude-code-best#353)
startCapturingEarlyInput() 调用 stdin.ref() 后,如果 Ink 未能接管 (如 raw mode 不支持或 setup 阶段异常),unref() 永远不会被调用, 导致 Node.js 事件循环无法退出。修复包括: - stopCapturingEarlyInput() 中补充 stdin.unref() 调用 - 新增 10s 安全阀定时器自动清理 leaked ref() - Ink App.componentWillUnmount 兜底 unref() 非 TTY stdin Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e0c8e9d commit e38d454

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

packages/@ant/ink/src/components/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,15 @@ export default class App extends PureComponent<Props, State> {
286286
// ignore calling setRawMode on an handle stdin it cannot be called
287287
if (this.isRawModeSupported()) {
288288
this.handleSetRawMode(false)
289+
} else {
290+
// Even when raw mode was never enabled (e.g. non-TTY stdin on
291+
// Windows Node.js), ensure stdin is unref'd so the process can
292+
// exit. earlyInput may have called ref() before Ink mounted.
293+
try {
294+
this.props.stdin.unref()
295+
} catch {
296+
// stdin may already be destroyed
297+
}
289298
}
290299
}
291300

src/utils/earlyInput.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ let earlyInputBuffer = ''
1919
let isCapturing = false
2020
// Reference to the readable handler so we can remove it later
2121
let readableHandler: (() => void) | null = null
22+
// Safety valve: auto-cleanup after timeout so stdin.ref() never leaks
23+
let safetyTimer: ReturnType<typeof setTimeout> | null = null
2224

2325
/**
2426
* Start capturing stdin data early, before the REPL is initialized.
@@ -60,6 +62,20 @@ export function startCapturingEarlyInput(): void {
6062
}
6163

6264
process.stdin.on('readable', readableHandler)
65+
66+
// Safety valve: if Ink never takes over within 10s (e.g. setup dialog
67+
// stalls, or an error prevents Ink mount on Windows), unref stdin so
68+
// the process doesn't hang forever. The REPL's Ink App normally calls
69+
// consumeEarlyInput() → stopCapturingEarlyInput() long before this.
70+
safetyTimer = setTimeout(() => {
71+
if (isCapturing) {
72+
stopCapturingEarlyInput()
73+
}
74+
}, 10_000)
75+
// Don't let the timer itself keep the event loop alive
76+
if (safetyTimer && typeof safetyTimer === 'object' && 'unref' in safetyTimer) {
77+
safetyTimer.unref()
78+
}
6379
} catch {
6480
// If we can't set raw mode, just silently continue without early capture
6581
isCapturing = false
@@ -172,14 +188,34 @@ export function stopCapturingEarlyInput(): void {
172188

173189
isCapturing = false
174190

191+
// Clear safety timer
192+
if (safetyTimer) {
193+
clearTimeout(safetyTimer)
194+
safetyTimer = null
195+
}
196+
175197
if (readableHandler) {
176198
process.stdin.removeListener('readable', readableHandler)
177199
readableHandler = null
178200
}
179201

180-
// Don't reset stdin state - the REPL's Ink App will manage stdin state.
181-
// If we call setRawMode(false) here, it can interfere with the REPL's
182-
// own stdin setup which happens around the same time.
202+
// Undo the ref() from startCapturingEarlyInput so the event loop isn't
203+
// kept alive if Ink never takes over (e.g. raw mode unsupported on
204+
// Windows Node.js, or an error during setup). Ink's own
205+
// handleSetRawMode(true) calls stdin.ref() again, and its
206+
// handleSetRawMode(false) / unmount path calls stdin.unref(), so this
207+
// unref is safe even when Ink does take over — the two ref/unref calls
208+
// balance out.
209+
try {
210+
process.stdin.unref()
211+
} catch {
212+
// stdin may already be destroyed
213+
}
214+
215+
// Don't reset setRawMode here — Ink's App.handleSetRawMode(true)
216+
// calls stopCapturingEarlyInput() synchronously and then immediately
217+
// calls setRawMode(true) + ref() on the same stdin, so toggling it
218+
// off here would add a visible flicker on Windows.
183219
}
184220

185221
/**

0 commit comments

Comments
 (0)