Skip to content

land #8861: deliver piped stdin to -p - #8863

Merged
proggeramlug merged 2 commits into
mainfrom
land-8861-stdin
Aug 26, 2026
Merged

land #8861: deliver piped stdin to -p#8863
proggeramlug merged 2 commits into
mainfrom
land-8861-stdin

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Lands #8861 (fix(stdin): deliver piped stdin to -p) with a missing GC root fixed.

The PR itself fixes three stacked defects that together made echo hi | claude -p "…" print nothing while exiting 0. Its diagnosis holds up on review:

  1. process.stdin.once(…) was never loweredperry-hir matched only ("stdin","on") | ("stdin","addListener"), so once fell through to the generic member-call path and never registered with the fd-0 reader. Decisive, because the print-mode reader awaits race(stdin.once("end"), timeout(3000)); with once dropped the end half could never win, and the timer is unref'd so nothing kept the loop alive.
  2. Only one stdin.on("end") listener survived — they shared readline's single-slot CLOSE_CALLBACK; the bundle registers three. Now a list fired in registration order. Verified the pump uses std::mem::take, so each fires exactly once and all fire in order.
  3. Pull-mode bytes were discardedon("readable") + read() set neither the raw nor the flowing flag, so bytes were routed to readline's line queue that read() never drains.

Fix applied while landing: a missing GC root

gc_runtime_root_holders flagged the new STDIN_END_CALLBACKS: Mutex<Vec<i64>> — it stores closure pointers and no registered scanner reached it.

This is a real missing root, not a classification gap. scan_readline_roots_mut already visits DATA_CALLBACKS, KEYPRESS_CALLBACKS, READABLE_CALLBACKS and the single-slot CLOSE_CALLBACK this list replaces — the new list was simply not added. Its closures are reachable only from there between registration and EOF, so a collection in that window would leave stale pointers that the pump then calls. Added &STDIN_END_CALLBACKS to the scanner.

That is exactly the failure mode CLAUDE.md describes for an unrooted runtime-side cache: it goes bad at collection #0 and stays bad.

Also applied

  • check_file_size: readline/mod.rs reached 2049 lines. Extracted its #[cfg(test)] mod tests (262 lines) to readline/mod_tests.rs; now 1796.
  • cargo fmt --all.

Note on once semantics

The lowering routes once to the same registry as on. once("end") gets true one-shot behaviour because the pump takes the list, but once("data") / once("readable") behave like on — a deviation from Node that the PR documents inline rather than hides. That is a net improvement (previously once did nothing at all), but it is a residual worth its own issue.

Validation

  • all 30 lint-job gates pass
  • new process_stdin_once_lowering test: 2 passed
  • perry-stdlib 122, perry-hir 339, perry-runtime 2702 — all 0 failed

Summary by CodeRabbit

  • Bug Fixes
    • Fixed process.stdin.once(...) so EOF and other stream events are delivered correctly.
    • Improved stdin handling for end, close, readable, and data events.
    • Preserved multiple end/close callbacks and invoked them in registration order.
    • Improved paused and flowing input modes, including cooked-mode data delivery.
    • Ensured pending stdin callbacks keep the event loop active until processed.
  • Tests
    • Added comprehensive coverage for stdin events, lifecycle cleanup, pause/resume behavior, and readline input handling.

