Skip to content

Commit eb43657

Browse files
bartlomiejuclaude
andauthored
fix(ext/node): rewrite Windows TTY reading to match libuv (console mode, encoding, raw + line mode) (#32999)
## Summary Rewrites the Windows TTY read/write paths to match libuv's behavior, fixing regressions introduced in the `node:tty` rewrite (#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), losing `ENABLE_QUICK_EDIT_MODE`, `ENABLE_INSERT_MODE`, and `ENABLE_EXTENDED_FLAGS` that 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_write` used `WriteFile` which 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 uses `WriteConsoleW`, matching libuv's approach. ### Raw mode reading rewrite Replaced `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. The new implementation: - Reads individual `INPUT_RECORD` structs via `ReadConsoleInputW` - Filters non-key events, KEY_UP events (except Alt composition), and numpad keys during Alt-code entry - Encodes Unicode characters to UTF-8 with surrogate pair support - Maps function keys (arrows, F1-F12, Home/End/etc.) to VT100/xterm escape sequences (ported from libuv's `get_vt100_fn_key`) - Handles key repeat counts and Alt-prefix for escape sequences ### Line mode reading rewrite Replaced blocking `ReadFile` with `ReadConsoleW` on a worker thread, matching libuv's `uv_tty_line_read_thread` + `QueueUserWorkItem` approach. `ReadFile`/`ReadConsoleW` block until Enter is pressed, so they must run off the event loop thread. The worker thread reads UTF-16 via `ReadConsoleW`, converts to UTF-8, and wakes the event loop when the line is complete. ### Async console input notification Uses `RegisterWaitForSingleObject` (matching libuv's `uv__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_cb` when there is actually data to process, preventing spurious `read_cb(nread=0)` calls on every poll cycle. Fixes #32996 Fixes #32997 Fixes #32639 Fixes #33002 Fixes #32992 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7250271 commit eb43657

8 files changed

Lines changed: 1159 additions & 83 deletions

File tree

libs/core/uv_compat.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ impl UvLoopInner {
199199
}
200200
}
201201

202+
/// Wake the event loop so it re-polls on the next tick. Used on
203+
/// Windows to ensure pending TTY write callbacks are processed
204+
/// promptly when there is no async I/O notification mechanism.
205+
#[cfg(windows)]
206+
pub(crate) fn wake(&self) {
207+
if let Some(waker) = self.waker.borrow().as_ref() {
208+
waker.wake_by_ref();
209+
}
210+
}
211+
202212
#[inline]
203213
fn alloc_timer_id(&self) -> u64 {
204214
let id = self.next_timer_id.get();
@@ -568,12 +578,13 @@ impl UvLoopInner {
568578
}
569579
}
570580

571-
// Close the handle on Windows.
581+
// Tear down Windows async read machinery, then close the handle.
572582
#[cfg(windows)]
573583
{
584+
tty::close_tty_read(handle);
574585
if !tty.internal_handle.is_null() {
575586
if tty.internal_handle_owned {
576-
// We duplicated this handle in init close it directly.
587+
// We duplicated this handle in init -- close it directly.
577588
tty::win_console::CloseHandle(tty.internal_handle);
578589
} else if tty.internal_fd >= 0 {
579590
// Non-duplicated: close through the CRT to free the fd slot.

0 commit comments

Comments
 (0)