Skip to content

fix(process): read stdout and stderr together so large output can't deadlock - #1939

Merged
mrrobot47 merged 2 commits into
EasyEngine:developfrom
mrrobot47:fix/process-pipe-deadlock
Sep 29, 2026
Merged

mrrobot47 merged 2 commits into
EasyEngine:developfrom
mrrobot47:fix/process-pipe-deadlock

Conversation

@mrrobot47

Copy link
Copy Markdown
Member

EE\Process::run() read the child's stdout to EOF and only then read stderr. A child that writes more than a pipe buffer (about 64 KB on Linux) to stderr blocks on that write while EE is still waiting for stdout to close, so EE::exec() / EE::launch() hang forever. EE::runcommand() with return read its pipes the same way.

This adds Utils\read_pipes(), which makes both pipes non-blocking and reads whichever is ready with stream_select() until both reach EOF. run() returns the same ProcessRun fields as before.

  • If a caught signal interrupts stream_select() (site-command installs pcntl handlers), the select is retried, as Symfony Process does.
  • On Windows, where stream_select() can't wait on process pipes, or if stream_select() fails for another reason, it falls back to the previous blocking reads.

Testing

  • Process::create()->run() on PHP 7.4 and 8.0–8.5: 100 KB of stderr, 1 MB of stdout, 200 KB of both interleaved, output in bursts, binary bytes, a non-zero exit, no output, env and cwd, stdin passed through, and an stdin that is never closed. Before this change, every case with more than 64 KB of stderr hangs; after it, all cases pass.
  • A child that is sent two caught signals while it writes 200 KB to stderr: it hangs before this change and passes after it, on 7.4 and 8.0–8.5.
  • php -l on 7.4 and 8.5.

…eadlock

Process::run() read stdout to EOF before it read stderr. A child that wrote more than a pipe buffer (about 64 KB) to stderr blocked on that write while EE waited for stdout, so both hung forever. EE::runcommand() with 'return' read its pipes the same way.

The new Utils\read_pipes() waits on both pipes with stream_select() and reads whichever has data until both reach EOF. On Windows, where stream_select() can't wait on process pipes, or if stream_select() fails, it falls back to the previous blocking reads.
stream_select() returns false on EINTR, and select() is never restarted after a caught signal. site-command installs pcntl handlers for SIGTERM, SIGHUP, SIGUSR1 and SIGINT, so a signal during a site command dropped read_pipes() back to the sequential blocking reads, where more than 64 KB of stderr could hang again. Retry on an interrupted system call, as Symfony Process does, and fall back only on other errors.

Also say in the docblock that read_pipes() takes output pipes only.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The deadlock fix lacks an automated regression test covering outputs larger than the pipe buffer.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Prevents process deadlocks by consuming stdout and stderr concurrently.

Changes:

  • Adds non-blocking pipe reads using stream_select().
  • Handles interrupted selects and Windows fallback.
  • Integrates concurrent reads into both process execution paths.
File Description
php/​utils.php Adds concurrent pipe-reading utility.
php/​EE/​Process.php Uses concurrent reads in Process::run().
php/​class-ee.php Uses concurrent reads in runcommand().

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread php/utils.php
@mrrobot47
mrrobot47 merged commit e55a59e into EasyEngine:develop Sep 29, 2026
11 checks passed
@mrrobot47
mrrobot47 deleted the fix/process-pipe-deadlock branch September 30, 2026 07:57
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.

2 participants