Ralph Küpper added 2 commits August 26, 2026 16:11
…nd` listener, buffer pull-mode bytes

`echo hi | claude -p "…"` produced NOTHING (exit 0, zero bytes on both
streams) where node prints the result. Three independent defects in the
`process.stdin` path stacked up; each is fixed here.

1. `process.stdin.once(…)` was never lowered.
   perry-hir matched only `("stdin","on") | ("stdin","addListener")`, so
   `once` fell through to the generic member-call path and never reached
   `js_readline_stdin_on` — the listener was never registered with the
   fd-0 reader and simply never fired. Claude Code's print-mode reader is
   `stdin.on("data", acc)` + `await race(stdin.once("end"), timeout(3000))`,
   so with `once` dropped the `end` half could never win; the race fell to
   the timer, and because that timer is unref'd nothing kept the event loop
   alive and the process exited silently.

2. Only ONE `stdin.on("end")` listener survived.
   They shared readline's single-slot `CLOSE_CALLBACK` ("only one terminal
   close listener is supported"), so each registration clobbered the
   previous. The bundle registers three; the one that resolves its
   read-stdin promise was dropped. Replaced with `STDIN_END_CALLBACKS`, a
   list fired in registration order, honoured by the keep-alive predicate
   and by `removeListener`.

3. Pull-mode (`on("readable")` + `read()`) bytes were discarded.
   The fd-0 reader routed bytes by mode: raw and `data`-flowing went to
   `PENDING_DATA`, everything else to `PENDING_LINES` — readline's *line*
   queue, which `process.stdin.read()` never drains. Paused/pull mode set
   neither flag, so its bytes were consumed off fd 0 and thrown away and
   `read()` returned null forever. New `STDIN_PULL_MODE` flag, set while a
   `readable` listener exists, routes those bytes (and the EOF trailing
   chunk) to the buffer `read()` actually drains. This is the same hazard
   the `PENDING_LINES` comment already records for the `data` case (#5227),
   left unfixed for `readable`.

Verified against node with the real bundle and with focused replicas:
  * `on("readable")+read()` — was "", now "hello pipe" (node: "hello pipe")
  * three `on("end")` listeners — now all fire, in order, with the data
  * a faithful replica of the `-p` reader stops taking the 3s timeout path

Tests: `process_stdin_once_lowering.rs` (sabotage-checked — both cases fail
without the lowering arm), plus `every_stdin_end_listener_fires` and
`readable_listener_enables_pull_mode` in the readline suite. Full
perry-stdlib readline suite green (19/19).

Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
@proggeramlug
proggeramlug merged commit 35e3136 into main Aug 26, 2026
16 of 17 checks passed
@proggeramlug
proggeramlug deleted the land-8861-stdin branch August 26, 2026 14:52
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3232c8c-68d8-45c2-b7eb-11310025cd95

📥 Commits

Reviewing files that changed from the base of the PR and between 926d957 and d771dc7.

📒 Files selected for processing (6)
  • crates/perry-hir/src/lower/expr_call/module_class_static.rs
  • crates/perry-hir/tests/process_stdin_once_lowering.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/mod_tests.rs
  • crates/perry-stdlib/src/readline/pump.rs
  • crates/perry-stdlib/src/readline/test_support.rs

📝 Walkthrough

Walkthrough

process.stdin.once(...) now uses the stdin listener lowering path. Readline now supports multiple end/close callbacks, readable pull mode, expanded cooked-input routing, ordered EOF dispatch, state cleanup, and comprehensive tests.

Changes

Stdin listener support

Layer / File(s) Summary
Lower stdin once-listeners
crates/perry-hir/src/lower/expr_call/module_class_static.rs, crates/perry-hir/tests/process_stdin_once_lowering.rs
process.stdin.once(...) now lowers to ProcessStdinOn. Regression tests cover once, on, and addListener.
Manage stdin listener state
crates/perry-stdlib/src/readline/mod.rs
Readline stores multiple end and close callbacks, tracks readable pull mode, routes cooked input, and updates listener removal and GC scanning.
Dispatch and reset stdin callbacks
crates/perry-stdlib/src/readline/pump.rs, crates/perry-stdlib/src/readline/test_support.rs, crates/perry-stdlib/src/readline/mod_tests.rs
The pump invokes pending EOF callbacks in registration order and keeps the event loop active while callbacks remain. Reset logic and tests cover listener, queue, lifecycle, input, and raw-mode behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProcessStdinOn
  participant ReadlineListeners
  participant ReadlinePump
  ProcessStdinOn->>ReadlineListeners: register stdin listener
  ReadlineListeners->>ReadlinePump: enable pull mode or EOF observation
  ReadlinePump->>ReadlineListeners: deliver input and end/close callbacks
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch land-8861-stdin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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