Repository navigation
fix: prevent process hang on Windows when stdin EOF does not propagate through Volta shim - #421
FelixIsaac wants to merge 1 commit into
Conversation
|
The root-cause diagnosis here is correct and bounding the read is the right direction. A few edge cases worth resolving before this lands, since this same Scope
Timeout semantics
Exit vs stdout flush
Tests
|
|
Thank you for the thorough review and clear pointers, @CorticalCode! All points have been addressed in commit e497d6e: 1. Scope (Bun Branch)
2. Timeout Semantics (Idle Timeout & Early JSON Resolution)
3. Exit vs. stdout Flush
4. Test Coverage
|
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate issues and one test-coverage gap remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR prevents Windows/Volta subprocess hangs by adding timeout-aware stdin handling and bounded process shutdown.
Changes:
- Adds cross-runtime stdin handling and timeout safeguards.
- Flushes stdout and schedules process termination.
- Adds stdin and subprocess regression tests.
File summaries
| File | Summary | Findings |
|---|---|---|
src/utils/stdin.ts |
Implements stdin handling and stdout flushing. | Moderate (3 votes): Default timeout is 1 second instead of the intended 3 seconds. Moderate (2 votes): writableLength can add unnecessary 200 ms latency. |
src/utils/__tests__/stdin.test.ts |
Adds stdin and subprocess regression coverage. | Nit (3 votes): The Node subprocess test silently skips when the build artifact is unavailable, leaving the regression path untested. |
src/ccstatusline.ts |
Integrates the utilities and bounded shutdown. | No findings. |
Review details
Suppressed comments (2)
src/utils/tests/stdin.test.ts:296
- Returning when
dist/ccstatusline.jsis absent makes this Node integration test silently pass without running. The CI test job runsbun testbefore the separate build job, so this slow-chunk stdin case is skipped in normal CI and cannot catch regressions in the bundled Node path; build the artifact in the test job or exercise the source instead of returning.
if (!fs.existsSync(scriptPath)) {
return;
src/utils/stdin.ts:92
- The Node implementation has the same 1-second default instead of the described 3-second bail window. A slow inter-chunk delay can therefore truncate an otherwise valid status payload before JSON.parse runs; this default should match the Bun path and the intended 3000 ms timeout.
const idleTimeoutMs = options?.idleTimeoutMs ?? 1000;
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
| export async function readStdinBun(options?: ReadStdinOptions): Promise<string> { | ||
| const idleTimeoutMs = options?.idleTimeoutMs ?? 1000; |
| const needsDrain = stdout.writableNeedDrain | ||
| || stdout.writableLength > 0; |
| if (!fs.existsSync(scriptPath)) { | ||
| return; |
|
Heads-up in case it's useful: #548 is working on the same thing, bounding the same |
|
Thanks for looping us in and comparing the two PRs, @eric-engberg! And kudos to @filipemb for digging into the same issue in #548. Comparing notes on the two implementations, here is the rationale behind the design choices in #421:
That said, #548 has a great addition with I’m very happy to incorporate |
2734f2e to
2630e6d
Compare
|
Update: I've gone ahead and rebased this PR cleanly onto the latest |
…e through Volta shim
2630e6d to
46608bc
Compare
|
Rebased cleanly onto latest |
Problem
On Windows with a Volta-managed ccstatusline, spawning ccstatusline as a child subprocess causes it to hang indefinitely. The process chain:
The
for await (const chunk of process.stdin)loop never terminates because stdin EOF from the parent does not propagate throughcmd.exe → bash → voltaon Windows. Claude Code's statusline lifecycle abandons (does not kill) processes that take too long — so each refresh leaks one hung node.exe process.Real-world impact: 961 leaked node.exe processes, 2.1 GB RAM, 90.5% kernel-mode CPU from Windows scheduler overhead managing ~1000 sleeping processes.
This is a known Windows/Node.js behavior:
process.stdintochild.stdinleaves behind an open handle nodejs/node#32291 — piping stdin to child.stdin leaves open handlespawnwith stdio/stdin 'pipe' options causes pwsh shell to hang after execution nodejs/node#52364 — spawn + stdin:pipe causes pwsh to hang (2024)child_process.execincmd(Windows) volta-cli/volta#1199 — Volta shims + child_process broken on Windows cmdNote: the normal use case (
"command": "ccstatusline"direct from Claude Code) is not affected — Claude Code pipes directly, single process layer, EOF works correctly.Fix
Fix 1 —
readStdin(): Replacefor awaitwith event-based reading + 3s bail timeout. When EOF never arrives (Volta chain), the timeout destroys stdin and resolves the Promise. Data already accumulated inchunks[]is preserved and returned normally — no data loss.Fix 2 —
main(): Add explicitprocess.exit(0)afterrenderMultipleLines. Claude Code's "abandon not kill" lifecycle means any residual async handles from rendering keep the process alive indefinitely. An explicit exit prevents accumulation.Build note
Bun was not available in this environment so
dist/was not rebuilt. The source change is insrc/ccstatusline.tsonly — maintainer build required before publishing.