Skip to content

fix(cli): read stdin lazily so open pipes don't hang the CLI - #1330

Open
l2ysho wants to merge 2 commits into
masterfrom
claude/apify-cli-1206-verify-37b57c
Open

fix(cli): read stdin lazily so open pipes don't hang the CLI#1330
l2ysho wants to merge 2 commits into
masterfrom
claude/apify-cli-1206-verify-37b57c

Conversation

@l2ysho

@l2ysho l2ysho commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note

TL;DR — The CLI read stdin at startup for every command, so a pipe that stays open hung it forever. Now it only reads stdin when a command wants it, and a read nobody asked for gives up when the pipe goes quiet.

Closes #1206.

The startup read

_shared.ts read stdin at module scope (export const cachedStdinInput = await readStdin()), so every command blocked on stdin whether or not it uses it. #1294 fixed the socket case (a spawned child's stdio: 'pipe'), but a named pipe has no wait deadline — the read never ended and the command never even ran, so 1.8.0 still hangs with no output at all:

sleep 60 | apify --version   # 1.8.0: hangs, no output

Read stdin only where it's asked for: a - arg/flag, apify run without --input, push-data/push-items without an item arg. readStdin() memoizes, since stdin can only be drained once.

Confirmations keyed off hasData, which the eager read flipped to false after draining. Without that read it stays true, so apify <confirm-cmd> < /dev/null would try to prompt on a non-TTY — gated on isTTY instead.

The implicit read

That left one path still hanging. apify run without --input falls back to stdin, and waited there for an end a named pipe never reaches. actors call and actors start share it. Two ways to hang:

apify run < /some/fifo                          # nothing ever sent
(printf '{"a":1}'; sleep 30) | apify run        # sent, then held open

A read the user never asked for now stops at the first quiet gap: each chunk restarts a 2s clock, and when stdin goes silent the CLI uses what arrived. An explicit - is unchanged — it waits for the writer to close, the way cat does.

Trade-off: a writer slower than ~2.5s to its first byte is now ignored (curl slow-endpoint | apify run). Truncation mid-stream stays loud, since the JSON fails to parse. 2s is a judgement call, not a derived number.

Verification

Measured against a build of master and a build of this branch.

stdin command before after
open pipe, no data --version hang, no output 405 ms
open pipe, with data --version hang, no output 654 ms
closed pipe --version exits exits
spawn stdio: 'pipe' (#1294's case) --version exits exits
open pipe, silent run in a project hang 2621 ms, no input
open pipe, sends then holds run in a project hang 2619 ms, input used
writer delays 0 / 1 / 2 s, then closes run in a project input used input used
writer delays 4 s, then closes run in a project input used input dropped, 2603 ms

Three new e2e cases, all skipped on Windows (no mkfifo). Each fails on the pre-fix build at the 15s deadline and passes after:

  • FIFO on stdin, empty dir — the startup read
  • FIFO on stdin, real project, silent — the implicit read
  • FIFO on stdin, real project, sends then holds open — asserts the Actor received the input

pnpm run test:local green (540 passed). pnpm run test:e2e:local green (44 passed). No dependency or install-size change.

🤖 Generated with Claude Code

@github-actions github-actions Bot added this to the 147th sprint - Tooling team milestone Aug 14, 2026
@github-actions github-actions Bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Aug 14, 2026
@l2ysho
l2ysho requested a review from vladfrangu August 14, 2026 11:00
@l2ysho l2ysho added t-builders Issues owned by the Builders team. adhoc Ad-hoc unplanned task added during the sprint. and removed t-tooling Issues with this label are in the ownership of the tooling team. labels Aug 14, 2026
@l2ysho
l2ysho marked this pull request as ready for review August 14, 2026 11:02
@l2ysho
l2ysho requested a review from DaveHanns as a code owner August 14, 2026 11:02
Every command paid for stdin: `_shared.ts` did `await readStdin()` at module
scope, so `apify --version` blocked on a stream it never reads. With a named
pipe (no wait deadline, unlike the socket a spawned child gets) the read never
ended and the command never even ran.

Read stdin only when a command asks for it — a `-` arg/flag, `apify run`
without `--input`, `push-data`/`push-items` without an item. Result is
memoized, since stdin can only be drained once.

Confirmations previously keyed off `hasData`, which the eager read flipped to
false once stdin was drained. Without that read the flag stays true, so
`apify <confirm-cmd> < /dev/null` would try to prompt on a non-TTY; gate on
`isTTY` instead.

Commands that genuinely consume stdin still wait for the writer to close, same
as `cat`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@l2ysho
l2ysho force-pushed the claude/apify-cli-1206-verify-37b57c branch from 29e4187 to 2b8ebae Compare September 3, 2026 12:25
`apify run` without `--input` falls back to stdin, and waited there for an
end that a named pipe never reaches. A pipe the process merely inherited
hung the command forever, and so did a writer that sent data and kept the
pipe open. `actors call` and `actors start` share the path.

Reads that nobody asked for now stop at the first quiet gap: each chunk
restarts a 2s clock, and when stdin goes silent the CLI uses what arrived.
An explicit `-` is unchanged and still waits for the writer to close.

The cost is a writer slower than ~2.5s to its first byte, whose input is
now ignored. Truncation mid-stream stays loud, as the JSON fails to parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-builders Issues owned by the Builders team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI hangs after command completes when stdin is open but not a TTY

2 participants