Skip to content

fix(cli): exit cleanly on terminal hangup (read EIO) during interactive prompts#650

Merged
rayhanadev merged 4 commits into
mainfrom
ray/f762b202
Jun 2, 2026
Merged

fix(cli): exit cleanly on terminal hangup (read EIO) during interactive prompts#650
rayhanadev merged 4 commits into
mainfrom
ray/f762b202

Conversation

@rayhanadev
Copy link
Copy Markdown
Member

@rayhanadev rayhanadev commented Jun 2, 2026

Why

Catches the Error: read EIO crash reported in Sentry (REACT-DOCTOR-S).

The only thing that reads stdin is the interactive prompts UI (e.g. the multiselect "Select projects" prompt in a monorepo), which opens the TTY in raw mode and never attaches an 'error' listener. When the terminal/PTY backing that prompt goes away mid-read — closing the tab, the parent shell exiting, a dropped SSH/tmux/screen session, or the machine sleeping/waking — the next read() returns EIO. With no listener on process.stdin, Node escalates that stream error into a fatal uncaught exception, which the Sentry onuncaughtexception integration reports as a crash (level: fatal, handled: no). The stack is pure Node internals (TTY.onStreamRead), confirming an environmental terminal teardown rather than a logic bug.

Before (no stdin error listener — an environmental hangup crashes and is reported to Sentry):

process.on("SIGINT", exitGracefully);
process.on("SIGTERM", exitGracefully);
unrefStdin();
// stdout EPIPE is guarded, but stdin has no 'error' handler →
// `read EIO` becomes a fatal uncaught exception.

After (stdin is guarded; a hangup exits cleanly, genuine errors still report):

unrefStdin();
guardStdin(); // process.stdin.on("error", …): exit 129 on EIO/ENXIO, re-throw otherwise

What changed

  • Added guardStdin() (src/cli/utils/guard-stdin.ts), armed at startup in cli/index.ts next to unrefStdin() and the existing stdout EPIPE guard it mirrors.
  • On a terminal-hangup errno (EIO/ENXIO) the CLI exits cleanly with code 129 (POSIX 128 + SIGHUP), so the run ends like an interruption instead of a crash.
  • Re-throws any other stdin error, so genuine stdin failures keep funneling to the crash reporter exactly as before.
  • Added TERMINAL_HANGUP_EXIT_CODE = 129 to constants.ts, alongside the existing SIGINT_EXIT_CODE.
  • Adds tests for: exits with 129 on EIO/ENXIO, re-throws (never exits) on EPIPE/EACCES/no-code, and that guardStdin() registers the handler.

Eval results

RDE not run — this is a CLI stdin/teardown fix, not a lint rule, so the rule-eval harness doesn't apply.

Test plan

  • pnpm test (react-doctor) — 170 passed, incl. new tests/guard-stdin.test.ts
  • pnpm typecheck — clean
  • pnpm format:check — clean; pnpm lint — clean (only pre-existing fixture warnings)

Made with Cursor


Note

Low Risk
Small, startup-only stdin listener with narrow errno handling; non-hangup errors unchanged; covered by unit tests.

Overview
Adds a startup stdin error guard (parallel to the existing stdout EPIPE handler) so losing the TTY while an interactive prompts read is in progress no longer becomes a fatal uncaught read EIO / Sentry crash.

guardStdin() wires process.stdin 'error' to handleStdinError: EIO / ENXIOprocess.exit(129) via new TERMINAL_HANGUP_EXIT_CODE; any other code is re-thrown so real stdin failures still hit the crash reporter. guardStdin() is invoked from cli/index.ts right after unrefStdin(), before commands run. Tests cover hangup vs non-hangup errno and listener registration; changeset documents the patch.

Reviewed by Cursor Bugbot for commit 134f305. Bugbot is set up for automated code reviews on this repo. Configure here.

…ve prompts

Guard process.stdin with an error handler so a vanished terminal/PTY
(closed tab, dropped SSH/tmux session, sleep/wake) that raises `read EIO`
on the raw-mode stdin handle exits cleanly (code 129) instead of crashing
as a fatal uncaught exception and reporting to Sentry. Non-hangup stdin
errors are re-thrown, preserving existing crash reporting.

Co-authored-by: Cursor <cursoragent@cursor.com>
@react-doctor-evals
Copy link
Copy Markdown

react-doctor-evals Bot commented Jun 2, 2026

Parity OK — no diagnostic differences.

Baseline: main · This PR: ray/f762b202 (134f305)

ℹ️ Re-run this parity check by commenting /rde parity on this PR.

trace 12b4a050eaff6d27ba2967eb59c19135 · rde

Align the guardStdin() call-site comment with the adjacent stdout EPIPE
handler (same class of Node stream-error workaround), per the repo's
"// HACK:" convention.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@650
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@650
npm i https://pkg.pr.new/react-doctor@650

commit: 134f305

rayhanadev and others added 2 commits June 2, 2026 11:55
Extract the duplicated process.exit stub into a named helper and remove the
unused `syscall` field from the fake stdin error (handleStdinError only reads
`code`).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/react-doctor/src/cli/index.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

React Doctor

React Doctor found 5 files changed in this pull request, but none matched the files covered by its enabled checks.

Scope: 5 files changed on ray/f762b202 vs. main.

View workflow run

Generated by React Doctor. Questions? Contact founders@million.dev.

@rayhanadev rayhanadev merged commit 3cc9971 into main Jun 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant