-
-
Notifications
You must be signed in to change notification settings - Fork 158
fix(stdin): restore the end/close arm on the listener provider path (#8861 regression) #8880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| Restored the `end`/`close` arm on the stdin listener **provider** path, which | ||
| was lost when #8861 was batch-landed. Without it `echo hi | claude -p "…"` | ||
| never completes. | ||
|
|
||
| #8861 fixed piped stdin for `-p` by teaching three layers about `end` | ||
| listeners. Two of them landed: the GC rooting of `STDIN_END_CALLBACKS`, and | ||
| `STDIN_PULL_MODE`. The third — the `"end" | "close"` arm in `stdin_on_op` — | ||
| did not, and it is the one that made the fix work. | ||
|
|
||
| `stdin_on_op` is the provider that the stdin object's native | ||
| `on` / `once` / `addListener` methods delegate to: every registration that is | ||
| **not** codegen's literal `process.stdin.x(…)` shape. That means an alias | ||
| (`const s = process.stdin; s.once("end", …)`) or stdin passed as a parameter | ||
| (`helper(process.stdin)`). Claude Code's print-mode reader is exactly the | ||
| parameter form — `X71(process.stdin, 3000)`, then `stream.once("end", …)` on | ||
| the parameter — so those registrations fell into `_ => return` and were | ||
| silently discarded. The `end` half of the reader's | ||
| `race(once("end"), timeout(3000))` could never win. | ||
|
|
||
| `stdin_off_op` gets the mirror arm. #8864 removed the `end`/`close` clause from | ||
| the removal path, so a provider-registered listener could be added but never | ||
| removed — `removeListener` / `off` leaked it and the pump kept a stale callback | ||
| pointer. | ||
|
|
||
| The two tests that guarded this — `provider_path_registers_end_listeners` and | ||
| `provider_path_removes_end_listeners` — were dropped alongside the arm, which is | ||
| why CI stayed green through the regression. Both are restored here. They assert | ||
| the provider entry points (`stdin_on_op` / `stdin_off_op`) **directly** rather | ||
| than going through the `js_readline_stdin_on` extern, because the extern path | ||
| kept working the whole time and therefore cannot catch this. Sabotage-checked: | ||
| deleting the arm again fails `provider_path_registers_end_listeners`. | ||
|
|
||
| Measured on the claude-code 2.1.112 bundle, `echo hello | cc -p "…"`: | ||
|
|
||
| | build | result | | ||
| |---|---| | ||
| | before this fix | 4/4 hang (120 s timeout, no output) | | ||
| | with the arm restored | completes in ~7 s | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -376,6 +376,27 @@ extern "C" fn stdin_on_op(name_ptr: *const u8, name_len: usize, cb: i64, _once: | |
| v.push(cb); | ||
| } | ||
| } | ||
| // #8861: `end`/`close` MUST be handled here, not only in the syntactic | ||
| // `js_readline_stdin_on` extern. | ||
| // | ||
| // This provider is what the stdin OBJECT's native `on`/`once`/ | ||
| // `addListener` methods delegate to — i.e. every registration that does | ||
| // not match codegen's literal `process.stdin.x(…)` pattern: an alias | ||
| // (`const s = process.stdin; s.once("end", …)`) or stdin passed as a | ||
| // parameter (`helper(process.stdin)`). That is exactly what Claude | ||
| // Code's print-mode reader does: `X71(process.stdin, 3000)`, then | ||
| // `stream.once("end", …)` on the parameter inside. | ||
| // | ||
| // Falling into the `_ => return` below silently discards those | ||
| // listeners. Node fires the direct, aliased and parameter forms alike; | ||
| // without this arm perry fires only the direct one, so the `end` half | ||
| // of the reader's `race(once("end"), timeout(3000))` can never win and | ||
| // `echo hi | claude -p …` never completes. | ||
| "end" | "close" => { | ||
| if let Ok(mut v) = STDIN_END_CALLBACKS.lock() { | ||
| v.push(cb); | ||
| } | ||
| } | ||
|
Comment on lines
+395
to
+399
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
knowledge=/tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc
printf '%s\n' '--- applicable repository knowledge ---'
for f in "$knowledge"/*/*.md; do
[ -f "$f" ] || continue
printf '\n--- %s ---\n' "$f"
head -80 "$f"
done
printf '%s\n' '--- readline outline ---'
ast-grep outline crates/perry-stdlib/src/readline/mod.rs
printf '%s\n' '--- relevant symbols ---'
rg -n -C 8 'STDIN_END_CALLBACKS|scan_readline_roots_mut|js_closure_call0|readable|keypress|data|end.*close|EOF|eof' crates/perry-stdlib/src/readline/mod.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
file=crates/perry-stdlib/src/readline/mod.rs
printf '%s\n' '--- scanner and registration ---'
sed -n '230,430p' "$file"
printf '%s\n' '--- pump module ---'
sed -n '1810,2100p' "$file"
printf '%s\n' '--- exact callback call sites ---'
rg -n -C 12 'js_closure_call0|STDIN_END_CALLBACKS|END_CALLBACKS|mem::take|RuntimeHandleScope|root_' "$file"
printf '%s\n' '--- bound js_closure_call0 definitions/imports ---'
rg -n -C 10 'pub(\s+unsafe)?\s+extern.*js_closure_call0|fn js_closure_call0|js_closure_call0' crates/perry-runtime crates/perry-stdlibRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
readline=crates/perry-stdlib/src/readline/mod.rs
calln=crates/perry-runtime/src/closure/dispatch/calln.rs
printf '%s\n' '--- readline scanner and listener operations ---'
sed -n '235,430p' "$readline"
printf '%s\n' '--- readline pump symbol locations ---'
rg -n '^(pub )?(unsafe )?fn |process_pending|STDIN_END_CALLBACKS|READABLE_CALLBACKS|DATA_CALLBACKS|KEYPRESS_CALLBACKS|RuntimeHandleScope|mem::take' "$readline" | sed -n '1,180p'
printf '%s\n' '--- readline pump and tests ---'
sed -n '1810,2050p' "$readline"
printf '%s\n' '--- js_closure_call0 ---'
sed -n '1,45p' "$calln"Repository: PerryTS/perry Length of output: 15812 🏁 Script executed: #!/bin/bash
set -eu
pump=crates/perry-stdlib/src/readline/pump.rs
readline=crates/perry-stdlib/src/readline/mod.rs
printf '%s\n' '--- pump outline and size ---'
wc -l "$pump"
ast-grep outline "$pump"
printf '%s\n' '--- pump callback dispatch ---'
rg -n -C 18 'STDIN_END_CALLBACKS|DATA_CALLBACKS|KEYPRESS_CALLBACKS|READABLE_CALLBACKS|mem::take|RuntimeHandleScope|js_closure_call' "$pump"
printf '%s\n' '--- end-listener removal ---'
sed -n '425,450p' "$readline"
printf '%s\n' '--- readline GC comments ---'
sed -n '210,240p' "$readline"Repository: PerryTS/perry Length of output: 20330 Root drained When 🤖 Prompt for AI Agents |
||
| _ => return, | ||
| } | ||
| try_register_pump(); | ||
|
|
@@ -410,6 +431,21 @@ extern "C" fn stdin_off_op(name_ptr: *const u8, name_len: usize, cb: i64) { | |
| v.retain(|r| *r != cb); | ||
| } | ||
| } | ||
| // Mirror of the `end`/`close` arm in `stdin_on_op`. Without it a | ||
| // listener registered through the provider path can be added but never | ||
| // removed, so `removeListener`/`off` leaks it and the pump keeps a | ||
| // stale callback pointer. | ||
| "end" | "close" => { | ||
| if let Ok(mut v) = STDIN_END_CALLBACKS.lock() { | ||
| v.retain(|r| *r != cb); | ||
| } | ||
| CLOSE_CALLBACK.with(|slot| { | ||
| let mut slot = slot.borrow_mut(); | ||
| if *slot == Some(cb) { | ||
| *slot = None; | ||
| } | ||
|
Comment on lines
+442
to
+446
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository conventions ---'
find /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc -type f -name '*.md' -maxdepth 3 -print \
| while read -r f; do
case "$f" in
*/learnings/*|*/readline*|*/stdlib*|*/crates*|*/general*|*/global*) printf '%s\n' "$f";;
esac
done
printf '%s\n' '--- candidate source structure ---'
ast-grep outline crates/perry-stdlib/src/readline/mod.rs
printf '%s\n' '--- relevant callback definitions and uses ---'
rg -n -C 8 'CLOSE_CALLBACK|STDIN_END_CALLBACKS|stdin_off_op|scan_readline_roots_mut|js_closure_call0|end callback|close callback' crates/perry-stdlib/src/readline/mod.rsRepository: PerryTS/perry Length of output: 22550 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- callback registration/removal implementation ---'
sed -n '324,454p' crates/perry-stdlib/src/readline/mod.rs
printf '%s\n' '--- pump module and callback dispatch ---'
sed -n '1810,1835p' crates/perry-stdlib/src/readline/mod.rs
find crates/perry-stdlib/src/readline -maxdepth 2 -type f -print
rg -n -C 10 'STDIN_END_CALLBACKS|CLOSE_CALLBACK|stdin_off_op|js_readline_stdin_remove_listener|removeListener|off\\(' crates/perry-stdlib/src/readline crates/perry-runtime crates/perry-codegen
printf '%s\n' '--- applicable convention and readline-related learning excerpts ---'
for f in /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc/conventions/*.md \
/tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc/learnings/*readline*.md; do
[ -f "$f" ] && { echo "### $f"; cat "$f"; }
doneRepository: PerryTS/perry Length of output: 6156 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- pump dispatch ---'
ast-grep outline crates/perry-stdlib/src/readline/pump.rs
rg -n -C 14 'STDIN_END_CALLBACKS|CLOSE_CALLBACK|js_closure_call0|js_readline_process_pending|stdin' crates/perry-stdlib/src/readline/pump.rs
printf '%s\n' '--- stdin listener bindings and removal callers ---'
rg -n -C 12 'js_register_stdin_listener_ops|stdin_off_op|js_readline_stdin_remove_listener|stdin_remove_listener|removeListener|addListener|\\.off|\\.removeListener' crates
printf '%s\n' '--- applicable convention files ---'
for f in /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc/conventions/*.md; do
echo "### $f"
cat "$f"
doneRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- pump file size and structure ---'
wc -l crates/perry-stdlib/src/readline/pump.rs
ast-grep outline crates/perry-stdlib/src/readline/pump.rs
printf '%s\n' '--- end/close dispatch in pump ---'
rg -n -C 18 'STDIN_END_CALLBACKS|CLOSE_CALLBACK|END_CALLBACK|close callback|end callback|js_closure_call0' crates/perry-stdlib/src/readline/pump.rs
printf '%s\n' '--- exact stdin native method installation ---'
sed -n '900,1015p' crates/perry-runtime/src/os_process_streams.rs
printf '%s\n' '--- exact stdin add/remove implementations ---'
rg -n -C 20 'process_stdin_add_listener|process_stdin_remove_listener|stdin_native_method' crates/perry-runtime/src/os_process_streams.rsRepository: PerryTS/perry Length of output: 24513 Keep 🤖 Prompt for AI Agents |
||
| }); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the Markdown issue reference syntax.
Line [5] starts with
#8861and triggers markdownlint MD018 because an ATX heading requires a space after#. Change it toIssue#8861fixed...so the changelog lint warning is removed.🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 5-5: No space after hash on atx style heading
(MD018, no-missing-space-atx)
🤖 Prompt for AI Agents
Source: Linters/SAST tools