Skip to content

Reset cursor and mouse modes on every exit that restores termios - #41977

Open
robobun wants to merge 1 commit into
mainfrom
robobun/345aa4a0/tty-dec-modes-reset-on-exit
Open

robobun wants to merge 1 commit into
mainfrom
robobun/345aa4a0/tty-dec-modes-reset-on-exit

Conversation

@robobun

@robobun robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • bun_core::tty::DecModesGuard sets the modes, records them in one AtomicU8, resets them on drop. reset_active_dec_modes() writes the resets for what is still recorded with a plain write(2), from inside bun_restore_stdio() and windows_stdio::restore(), the hooks every exit path already calls (the Windows console Ctrl+C handler too).
  • The two pickers and Output::synchronized() use the guard. The --watch SIGINT exit calls stdio::restore() before _exit(0).
  • Verified: new pty tests in update_interactive_formatting.test.ts, init/init.test.ts and watch/watch.test.ts fail on 1.4.3 and pass here. The three files pass in full. Windows Ctrl+C checked by hand (Notes). Self-reviewed: 4 concerns raised, 4 addressed.

Background

  • termios is kernel tty state that tcsetattr restores. Cursor visibility and mouse reporting are DEC private modes: terminal emulator state that only a written CSI ? n h/l changes.
  • onExitSignal is Bun's SIGINT/SIGTERM handler: restore termios, re-raise. In raw mode Ctrl-C is a byte the picker reads, so only outside signals take this path.
  • bun --watch owns SIGINT for node parity: the watcher exits 0.

Fixes #30890. Supersedes #30891.

