From e38d45460ec1204b6accc950a914952d25a5444f Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Fri, 24 Apr 2026 21:16:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Windows=20Node.js?= =?UTF-8?q?=20=E6=9E=84=E5=BB=BA=E4=BA=A7=E7=89=A9=E5=9B=A0=20stdin.ref()?= =?UTF-8?q?=20=E6=B3=84=E6=BC=8F=E5=AF=BC=E8=87=B4=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E6=8C=82=E8=B5=B7=20(#353)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/@ant/ink/src/components/App.tsx | 9 +++++ src/utils/earlyInput.ts | 42 ++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/@ant/ink/src/components/App.tsx b/packages/@ant/ink/src/components/App.tsx index 8b7f5bdaa..543cd359b 100644 --- a/packages/@ant/ink/src/components/App.tsx +++ b/packages/@ant/ink/src/components/App.tsx @@ -286,6 +286,15 @@ export default class App extends PureComponent { // 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 + } } } diff --git a/src/utils/earlyInput.ts b/src/utils/earlyInput.ts index a5d58db5e..3d8d03554 100644 --- a/src/utils/earlyInput.ts +++ b/src/utils/earlyInput.ts @@ -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 | 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. } /**