mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: 修复 Windows Node.js 构建产物因 stdin.ref() 泄漏导致进程挂起 (#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>
This commit is contained in:
@@ -286,6 +286,15 @@ export default class App extends PureComponent<Props, State> {
|
||||
// ignore calling setRawMode on an handle stdin it cannot be called
|
||||
if (this.isRawModeSupported()) {
|
||||
this.handleSetRawMode(false)
|
||||
} else {
|
||||
// Even when raw mode was never enabled (e.g. non-TTY stdin on
|
||||
// Windows Node.js), ensure stdin is unref'd so the process can
|
||||
// exit. earlyInput may have called ref() before Ink mounted.
|
||||
try {
|
||||
this.props.stdin.unref()
|
||||
} catch {
|
||||
// stdin may already be destroyed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ let earlyInputBuffer = ''
|
||||
let isCapturing = false
|
||||
// Reference to the readable handler so we can remove it later
|
||||
let readableHandler: (() => void) | null = null
|
||||
// Safety valve: auto-cleanup after timeout so stdin.ref() never leaks
|
||||
let safetyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/**
|
||||
* Start capturing stdin data early, before the REPL is initialized.
|
||||
@@ -60,6 +62,20 @@ export function startCapturingEarlyInput(): void {
|
||||
}
|
||||
|
||||
process.stdin.on('readable', readableHandler)
|
||||
|
||||
// Safety valve: if Ink never takes over within 10s (e.g. setup dialog
|
||||
// stalls, or an error prevents Ink mount on Windows), unref stdin so
|
||||
// the process doesn't hang forever. The REPL's Ink App normally calls
|
||||
// consumeEarlyInput() → stopCapturingEarlyInput() long before this.
|
||||
safetyTimer = setTimeout(() => {
|
||||
if (isCapturing) {
|
||||
stopCapturingEarlyInput()
|
||||
}
|
||||
}, 10_000)
|
||||
// Don't let the timer itself keep the event loop alive
|
||||
if (safetyTimer && typeof safetyTimer === 'object' && 'unref' in safetyTimer) {
|
||||
safetyTimer.unref()
|
||||
}
|
||||
} catch {
|
||||
// If we can't set raw mode, just silently continue without early capture
|
||||
isCapturing = false
|
||||
@@ -172,14 +188,34 @@ export function stopCapturingEarlyInput(): void {
|
||||
|
||||
isCapturing = false
|
||||
|
||||
// Clear safety timer
|
||||
if (safetyTimer) {
|
||||
clearTimeout(safetyTimer)
|
||||
safetyTimer = null
|
||||
}
|
||||
|
||||
if (readableHandler) {
|
||||
process.stdin.removeListener('readable', readableHandler)
|
||||
readableHandler = null
|
||||
}
|
||||
|
||||
// Don't reset stdin state - the REPL's Ink App will manage stdin state.
|
||||
// If we call setRawMode(false) here, it can interfere with the REPL's
|
||||
// own stdin setup which happens around the same time.
|
||||
// Undo the ref() from startCapturingEarlyInput so the event loop isn't
|
||||
// kept alive if Ink never takes over (e.g. raw mode unsupported on
|
||||
// Windows Node.js, or an error during setup). Ink's own
|
||||
// handleSetRawMode(true) calls stdin.ref() again, and its
|
||||
// handleSetRawMode(false) / unmount path calls stdin.unref(), so this
|
||||
// unref is safe even when Ink does take over — the two ref/unref calls
|
||||
// balance out.
|
||||
try {
|
||||
process.stdin.unref()
|
||||
} catch {
|
||||
// stdin may already be destroyed
|
||||
}
|
||||
|
||||
// Don't reset setRawMode here — Ink's App.handleSetRawMode(true)
|
||||
// calls stopCapturingEarlyInput() synchronously and then immediately
|
||||
// calls setRawMode(true) + ref() on the same stdin, so toggling it
|
||||
// off here would add a visible flicker on Windows.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user