Notes
  • reset_active_dec_modes() is async-signal-safe: an atomic swap, a 64-byte stack buffer, write(2) with SIGTTOU blocked (a background job on a TOSTOP terminal gets EIO instead of a stop). It only writes when stdout is a terminal, so a FORCE_COLOR pipe consumer never gets a blocking write from a signal handler.
  • DecModesGuard::drop flushes before it clears the bits. Until the reset bytes reach the terminal a signal still finds the modes recorded. A reset written twice is harmless.
  • Windows: the picker keeps ENABLE_PROCESSED_INPUT, so a console Ctrl+C raises CTRL_C_EVENT. Ctrlhandler calls Bun__restoreWindowsStdio, which now writes the resets with WriteFile before the default handler ends the process. Checked by hand on Windows Server 2019: a sibling process in the same console reads GetConsoleCursorInfo after GenerateConsoleCtrlEvent ends bun init at the menu. bVisible is true with this change and false on 1.4.3. ConPTY output cannot show this (conhost re-renders and emits its own ?25l/?25h, and it does not turn a written \x03 into CTRL_C_EVENT), so there is no automated Windows test.
  • Relation to update -i: restore cursor on Ctrl+C (Windows) #30891: that PR fixed the picker case with a prompt-scoped module that installs its own sigaction handler (POSIX) and SetConsoleCtrlHandler (Windows), each with a hardcoded restore string and its own _exit. This change keeps one handler. The modes are data that the existing onExitSignal -> bun_restore_stdio() funnel reads, so the crash handler, Global::exit, reload_process and the Windows Ctrl handler are covered by the same line, and a new widget only has to hold a guard.
  • The --watch hunk is an independent regression from node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests) #34660 (the watch-mode SIGINT fast exit). It rides along because it is the same class (an exit path that skips the termios restore) and uses the same call onExitSignal already makes from signal context.
  • Reset raw mode on a tty opened on fd >= 3 when a signal exits the process #41505 and Exit on SIGINT/SIGTERM/SIGHUP when bun is PID 1 of a container #38843 edit the same bun_restore_stdio() / onExitSignal region. The overlap is textual only (adjacent lines); whichever lands second needs a one-line rebase.
  • ?2026 (synchronized output): Output::synchronized() now records the mode per frame, so a kill in the middle of a frame also writes ESC[?2026l. The render loop already flushed after every frame, so the flush in the guard's drop adds no write.
  • Also ran test/js/bun/terminal/terminal-spawn.test.ts, update_interactive_snapshots.test.ts and update_interactive_install.test.ts locally (Linux x64 debug+ASAN).

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/watch/watch.test.ts, test/cli/update_interactive_formatting.test.ts, test/cli/init/init.test.ts

The `bun update -i` picker hides the cursor and enables SGR mouse
reporting, and the `bun init` template menu hides the cursor. Both undid
that in a scope guard, which a SIGINT/SIGTERM exit never runs: the
exit-signal handler wrote the startup termios back and re-raised, leaving
the shell typing mouse escape sequences with no cursor.

Track the active DEC private modes in one atomic (bun_core::tty) and have
bun_restore_stdio(), the hook every exit path already goes through, write
the resets with a plain write(2). The pickers set the modes through a
DecModesGuard that registers them there.

`bun --watch` installs its own SIGINT handler that _exit(0)s from signal
context. It skipped the termios restore entirely, so a watched script in
raw mode left the shell raw. Restore before the _exit.
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 12 days. After that, they cost $0.25 per reviewed file.

Or wait 1 minute for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0a133e19-03c8-444c-8e68-d83414913ad3

📥 Commits

Reviewing files that changed from the base of the PR and between d745f03 and 3b805b2.

📒 Files selected for processing (9)
  • src/bun_core/output.rs
  • src/bun_core/tty.rs
  • src/jsc/PosixSignalHandle.rs
  • src/jsc/bindings/c-bindings.cpp
  • src/runtime/cli/init_command.rs
  • src/runtime/cli/update_interactive_command.rs
  • test/cli/init/init.test.ts
  • test/cli/update_interactive_formatting.test.ts
  • test/cli/watch/watch.test.ts

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

@github-actions github-actions Bot added the claude label Sep 8, 2026
@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:02 AM PT - Sep 8th, 2026

❌ @robobun, your commit 3b805b2 has 1 failures in Build #112870 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41977

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

bun-41977 --bun

@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI pending.

Reproduced on bun 1.4.3 (Linux x64) under a pty: bun update -i and bun init killed with SIGINT or SIGTERM at the menu end with ESC[?25l (and ESC[?1000h ESC[?1006h for the picker) and no matching resets. bun --watch raw.js + SIGINT exits 0 with ICANON/ECHO still off. The three new tests encode exactly that and fail on 1.4.3, pass on this branch.

Windows console Ctrl+C (#30890) checked by hand on Server 2019 with GenerateConsoleCtrlEvent + GetConsoleCursorInfo from a sibling process: cursor visible after exit with this branch, hidden on 1.4.3.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/runtime/cli/filter_run.rs — pre-existing: bun run --filter still writes ?2026h/?2026l as raw bytes into draw_buf via Output::SYNCHRONIZED_START/END, bypassing DecModesGuard/ACTIVE_DEC_MODES; a SIGINT/SIGTERM landing during write_all of a large redraw frame leaves the terminal in synchronized-output mode, same as the class this PR fixes for Output::synchronized(). Fix: record dec::SYNCHRONIZED_OUTPUT in ACTIVE_DEC_MODES around flush_draw_buf() (or route through a DecModesGuard) so bun_restore_stdio() writes ?2026l; if intentionally excluded, say so in the PR per REVIEW.md.

    Extended reasoning...

    redraw() (src/runtime/cli/filter_run.rs:508-648) builds draw_buf starting with \x1b[?2026h (line 514) and ending with \x1b[?2026l (line 640), then calls flush_draw_buf() -> write_all on stdout. With many filtered scripts producing output, draw_buf can be tens of KB, requiring several write(2) calls to the tty. A SIGINT delivered after the first write (which contains ?2026h) but before the last (which contains ?2026l) reaches onExitSignal -> bun_restore_stdio() -> Bun__ttyResetActiveDecModes(), but ACTIVE_DEC_MODES is 0 because this path never touched it, so no ?2026l is written and the emulator stays in deferred-render mode until its own timeout. On the base branch this behaves identically (pre-existing), but the PR's stated purpose is to route every DEC-mode flip through the atomic so the exit hook resets it, and its Notes section explicitly claims ?2026 coverage — this sibling writer of the same escape is not covered and not called out as excluded.

    Verification: pre-existing. src/runtime/cli/filter_run.rs is not in the diff (verified: git diff --name-only a3e0ab60..HEAD does not list it), and on the base branch nothing resets ?2026 on signal either — merging makes this path no worse. The finding is nonetheless the same bug class this PR fixes, and REVIEW.md requires "Fix the whole class in the same PR … every sibling site sharing the pattern".…

@gameroman

Copy link
Copy Markdown
Contributor

@robobun please resolve the merge conflicts

@robobun

robobun commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Overlap note: #46248 adds a one-row version of this table for the bracketed paste mode of bun repl: dec::BRACKETED_PASTE, DEC_MODES, ACTIVE_DEC_MODES and reset_active_dec_modes, with the same names and the same two call sites (bun_restore_stdio and windows_stdio::restore). It also adds mark_dec_modes_set and mark_dec_modes_reset for a caller that writes the sequences itself. The two PRs conflict in tty.rs on purpose. The second one to merge adds its rows to the table of the first.

This branch has not been deployed

No deployments
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.

Cursor does not reset when pressing Ctrl+C during interactive update

3 participants