Skip to content

fix: bound stdin read so the process cannot outlive its render - #548

Open
filipemb wants to merge 1 commit into
sirmalloc:mainfrom
filipemb:fix/stdin-timeout
Open

fix: bound stdin read so the process cannot outlive its render#548
filipemb wants to merge 1 commit into
sirmalloc:mainfrom
filipemb:fix/stdin-timeout

Conversation

@filipemb

Copy link
Copy Markdown

Problem

readStdin() iterates process.stdin until EOF. When the host writes the status JSON but keeps the write end open, that iteration never settles: the render finishes, but the process stays resident. On a long-lived host with a short refresh interval these accumulate — I found 77 of them alive, the oldest at 257 hours, holding ~2.3 GB.

Repro — spawn the CLI, write the payload, never call end():

const p = spawn(process.execPath, ['dist/ccstatusline.js'], { stdio: ['pipe', 'pipe', 'pipe'] });
p.stdin.write(JSON.stringify({ session_id: 't', model: { display_name: 'Opus' } })); // no end()
setTimeout(() => console.log('still alive'), 12000);

Before: still alive, indefinitely. After: exits in ~5s, having rendered the line.

Fix

Race the read against a timeout — 5000 ms default, CCSTATUSLINE_STDIN_TIMEOUT_MS to override. On timeout it resolves with whatever already arrived instead of discarding it: the payload is written in one shot, so a missing EOF means the EOF is missing, not the data, and the status line still renders correctly. finally clears the timer and pauses/unrefs the stream.

readStdin moves to src/utils/stdin.ts because the entrypoint runs main() on import, which makes the function untestable where it was.

Tests

src/utils/__tests__/stdin.test.ts covers three cases: TTY returns null, a normal close reads the payload, and no-EOF resolves with what arrived. The third one fails on main (times out) and passes with this change. The rest of the suite shows no new failures.

readStdin() iterated process.stdin until EOF. A host that writes the status
JSON but keeps the write end open leaves that iteration suspended forever:
the line renders, but the process stays resident. With a short refresh
interval these accumulate.

Race the read against a timeout (5s default, CCSTATUSLINE_STDIN_TIMEOUT_MS
to override) and resolve with whatever already arrived — the payload is
written in one shot, so a missing EOF does not mean missing data and the
status line still renders. Clear the timer and pause/unref the stream on the
way out.

readStdin moves to src/utils/stdin.ts because the entrypoint runs main() on
import, which makes the function untestable in place.
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