Skip to content

tty(windows): make non-stdin ReadStream#setRawMode use VT raw mode - #34788

Open
robobun wants to merge 6 commits into
mainfrom
farm/6ea66554/tty-setrawmode-vt-nonstdin
Open

tty(windows): make non-stdin ReadStream#setRawMode use VT raw mode#34788
robobun wants to merge 6 commits into
mainfrom
farm/6ea66554/tty-setrawmode-vt-nonstdin

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What

On Windows, process.stdin.setRawMode(true) requests UV_TTY_MODE_RAW_VT (ENABLE_VIRTUAL_TERMINAL_INPUT) so the terminal supplies VT input such as bracketed paste. A tty.ReadStream on any other console fd took a branch that depended on this.$bunNativePtr, which fs.ReadStream never populates, so it always emitted setRawMode failed because it was called on something that is not a TTY and never reached Source::set_raw_mode (which itself would have requested plain UV_TTY_MODE_RAW, leaving the VT-input flag off).

const fd = fs.openSync("CONIN$", "r");
const stream = new tty.ReadStream(fd);
stream.setRawMode(true);
// before: 'error' event "setRawMode failed because it was called on something that is not a TTY"
// after:  console input mode = ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT,
//         same as process.stdin.setRawMode(true)

Fix

  • Source::set_raw_mode now requests TtyMode::Vt.
  • Source__setRawModeStdin is generalised to Source__setRawModeTty(loop, fd, raw). Console input mode is a property of the input buffer (not the handle), and uv_tty_set_mode short-circuits when the requested mode matches the cached tty.rd.mode.mode on the stdin singleton, so every console-input fd routes the actual mode change through uv_tty_set_mode on that singleton. For fd != 0 the caller's fd is only used to gate that it is a console-input handle (GetNumberOfConsoleInputEvents, which rejects screen-buffer handles with ENOTTY). When fd 0 itself is not a console (piped stdin + CONIN$ reopen) there is no libuv cache on the input buffer, and the same UV_TTY_MODE_RAW_VT/UV_TTY_MODE_NORMAL console-mode masks libuv uses are written on a fresh RW CONIN$ handle instead; a transient uv_tty_t on the caller's fd is not usable here because uv__tty_close calls _close(fd), and SetConsoleMode needs GENERIC_READ | GENERIC_WRITE which an O_RDONLY CONIN$ handle lacks.
  • jsTTYSetMode on Windows now takes (fd, flag) (validated with isNumber()/toInt32 under a throw scope, since this.fd is user-mutable and set to null on close). src/js/node/tty.ts calls it for every fd, dropping the dead $bunNativePtr branch.

Verification

New Windows-only test in test/js/node/tty.test.ts spawns a child under Bun.Terminal (ConPTY) and asserts via GetConsoleMode (bun:ffi) that:

  • raw-moding process.stdin and a tty.ReadStream on fs.openSync("CONIN$", "r") leave the console in the same VT raw / cooked masks,
  • process.stdin.setRawMode(true) still takes effect after a CONIN$ stream has restored cooked mode in between (libuv's mode cache stays coherent), and
  • new tty.ReadStream(1).setRawMode(true) emits an error rather than touching the input buffer.

Skipped on arm64 (no bun:ffi backend there). On Windows x64:

USE_SYSTEM_BUN=1 bun test test/js/node/tty.test.ts -t "VT raw console mode"
  -> fail: {stdinRawMode: 520, coninRawMode: 7, err: "...not a TTY"}

bun bd test test/js/node/tty.test.ts
  -> 5 pass, 3 skip, 0 fail

cargo check -p bun_io --target x86_64-pc-windows-msvc   -> ok
cargo check -p bun_io --target aarch64-pc-windows-msvc  -> ok

POSIX is untouched (source.rs is cfg(windows); the POSIX branch in tty.ts is unchanged).


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/tty.test.ts

On Windows, process.stdin.setRawMode already requests UV_TTY_MODE_RAW_VT
(ENABLE_VIRTUAL_TERMINAL_INPUT) so the terminal supplies VT input such as
bracketed paste. A tty.ReadStream on any other console fd, e.g.
new tty.ReadStream(fs.openSync("CONIN$", "r")), took the $bunNativePtr
branch, which fs.ReadStream never populates, so it always emitted
"setRawMode failed because it was called on something that is not a TTY"
and never reached Source::set_raw_mode (which itself would have used
UV_TTY_MODE_RAW, not RAW_VT).

Unify on the VT raw mode:

  * Source::set_raw_mode now requests TtyMode::Vt.
  * Source__setRawModeStdin is generalised to Source__setRawModeTty(fd).
    fd 0 keeps the stdin uv_tty_t singleton so the mode change is
    coordinated with any in-flight libuv console read. For other console
    fds the caller's fd is checked with GetConsoleMode and the same
    RAW_VT / NORMAL console-mode masks libuv uses are written on a fresh
    CONIN$ handle (SetConsoleMode needs GENERIC_READ|GENERIC_WRITE, which
    an O_RDONLY handle lacks, and a transient uv_tty_t would _close() the
    caller's fd on uv_close).
  * jsTTYSetMode on Windows now takes (fd, flag) and tty.ts calls it for
    every fd, dropping the dead $bunNativePtr branch.

Console input mode is per input buffer, so after setRawMode(true) on
either path, GetConsoleMode on any console input handle reports the same
ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT mask.
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:02 AM PT - Jul 20th, 2026

@robobun, your commit 7165d46fec0a5623146b0e3f9b0e45d20a573475 passed in Build #76196! 🎉


🧪   To try this PR locally:

bunx bun-pr 34788

That installs a local version of the PR into your bun-34788 executable, so you can run:

bun-34788 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix setRawMode on Windows for non-stdin TTYs #32153 - Also fixes tty.ReadStream#setRawMode() on Windows for non-stdin console handles (e.g., reopened CONIN$), creating a native TTY wrapper for non-stdin read streams

🤖 Generated with Claude Code

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Related to #32153 but not a duplicate. Both make setRawMode work on a non-stdin console tty.ReadStream, but differ in approach and in which raw mode they request:

#32153 this PR
mechanism new TTY(fd) stored as $bunNativePtr, uv_tty_set_mode on it ttySetMode(fd, flag) everywhere; fd 0 via the stdin uv_tty_t singleton, other fds via SetConsoleMode on a fresh RW CONIN$ handle
raw mode requested UV_TTY_MODE_RAW (handle.setRawMode(flag ? 1 : 0)) UV_TTY_MODE_RAW_VT (ENABLE_VIRTUAL_TERMINAL_INPUT), same as process.stdin
fs.openSync("CONIN$", "r") fails EPERM (SetConsoleMode needs GENERIC_WRITE; "r+" is required) works (mode is written through a separate RW handle)
caller's fd uv__tty_close on the TTYWrap eventually _close()s it untouched

The specific goal here was unifying on the VT raw mode so bracketed paste and other VT-only input works regardless of which fd the program raw-modes, which #32153 on its own does not change (it passes mode 1).

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c768ee1b-fdfa-490e-b51b-293015475eed

📥 Commits

Reviewing files that changed from the base of the PR and between ba6c918 and 7165d46.

📒 Files selected for processing (6)
  • src/io/source.rs
  • src/js/node/tty.ts
  • src/jsc/bindings/ProcessBindingTTYWrap.cpp
  • src/sys/windows/mod.rs
  • src/windows_sys/externs.rs
  • test/js/node/tty.test.ts

Walkthrough

Changes

Windows TTY raw-mode routing

Layer / File(s) Summary
TTY mode implementation
src/io/source.rs
Raw mode now uses VT-capable libuv behavior, while the Windows FFI entry point handles stdin and explicit console input descriptors.
Binding and JavaScript wiring
src/jsc/bindings/ProcessBindingTTYWrap.cpp, src/js/node/tty.ts
The Windows binding and ReadStream.setRawMode now pass the file descriptor and raw-mode flag together.
Windows raw-mode validation
test/js/node/tty.test.ts
A Windows/arm64 test compares raw-mode console flags for process.stdin and a CONIN$ stream.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main Windows tty raw-mode change for non-stdin ReadStream handles.
Description check ✅ Passed The description covers what changed, why, and how it was verified, even though it uses different headings than the template.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/tty.ts`:
- Around line 59-64: Condense the comment surrounding Source__setRawModeTty to
three lines or fewer, preserving only the durable Windows VT-path and POSIX
termios distinction. Remove the fd coordination details and first-person
explanation.

In `@test/js/node/tty.test.ts`:
- Around line 194-200: Shorten the comment above the Windows raw-mode test to
three lines or fewer, retaining only the durable invariant that CONIN$ and stdin
must resolve to the same VT raw console mode. Remove historical implementation
details and references to prior failures or bun:ffi limitations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e9da91c-6087-40c0-8ad8-e9f5fc60f6df

📥 Commits

Reviewing files that changed from the base of the PR and between 86fc072 and ba6c918.

📒 Files selected for processing (4)
  • src/io/source.rs
  • src/js/node/tty.ts
  • src/jsc/bindings/ProcessBindingTTYWrap.cpp
  • test/js/node/tty.test.ts

Comment thread src/js/node/tty.ts Outdated
Comment thread test/js/node/tty.test.ts Outdated
Comment thread src/io/source.rs
Comment thread src/jsc/bindings/ProcessBindingTTYWrap.cpp Outdated
Comment thread src/io/source.rs Outdated
robobun and others added 2 commits July 20, 2026 08:38
Address claude[bot] findings on #34788:

  * uv_tty_set_mode short-circuits when the requested mode equals the cached
    tty.rd.mode.mode on the stdin singleton, so writing the console mode via a
    side channel for fd != 0 could leave that cache stale and make a later
    process.stdin.setRawMode(true) no-op. Route every console-input fd through
    the singleton; fall back to direct SetConsoleMode on CONIN$ only when
    fd 0 itself is not a console (no cache to desynchronise then).
  * Gate fd != 0 with GetNumberOfConsoleInputEvents so console output handles
    (CONOUT$, fd 1/2) are rejected with ENOTTY instead of silently raw-moding
    the input buffer.
  * jsTTYSetMode on Windows now validates fd with isNumber() + toInt32 under a
    throw scope; this.fd is a public user-mutable property that fs.ReadStream
    sets to null on close.

The test is extended to cover both the cache-desync sequence and the
output-handle rejection.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three findings from the earlier pass (libuv mode-cache desync on the CONIN$ path, .asInt32() on user-mutable this.fd, and GetConsoleMode accepting output handles) are all correctly addressed in f8ba2a8 — verified against the current diff, and the test now covers each. Also checked that Source::set_raw_mode's RawVt change has no other reachable JS callers (the removed $bunNativePtr branch was the only path to it via src/js/).

Extended reasoning...

Confirming prior findings are resolved and recording one additional thing ruled out this pass; the remaining inline comment is a placement nit with no behavioral impact. Deferring rather than approving because this is moderately complex Windows-only console/libuv coordination that CI has not yet verified on-target, and there is a competing design in #32153 a maintainer should weigh.

Comment thread src/io/source.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three earlier findings (libuv mode-cache desync, asInt32 on user-mutable this.fd, GetConsoleMode accepting output handles) and the ENABLE_WINDOW_INPUT placement nit are all addressed — nothing new to flag on this revision. Deferring to a human because this is non-trivial Windows-native console/libuv logic whose test only runs on Windows x64 CI, and it overlaps with #32153's alternative approach.

What was reviewed:

  • Source__setRawModeTty: fd gating via GetNumberOfConsoleInputEvents, stdin-singleton routing keeps libuv's cached mode coherent, CONIN$ fallback masks/close match libuv's uv_tty_set_mode.
  • jsTTYSetMode (Windows): now mirrors the POSIX branch's throw-scope + isNumber()/toInt32 shape.
  • Test asserts stdin↔CONIN$ parity, cache-coherence sequence, and ReadStream(1).setRawMode error — matches the fixes.
Extended reasoning...

Overview

Generalises Windows tty.ReadStream#setRawMode so non-stdin console fds (e.g. fs.openSync("CONIN$", "r")) enter the same UV_TTY_MODE_RAW_VT state as process.stdin. Touches src/io/source.rs (rewrites Source__setRawModeStdinSource__setRawModeTty, ~80 new lines of Win32/libuv logic), src/jsc/bindings/ProcessBindingTTYWrap.cpp (Windows branch of jsTTYSetMode now takes (fd, flag) with proper coercion), src/js/node/tty.ts (drops the dead $bunNativePtr branch), plus a new GetNumberOfConsoleInputEvents extern and ENABLE_WINDOW_INPUT const in the Windows sys crates, and a Windows-only ConPTY test.

Security risks

None identified. No auth/crypto/permissions surface; the input is a JS fd that is validated (isNumber() + toInt32) before being handed to uv_get_osfhandle and gated by GetNumberOfConsoleInputEvents. The fresh CONIN$ handle is opened RW and closed in the same block on all paths.

Level of scrutiny

Medium-high. This is platform-specific native code with subtle correctness concerns: libuv's per-handle mode cache, per-buffer vs per-handle console-mode semantics, and interaction with an in-flight stdin reader. The prior review round surfaced three real bugs in exactly those areas; the author fixed all of them cleanly, and this run's re-review found nothing further. But the fix's design (route every console-input fd through the stdin uv_tty_t singleton, fall back to direct SetConsoleMode only when fd 0 isn't a console) is a judgement call that differs from the competing #32153 approach, and the only test coverage runs on Windows x64 CI — the author could not verify locally.

Other factors

  • All four prior inline findings are resolved and the fixes are covered by new assertions in the test (cache-coherence sequence, ReadStream(1) error).
  • POSIX is untouched (source.rs is cfg(windows); the POSIX tty.ts branch is unchanged).
  • The Source::set_raw_mode change from TtyMode::RawTtyMode::Vt also affects any other Windows caller of that method; that's intentional per the PR's stated goal but worth a maintainer glance.
  • Given the platform scope, the design overlap with #32153, and CI-only verification, a human sign-off is appropriate rather than auto-approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant