fix(ext/node): rewrite Windows TTY reading to match libuv (console mode, encoding, raw + line mode)#32999
Conversation
…ing, and read blocking Three fixes for Windows TTY regressions introduced in the node:tty rewrite (denoland#32777): 1. Restore saved console mode for NORMAL: uv_tty_set_mode(NORMAL) was setting a hardcoded subset of flags (ECHO|LINE|PROCESSED = 0x0007), losing ENABLE_QUICK_EDIT_MODE, ENABLE_INSERT_MODE, and ENABLE_EXTENDED_FLAGS that Windows sets by default. Now restores the original mode saved at init time, matching libuv behavior. 2. Use WriteConsoleW for TTY output: WriteFile writes raw bytes interpreted by the console's active code page, garbling non-ASCII output (box-drawing chars, Unicode) when the code page isn't UTF-8. Now converts UTF-8 to UTF-16 and calls WriteConsoleW, matching libuv. 3. Re-check events before each read: the Windows read loop evaluated GetNumberOfConsoleInputEvents once and looped with a stale boolean, so ReadFile could block the event loop after consuming all available events. Now re-checks before each ReadFile call. Fixes denoland#32996 Fixes denoland#32997 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The loop that re-checked GetNumberOfConsoleInputEvents before each ReadFile call caused the event loop to block. The Windows console input buffer contains ALL event types (KEY_UP, FOCUS, MOUSE, etc.) but ReadFile only consumes KEY_DOWN events that produce characters. After reading a character, non-character events remain in the buffer, num_events > 0 still holds, and the next ReadFile blocks waiting for input that will never come -- freezing the prompt after the first keystroke and preventing Ctrl+C from working. Revert to a single-shot read per poll cycle: check once, read once, return to the event loop. The event loop re-enters poll_tty_handle on the next tick when more input arrives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n-character events ReadFile on a console handle only consumes KEY_DOWN events that produce characters. Other events in the input buffer (KEY_UP, FOCUS, MOUSE, WINDOW_BUFFER_SIZE) are ignored by ReadFile but not removed, so it blocks waiting for the next character-producing event. This caused the prompt to freeze after the first keystroke and prevented Ctrl+C from working. Now uses PeekConsoleInputW to inspect the front of the input queue and drains non-character events (anything that is not a KEY_DOWN with a non-zero UnicodeChar) via ReadConsoleInputW before calling ReadFile. This ensures ReadFile is only called when there is actually a character to read, preventing event loop blocking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…putW Replace ReadFile-based console reading with ReadConsoleInputW for raw mode, matching libuv's uv_process_tty_read_raw_req approach. ReadFile on a console handle only consumes KEY_DOWN events that produce characters, blocking on KEY_UP/FOCUS/MOUSE events and freezing the event loop. ReadConsoleInputW reads individual INPUT_RECORD structs, allowing us to filter and process events manually. Changes: - Add Win32 constants: virtual key codes, control key state flags - Add state fields to uv_tty_t for key buffering and UTF-16 surrogates - Port libuv's VT100 function key mapping (get_vt100_fn_key) - New tty_try_read_raw: processes INPUT_RECORD structs, filters KEY_UP/non-key events, handles Alt-code composition, encodes UTF-16 to UTF-8, maps function keys to VT100 escape sequences, handles repeat counts and surrogate pairs - Rename old tty_try_read to tty_try_read_line for line mode - Mode-aware poll_tty_handle: raw mode uses tty_try_read_raw, line mode uses tty_try_read_line gated by event count Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only allocate a buffer and call read_cb when there is actually something to read. Without this gate, poll_tty_handle would call alloc_cb + read_cb(nread=0) on every event loop tick even when no input was available, causing the JS readline layer to malfunction after processing the first character. On Unix, poll_read_ready(cx) serves this gating role via tokio's AsyncFd integration. On Windows there is no equivalent async notification mechanism, so we check explicitly: - Raw mode: pending decoded bytes, repeat count, or console events - Line mode: console events via GetNumberOfConsoleInputEvents Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Windows event loop had no mechanism to wake up when new console input arrived. After delivering the first character, tokio would park and never re-poll the TTY handle, freezing the prompt. Now uses RegisterWaitForSingleObject (matching libuv's uv__tty_queue_read_raw) to register a one-shot thread pool wait on the console input handle. When the handle is signaled (input available), the callback wakes the tokio event loop via the stored waker. The wait is re-registered after each signal (one-shot) and unregistered when reading stops. This eliminates the need for busy-polling while ensuring the event loop promptly processes new console input. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Verified the fix on Windows machine - unicode renders properly, prompt works fine, arrow key selector works correctly too 👍 |
When a write is queued to a TTY handle on Windows, the deferred write callback was not processed until something else woke the event loop (e.g. console input). This caused inquirer-style prompts to not render until the user pressed a key. Add UvLoopInner::wake() and call it from ensure_tty_registered on Windows so the event loop re-polls promptly after a write is queued, flushing the write callback and allowing the JS layer to render the prompt UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wake() from ensure_tty_registered only fires for queued writes. If read_start_tty is called after run_io() has already finished iterating tty_handles (e.g. from a microtask), the stdin handle won't be processed until the next event loop tick. But nothing wakes the loop to trigger that tick. Wake from read_start_tty so poll_tty_handle runs promptly, which both registers RegisterWaitForSingleObject for input notifications and drains any pending stdout write callbacks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReadConsoleW blocks until the user presses Enter, so calling it from the event loop thread freezes everything. This matches libuv's uv_tty_line_read_thread which uses QueueUserWorkItem to run ReadConsoleW on a dedicated worker thread. The implementation: - Spawns a "tty-line-read" thread that calls ReadConsoleW (UTF-16) - Converts the result to UTF-8 via String::from_utf16_lossy - Stores the result in a shared Arc<Mutex<>> and wakes the event loop - poll_tty_handle picks up the result on the next tick and delivers it to the read callback - Only one line-read thread is in flight at a time This fixes vite create and other programs that use line-mode console input on Windows (the default mode before setRawMode is called). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…move dead code Two fixes: 1. Drain pending write callbacks before spawning the ReadConsoleW worker thread. The JS stream layer may have prompt text buffered that only reaches the native WriteConsoleW call after a write completion callback fires. Without this drain, the line-read thread starts (and blocks on ReadConsoleW) before the prompt renders, requiring the user to press a key before seeing output. 2. Remove unused tty_try_read_line (replaced by the threaded ReadConsoleW approach) and fix unnecessary unsafe block warning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add tty_line_mode_read_pty: tests threaded ReadConsoleW line-mode reading with consecutive prompts - Add tty_raw_mode_arrow_keys_pty: tests VT100 escape sequence mapping for arrow keys via ReadConsoleInputW - Add tty_ctrl_c_raw_mode_pty: tests that Ctrl+C (0x03) is delivered as data in raw mode (not as SIGINT, since raw mode disables terminal signal processing) - Remove the write-queue drain before spawning the line-read thread (confirmed unnecessary -- inquirer works without it as of 2eee47e) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
i found one blocking issue here.
• a that opens up two bad races: • the wait callback keeps a raw pointer to so i don’t think this is safe to merge as-is. otherwise the direction looks right, but this one needs fixing first. |
…g handle stop_tty() was closing the console handle without first unregistering the RegisterWaitForSingleObject callback or detaching the line-mode reader thread. This opened two races: 1. The wait callback holds a raw pointer to internal_wait_waker; if the handle is closed and the uv_tty_t freed before UnregisterWaitEx, the callback can wake through freed memory. 2. The detached line-read thread could still be using the HANDLE while close was happening. Add close_tty_read() that tears down the wait registration (blocking until any in-flight callback completes via INVALID_HANDLE_VALUE), drops the waker, and detaches from any in-flight line-mode read. Call it from stop_tty() before CloseHandle/_close. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
re-reviewed after
• calls that addresses the use-after-free / close-race i flagged earlier. i don't see any new follow-up issues from this pass. |
Summary
Rewrites the Windows TTY read/write paths to match libuv's behavior, fixing regressions introduced in the
node:ttyrewrite (#32777, landed in v2.7.6).Console mode restoration
uv_tty_set_mode(NORMAL)was replacing the console mode with a hardcoded subset of flags (ECHO|LINE|PROCESSED= 0x0007), losingENABLE_QUICK_EDIT_MODE,ENABLE_INSERT_MODE, andENABLE_EXTENDED_FLAGSthat Windows sets by default. This broke interactive input after the first raw-mode cycle (e.g. vite create's multi-step prompts). Now restores the original mode saved at init time, matching libuv's behavior.TTY output encoding
tty_try_writeusedWriteFilewhich writes raw bytes interpreted according to the console's active code page. On non-UTF-8 code pages (common on Windows), this garbled Unicode output -- box-drawing characters, accented text, CJK, etc. Now converts UTF-8 to UTF-16 and usesWriteConsoleW, matching libuv's approach.Raw mode reading rewrite
Replaced
ReadFile-based console reading withReadConsoleInputWfor raw mode, matching libuv'suv_process_tty_read_raw_reqapproach.ReadFileon a console handle only consumes KEY_DOWN events that produce characters, blocking on KEY_UP/FOCUS/MOUSE events and freezing the event loop. The new implementation:INPUT_RECORDstructs viaReadConsoleInputWget_vt100_fn_key)Line mode reading rewrite
Replaced blocking
ReadFilewithReadConsoleWon a worker thread, matching libuv'suv_tty_line_read_thread+QueueUserWorkItemapproach.ReadFile/ReadConsoleWblock until Enter is pressed, so they must run off the event loop thread. The worker thread reads UTF-16 viaReadConsoleW, converts to UTF-8, and wakes the event loop when the line is complete.Async console input notification
Uses
RegisterWaitForSingleObject(matching libuv'suv__tty_queue_read_raw) to register a one-shot thread pool wait on the console input handle. When input becomes available, the callback wakes the tokio event loop. Without this, tokio would park after the first keystroke and never re-poll the TTY.Input gating
Only allocates buffers and calls
read_cbwhen there is actually data to process, preventing spuriousread_cb(nread=0)calls on every poll cycle.Fixes #32996
Fixes #32997
Fixes #32639
Fixes #33002
Fixes #32992
Test plan
cargo checkpassestools/format.jsandtools/lint.jspasstty_raw_mode_toggle_pty-- raw mode toggling across 3 consecutive promptstty_unicode_output_pty-- box-drawing and Unicode characters render correctlytty_line_mode_read_pty-- threaded ReadConsoleW line-mode with consecutive promptstty_raw_mode_arrow_keys_pty-- arrow keys produce correct VT100 escape sequencestty_ctrl_c_raw_mode_pty-- Ctrl+C (0x03) delivered as data in raw modereadline_multi_prompt_ptyandreadline_muted_multi_prompt_ptytests passdeno create npm:vite@latestmulti-step prompts work correctly🤖 Generated with Claude Code