Repository navigation
Conversation
node:readline (and anything else that calls setRawMode(true)) clears ICANON, ECHO, ISIG and ICRNL on the tty. While in that state Enter delivers a bare CR, typed characters are not echoed, and Ctrl+C is not translated into SIGINT. alert/confirm/prompt all block on a read that waits for LF, so calling any of them from inside a readline callback or from bun repl appeared to hang with no visible feedback. Wrap the blocking read in a guard that saves the current termios, sets ICANON|ECHO|ICRNL|ISIG|IEXTEN, and restores the saved termios on drop. tcgetattr fails on non-tty stdin so piped input keeps its existing behaviour. On Windows the existing console-mode guard for prompt() now also sets ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT|ENABLE_PROCESSED_INPUT. Fixes #5267 Fixes #27542
WalkthroughChangesBlocking dialog stdin handling
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 3:24 AM PT - Jul 25th, 2026
❌ @robobun, your commit ac2daf5 has some failures in 🧪 To try this PR locally: bunx bun-pr 35464That installs a local version of the PR into your bun-35464 --bun |
Make the Windows CookedStdinGuard wrap StdinModeGuard with ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT set and ENABLE_VIRTUAL_TERMINAL_INPUT cleared, so alert() and confirm() get the same treatment as prompt() when the console has been put into raw mode. The separate Windows block inside prompt() is now redundant and is removed.
Add a withTerminalRepl test that drives prompt/confirm/alert from the REPL, the original #27542 repro. Replace the tautological isRaw check in the readline test with an echo-count assertion that observably distinguishes restored raw mode from left-cooked mode.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/js/bun/repl/repl.test.ts (1)
1-1: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd explicit timeouts to the new multi-round-trip dialog tests. Both newly added tests drive several sequential terminal write/wait cycles on top of process-spawn overhead without an explicit per-test timeout, the same test shape for which this repo already has a documented mitigation (explicit ~15000ms timeouts on comparable PTY tests in
repl.test.ts).
test/js/bun/repl/repl.test.ts#L1183-1203: add an explicit timeout (e.g.,15000) to thetest(...)call, matching the file's existing convention forwithTerminalRepl-based tests.test/js/web/web-globals.test.js#L360-440: add an explicit timeout (e.g.,15000) to thetest.skipIf(isWindows)(...)call given its 5 sequential write/wait round trips plus child-process spawn overhead.🤖 Prompt for 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. In `@test/js/bun/repl/repl.test.ts` at line 1, Add explicit 15000ms per-test timeouts to the newly added multi-round-trip dialog tests: the withTerminalRepl-based test in repl.test.ts and the test.skipIf(isWindows) case in web-globals.test.js. Preserve their existing bodies and assertions while matching the repository’s timeout convention.Source: Learnings
🤖 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 `@test/js/bun/repl/repl.test.ts`:
- Around line 1183-1203: Update the “alert/confirm/prompt return when called
from the REPL” test to pass an explicit approximately 15000ms timeout, following
the established timeout convention used by comparable PTY tests in this file.
Keep the existing dialog round-trips and final evaluation unchanged.
In `@test/js/web/web-globals.test.js`:
- Around line 360-440: Add an explicit extended timeout to the multi-round-trip
test registered by “alert/confirm/prompt work while readline has stdin in raw
mode,” using the test framework’s existing timeout option or convention. Keep
the five terminal interaction steps and assertions unchanged.
---
Outside diff comments:
In `@test/js/bun/repl/repl.test.ts`:
- Line 1: Add explicit 15000ms per-test timeouts to the newly added
multi-round-trip dialog tests: the withTerminalRepl-based test in repl.test.ts
and the test.skipIf(isWindows) case in web-globals.test.js. Preserve their
existing bodies and assertions while matching the repository’s timeout
convention.
🪄 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: 9219ca1c-aa8b-48ae-b849-66200552c03a
📒 Files selected for processing (3)
src/runtime/webcore/prompt.rstest/js/bun/repl/repl.test.tstest/js/web/web-globals.test.js
The pre-constructed Bun.Terminal form does not fire exit() on child exit, so key the reject arm off proc.exited and resolve/reject the waiter directly instead of racing a never-firing promise.
The final PTY read can be dispatched after the pidfd event in the same poll batch, so rejecting on code 0 raced the success path.
There was a problem hiding this comment.
No further findings — all four earlier comments (Windows no-op guard, per-test timeout, dead exit() reject arm, clean-exit race) are addressed in the current diff. Deferring to a human for the final look since this changes TTY-mode handling for three user-facing globals and the Windows raw-mode path has no automated coverage (both new tests skip on Windows).
What was reviewed:
CookedStdinGuardon both platforms: POSIX termios save/OR-inICANON|ECHO|ISIG|IEXTEN+ICRNL/restore viaTCSADRAIN; Windows wraps the existingStdinModeGuardwithENABLE_LINE_INPUT|ECHO|PROCESSEDset and VT-input cleared — verified all referencedbun_sysitems exist and the guard is inert on non-TTY stdin.- Guard placement in
alert/confirm/prompt: created afterOutput::flush()and lives across the whole blocking read;prompt()'s previous Windows-onlyStdinModeGuardis subsumed. - New PTY test:
proc.exitednow rejects pendingwaitFors only on non-zero exit, avoiding the final-read race; asserts echo was on duringprompt()and raw mode was restored for readline afterwards.
Extended reasoning...
Overview
Adds an RAII CookedStdinGuard in src/runtime/webcore/prompt.rs and inserts it into alert(), confirm(), and prompt() immediately before their blocking stdin reads. On Unix the guard tcgetattrs fd 0, ORs in ICANON|ECHO|ISIG|IEXTEN and ICRNL, applies with TCSADRAIN, and restores the saved termios on drop; on Windows it wraps the existing bun_sys::windows::StdinModeGuard with line/echo/processed input set and VT-input cleared. Two new PTY-backed tests cover the readline and bun repl paths.
Prior review history
I left four inline findings on earlier revisions, all now resolved:
- The Windows guard was a no-op stub → fixed in 191190c (now wraps
StdinModeGuardfor all three functions). - Explicit
20_000per-test timeout → dropped in b6ae9bc. - Terminal
exit()callback doesn't fire for a pre-constructedBun.Terminalon child exit → d829957 keys offproc.exitedinstead. - That handler raced the final PTY read on clean exit → d7604c2 skips rejection when
code === 0.
I cross-checked bun_sys::posix::{tcgetattr, tcsetattr, TCSA} and bun_sys::windows::{StdinModeGuard, UpdateStdioModeFlagsOpts, ENABLE_*} — all exist with the shapes the PR uses. UpdateStdioModeFlagsOpts has exactly set/unset so the struct literal without ..Default::default() is complete.
Security risks
None. No untrusted input is parsed; the guard only toggles well-known termios/console-mode bits on fd 0 and restores them exactly. On non-TTY stdin tcgetattr/GetConsoleMode fail and the guard is inert, so piped-input behavior is unchanged (verified by the existing confirm (yes/no) tests that still run on all platforms).
Level of scrutiny
Medium. It's a targeted bugfix at the right layer (the blocking-read call sites own the invariant that stdin be in line mode for the duration), but it changes observable behavior of three Web-API globals whenever stdin is a TTY, and the Windows raw-mode path is untested — the new PTY test is skipIf(isWindows) and the REPL terminal suite is already todoIf(isWindows). The Windows code path reuses an existing in-tree guard and the flag values match the Win32 constants, so risk is low, but a human should sign off on shipping an untested platform branch.
Other factors
The tests are well-constructed per REVIEW.md: event-driven waitFor, every failure path (non-zero child exit) rejects the awaited promise with a diagnostic including accumulated PTY output, and the web-globals test additionally asserts the negative contract (raw mode is restored — kernel does not double-echo the second readline answer). CodeRabbit's contradictory "add a timeout" nits were correctly declined per test/CLAUDE.md. Given four rounds of iteration were needed to reach this state, it doesn't meet the "simple/mechanical/obvious" bar for auto-approval.
Use an underscore-prefixed named field instead so the dead-code lint stays silent without an escape.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@test/js/web/web-globals.test.js`:
- Around line 398-410: Update the child-exit handling around proc.exited and
waitFor so a non-zero exit is stored when no waiters are pending. In waitFor,
immediately reject new callers with the persisted failure instead of enqueueing
and pumping; preserve current waiter rejection behavior when waiters exist and
successful exits remain unaffected.
🪄 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: af418323-8ed3-4f17-904a-67a4f4ec7e02
📒 Files selected for processing (2)
src/runtime/webcore/prompt.rstest/js/web/web-globals.test.js
There was a problem hiding this comment.
No issues found, but this is ~90 lines of new platform-gated termios/console-mode manipulation applied to three call sites, and the Windows branch has no positive PTY coverage (both new tests are skipIf(isWindows) / inside describe.todoIf(isWindows)), so worth a human look.
What was reviewed:
- POSIX guard:
tcgetattr→ OR inICRNL+ICANON|ECHO|ISIG|IEXTEN→tcsetattr(TCSADRAIN), restore onDrop; inert on non-tty — matchesbun_sys::posixsignatures. - Windows guard: wraps the existing
StdinModeGuardwithset/unsetfields that exist onUpdateStdioModeFlagsOpts; replaces the old prompt-only VT-input clear. - Idempotence when stdin is already cooked (flags OR'd in are already set; exact original restored), and existing piped-stdin
confirmtests still exercise the inert path on both platforms. - The two PTY tests' waiter/exit plumbing after the four rounds of fixes (clean-exit early-return, persisted
childExitError).
Extended reasoning...
Overview
The PR fixes #5267 / #27542: alert(), confirm() and prompt() hang when called while stdin is in raw mode (inside a node:readline callback or from bun repl). It introduces an RAII CookedStdinGuard in src/runtime/webcore/prompt.rs (~80 lines across #[cfg(unix)] / #[cfg(windows)] / fallback) and drops one let _cooked = CookedStdinGuard::new(); into each of the three functions, replacing the old prompt-only Windows StdinModeGuard block. Two PTY-driven tests are added: one in test/js/web/web-globals.test.js driving readline → prompt/confirm/alert → readline, and one in test/js/bun/repl/repl.test.ts driving the same three dialogs from the REPL.
Security risks
None identified. The guard reads and writes termios / console mode on fd 0 via existing bun_sys wrappers, restores exactly what it saved, and is inert when stdin is not a tty/console. No user-controlled data reaches the termios path.
Level of scrutiny
Medium. This is native code that mutates process-global terminal state on every alert/confirm/prompt call, with three cfg-gated implementations. The POSIX side is textbook and well-covered by the new PTY tests. The Windows side reuses the pre-existing StdinModeGuard helper (already inert on non-console stdin per its doc comment) but adds three new set flags and applies it to two functions (alert, confirm) that previously touched nothing on Windows — and neither new test runs there. The existing piped-stdin confirm tests do run on Windows and verify the guard is inert on non-console input, but there is no positive Windows test that raw-mode → dialog → raw-mode actually works. A maintainer should at minimum sanity-check the Windows flag choice (ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT set, ENABLE_VIRTUAL_TERMINAL_INPUT cleared).
Other factors
- The PR explicitly chooses this approach over the earlier #27544 (accept bare
\\r) and documents why; that's a reasonable design call but one a maintainer may want to acknowledge. - Four earlier rounds of bot review feedback (Windows guard was a no-op stub; per-test timeout; dead
exit()reject arm; success-path exit race; persist exit error for later waiters) have all been addressed in follow-up commits and every thread is resolved. - No human reviewer has looked at this PR yet.
… text The line discipline applies ICRNL at receive time, not retroactively. If a driver writes its response the instant the prompt text becomes visible, a CR that arrives in the window between the flush and the tcsetattr stays a CR in the input queue and the canonical read never sees a line. Apply the mode change (with TCSANOW, since only input processing is affected) before any prompt text is written so ICRNL is already on by the time a response can arrive.
There was a problem hiding this comment.
No issues found in this pass, but this changes process-global tty/console state around three user-facing globals (alert/confirm/prompt) on both POSIX and Windows, so it's worth a human look — particularly the Windows branch, whose expanded ENABLE_LINE_INPUT|ECHO_INPUT|PROCESSED_INPUT behaviour has no direct test (both new PTY tests are POSIX-only).
What was reviewed:
- RAII shape of
CookedStdinGuard:tcgetattrfailure → inert;Droprestores saved termios on every early-return path including theto_slice(global)?throw. - Guard ordering vs
Output::flush()and the retained comment justifying it. - Windows
UpdateStdioModeFlagsOptshas exactlyset/unset, so the literal without..Default::default()is complete;StdinModeGuardis inert on non-console stdin. - Test-side
waitFor/proc.exitedwiring after the d7604c2/20fe4828 fixes — the clean-exit race and the between-calls exit gap are both handled.
Extended reasoning...
Overview
The PR adds a CookedStdinGuard RAII struct in src/runtime/webcore/prompt.rs that forces stdin into canonical mode (POSIX: OR in ICANON|ECHO|ISIG|IEXTEN on c_lflag and ICRNL on c_iflag; Windows: wrap the existing StdinModeGuard with ENABLE_LINE_INPUT|ECHO_INPUT|PROCESSED_INPUT set and ENABLE_VIRTUAL_TERMINAL_INPUT unset) for the duration of the blocking line read inside alert(), confirm() and prompt(), restoring the caller's mode on drop. It removes the old prompt-only Windows VT-input unset (now subsumed) and adds two PTY-driven regression tests: a readline→prompt/confirm/alert→readline round-trip in web-globals.test.js and a REPL interaction in repl.test.ts.
Security risks
None identified. The guard reads and restores termios/console mode on fd 0 via the in-tree bun_sys wrappers; no untrusted input reaches a size/index computation, no new allocation, no privilege change. The only process-global side effect is the tty mode toggle, which is scoped by RAII and inert on non-tty stdin.
Level of scrutiny
Medium-high. This is a small diff (~90 native LOC) but it changes user-visible behaviour of three global Web APIs and mutates process-global terminal state on every call. A restore-path bug would leave the user's terminal in the wrong mode after prompt() returns. The POSIX path is well-covered by the new PTY test (which explicitly asserts raw mode is put back — the single-echo check on rl2), but the Windows branch's expanded flag set is exercised only indirectly by the pre-existing piped-stdin confirm tests, which verify the guard is inert, not that it does the right thing on a real console. The describe.todoIf(isWindows) REPL suite and test.skipIf(isWindows) PTY test mean the Windows raw-mode path ships without a direct regression test.
Other factors
- All four earlier review findings (Windows coverage of alert/confirm, per-test timeout, dead
exit()reject arm, clean-exit race with the final PTY read) were addressed in follow-up commits and every thread is resolved. - The guard uses the in-tree
bun_sys::posix::tcgetattr/tcsetattrandbun_sys::windows::StdinModeGuardhelpers rather than raw libc, per REVIEW.md. - The guard is installed before
arguments[0].to_slice(global)?, so a throwingtoStringon the message argument still hitsDropand restores the saved termios — checked. - Minor: the PR description says
TCSADRAINbut the code usesTCSA::Now. Since the guard runs before any output is written this is equivalent; noting only so a reviewer isn't surprised by the mismatch.
Deferring rather than approving because this is a cross-platform runtime behaviour change with one platform branch effectively untested, not because anything looks wrong.
There was a problem hiding this comment.
No issues found, but I'd like a human to sign off: this is cross-platform #[cfg]-gated native code that manipulates process-global tty/console state, and the Windows path broadens behavior (all three dialogs now set ENABLE_LINE_INPUT|ECHO_INPUT|PROCESSED_INPUT, vs. only prompt() clearing VT-input before) with no new Windows test coverage — both new tests are POSIX-only.
What was reviewed:
CookedStdinGuardRAII:tcgetattrfailure on non-tty makes it inert;?early-returns after guard construction correctly restore termios viaDrop.- Verified
bun_sys::posix::{tcgetattr,tcsetattr,TCSA},UpdateStdioModeFlagsOptsfield set, and theENABLE_*constants all exist as used. - Confirmed all prior inline threads (exit-race in
waitFor, persisted exit error, comment-cop) are addressed in the current diff.
Extended reasoning...
Overview
The PR wraps alert(), confirm() and prompt() in a new CookedStdinGuard RAII type that forces stdin into canonical/cooked mode for the duration of the blocking line read, then restores the caller's termios/console mode. On POSIX it uses bun_sys::posix::tcgetattr/tcsetattr to OR in ICANON|ECHO|ISIG|IEXTEN and ICRNL; on Windows it wraps the existing bun_sys::windows::StdinModeGuard with ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT|ENABLE_PROCESSED_INPUT set and ENABLE_VIRTUAL_TERMINAL_INPUT unset. The old Windows-only VT-input clear inside prompt() is removed as subsumed. Two PTY-driven tests are added (readline→dialogs in web-globals.test.js, and a REPL test in repl.test.ts); both are POSIX-only.
Security risks
None identified. The change reads/writes termios on fd 0 only, via existing in-tree wrappers, and is inert when stdin is not a tty/console. No untrusted input reaches the termios path.
Level of scrutiny
Medium. This is not large, but it is platform-gated native runtime code that mutates process-global tty state on a user-facing Web API. REVIEW.md calls out cross-platform/#[cfg]-gated changes as needing extra care, and the Windows arm broadens behavior relative to the previous code (previously only prompt() touched console mode, and only to clear VT-input; now all three dialogs also set line/echo/processed input) without any new Windows test — the REPL terminal suite is todoIf(isWindows) and the new web-globals test is skipIf(isWindows). The existing piped-stdin confirm tests only verify the guard is inert on non-console stdin, not the new cooked-mode behavior on a Windows console.
Other factors
- All referenced helpers exist and match the signatures used:
bun_sys::posix::{tcgetattr,tcsetattr,TCSA::Now}(withTermios = libc::termios),UpdateStdioModeFlagsOpts { set, unset }(exactly two fields, so no..Default::default()needed),StdinModeGuard::set, and theENABLE_*constants. - The guard is armed before
arguments[0].to_slice(global)?, so a throwingtoStringon the message correctly restores termios viaDropon?propagation. - All prior review threads (mine on the
waitForexit wiring and the code-0 race, CodeRabbit's persisted-exit-error, comment-cop) are resolved and reflected in the current diff. - The one retained doc comment on
CookedStdinGuardwas justified by the author as documenting a load-bearing ordering invariant (ICRNLapplies at receive time), which fits REVIEW.md's "comments carry only durable non-obvious content" rule.
Given the untested Windows behavior expansion and the process-global-state nature of the change, deferring to a human reviewer rather than approving.
|
The diff is green: both new tests ( The remaining failures in that build are pre-existing flaky tests unrelated to this change (all passed on retry): Ready for review. |
|
Closing this PR because it has been inactive for more than 90 days. |
|
Superseded by #46894. It keeps the behaviour of this PR (line mode before the question is written, the owner's mode written back after) and moves the guard to |
What does this PR do?
Fixes #5267
Fixes #27542
node:readline,bun repl, and anything else that callssetRawMode(true)clearICANON,ECHO,ISIGandICRNLon the tty. While the tty is in that state:\r(no CR→LF translation)SIGINTalert(),confirm()andprompt()all block on a read that waits for\n, so calling any of them from inside a readline callback (or frombun repl) appeared to hang with no visible input and no way to interrupt.Repro
Fix
Wrap each dialog in an RAII
CookedStdinGuardthat, on POSIX,tcgetattr,ICANON | ECHO | ISIG | IEXTENandICRNL,tcsetattr(TCSANOW), andThe guard is installed before the prompt text is written: the line discipline applies
ICRNLat receive time, so a\rthat arrives between the prompt becoming visible andtcsetattrrunning stays a\rin the input queue and the canonical read never sees a line.tcgetattrfails on non-tty stdin, so piped input keeps its existing behaviour (the guard is inert). After the read, whatever mode the caller had installed (raw for readline/repl) is put back exactly as it was, so readline keeps working.On Windows the same guard wraps
StdinModeGuardwithENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUTset andENABLE_VIRTUAL_TERMINAL_INPUTcleared, applied to all three functions (previously onlyprompt()touched console mode, and only to clear the VT-input flag). The guard is inert when stdin is not a console.Why this fix rather than accepting
\rAn earlier attempt (#27544) taught the readers to accept a bare
\r. That unblocks Enter but leaves the user typing blind (no echo, no line editing) and unable to Ctrl+C. Restoring cooked mode for the duration of the read fixes all three at once and is exactly what a user expects from a dialog-style prompt.How did you verify your code works?
test/js/web/web-globals.test.js: a PTY-backed test drives readline →prompt()→confirm()→alert()→ readline again and asserts that each call returns the typed value, the typed characters are echoed, and raw mode is observably restored afterwards (readline's echo of the second answer appears exactly once; if the guard left stdin cooked the kernel would echo it too).test/js/bun/repl/repl.test.ts: awithTerminalRepltest typesprompt(...),confirm(...)andalert(...)at the REPL prompt and asserts each returns and the REPL keeps evaluating.confirmtests inweb-globals.test.jscontinue to pass on Linux and Windows.The PTY tests are POSIX-only; the Windows REPL terminal suite is already
todoIf(isWindows), and the existing piped-stdin tests verify the Windows guard is inert on non-console stdin.no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/repl/repl.test.ts test/js/web/web-globals.test.js