Dev server ignores Ctrl+C when stdin was paused before it started (raw mode without resume)

Offen Anfängerfreundlich
#1,515 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 2 Tagen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
82/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
node.js, react-native, typescript, webpack

Rechercherichtung

Das Issue benennt setupInteractions.js und die Einrichtung des Raw-Modus als Einstiegspunkt. Lies diese Datei und verfolge, wie stdin konfiguriert wird, bevor der Keypress-Listener Ctrl+C und die Tastenkürzel r/d/j/a verarbeitet. Bestätige das Verhalten, wenn stdin vor dem Start pausiert wurde, und überprüfe anschließend, dass die Tastenkürzel und Ctrl+C nach der Änderung funktionieren.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

Describe the bug

When process.stdin has been paused before the dev server starts (which is what a CLI prompt framework's teardown does: input.unpipe() makes Node's Readable.unpipe() call pause() on its source), Ctrl+C does nothing: the server keeps running, Ctrl+Z will not suspend it, and only an external signal stops the process.

setupInteractions switches the terminal to raw mode and then reads Ctrl+C from a keypress listener. Raw mode clears the TTY's ISIG flag, so the kernel stops converting Ctrl+C into a real SIGINT — the 0x03 byte is now deliverable only as stream data, and a paused stream delivers nothing. The keypress listener never fires, so the process.on('SIGINT') handler right next to it is unreachable. Nothing in the start path resumes stdin.

   CLI prompt before the bundler starts
        |
        v
   prompt close(): input.unpipe() -> Readable.pause()
        v
   process.stdin [ PAUSED ]
        |
        v
   setupInteractions()
       stdin.setRawMode(true)      <-- clears ISIG: kernel SIGINT gone
       stdin.on('keypress', ...)   <-- never fires: stream paused
       process.on('SIGINT', ...)   <-- never runs

The same CLI flow works with Metro, which calls process.stdin.resume() immediately after entering raw mode. The r / d / j / a shortcuts are dead in the same session, which is the same paused stream.

Reproduction

Inline, self-contained, no repository needed — repro.mjs in a project with @callstack/repack and react-native installed. It recreates a prompt framework's teardown (pipe stdin, then unpipe) and starts the dev server interactively:

import { Writable } from 'node:stream';
import { createRequire } from 'node:module';
const req = createRequire(import.meta.url);

// what a prompt framework's close() does: pipe stdin, then unpipe it
const sink = new Writable({ write(_c, _e, cb) { cb(); } });
process.stdin.pipe(sink);
await new Promise((r) => setTimeout(r, 200));
process.stdin.unpipe(sink);      // <- leaves stdin paused

const commands = (await import('@callstack/repack/commands')).default;
const start = commands.find((c) => c.name === 'start').func;
await start([],
  { root: process.cwd(), reactNativePath: req.resolve('react-native'), platforms: { ios: {}, android: {} } },
  { port: 8081, interactive: true });

Steps to reproduce

  1. node repro.mjs in an interactive terminal.
  2. Wait for Server listening.
  3. Press Ctrl+C.

All measurements below were taken in a pseudo-terminal with a real Ctrl+C byte injected.

variant result
as written hangs; Ctrl+Z and Ctrl+\ also ignored; external SIGTERM kills it
delete the unpipe() line exits in 0.0s
add process.stdin.resume() before start exits in 0.0s, and the r reload shortcut works again

Reached the same state through real usage as well: any prompt shown before the bundler starts (device selection, port-in-use confirmation) leaves stdin paused, and the dev server then cannot be interrupted.

System Info

System:
  OS: macOS 26.5.2
  CPU: (10) arm64 Apple M1 Pro
  Memory: 439.89 MB / 32.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 26.7.0
    path: /opt/homebrew/bin/node
  Yarn:
    version: 1.22.19
    path: /opt/homebrew/bin/yarn
  npm:
    version: 11.19.0
    path: /opt/homebrew/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.16.2
    path: ~/.rbenv/shims/pod
IDEs:
  Xcode:
    version: 26.6/17F113
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.9
    path: /usr/bin/javac
npmPackages:
  "@react-native-community/cli":
    installed: 20.1.0
    wanted: 20.1.0
  react:
    installed: 19.2.3
    wanted: 19.2.3
  react-native:
    installed: 0.86.0
    wanted: 0.86.0
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: false

Tested on macOS with the rspack engine. The webpack engine uses the same setupInteractions file, so it should behave identically; Linux and Windows untested.

Re.Pack Version

5.4.1 — and setupInteractions.js is byte-identical in 5.4.2 and the current canary, so upgrading does not help.

Proposed fix

One line, where raw mode is entered:

   readline.emitKeypressEvents(process.stdin);
   process.stdin.setRawMode(true);
+  // Raw mode clears the TTY's ISIG flag, so Ctrl+C reaches us only as stream
+  // data. A prior unpipe() (e.g. from a CLI prompt framework) leaves the stream
+  // paused, which would silently swallow the keypress.
+  process.stdin.resume();

resume() rather than an additional SIGINT listener: raw mode is what removed the kernel signal, and a listener registered on a paused stream is equally unreachable. The caller that pauses stdin is not at fault — unpipe() pausing its source is documented Node behaviour, and every consumer that later takes over raw-mode input has to resume it.

Also worth hardening on the calling side: a CLI that starts the dev server in-process after showing its own prompts should resume stdin before handing over, so the failure cannot be reintroduced by a bundler plugin.

Context from the consuming project: https://github.com/Expensify/App/issues/105422

Vorherrschende Sprache
TypeScript
Sterne
1.9k
Forks
177
Ø Merge
2 T. 10 Std.
Gemergte PRs (30 T.)
23

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus callstack/repack

Alle Issues in callstack/repack

Ähnliche Issues

Weitere Issues zu TypeScript

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.