fiber-interop / process
Cooperative proc_open() for fiber-interop/api: async\process\run() and start() capture stdout/stderr, pipe stdin and wait for exit on any adapter.
Requires
- php: ^8.1
- fiber-interop/api: ^0.2
Requires (Dev)
- fiber-interop/amp: *
- fiber-interop/phasync: *
- fiber-interop/react: *
- fiber-interop/revolt: *
- fiber-interop/swoole: *
- phpunit/phpunit: ^10.5 || ^11
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-14 06:19:12 UTC
README
Cooperative proc_open() for fiber-interop/api: inside
async\run() running a child process suspends only the calling task, on whichever adapter is
installed. Outside run() the same calls simply block.
composer require fiber-interop/process
use function async\{run, go, await_all}; [$a, $b] = run(fn() => await_all([ go(fn() => async\process\run(['git', 'log', '-1', '--format=%H'])), go(fn() => async\process\run('gzip -c | wc -c', stdin: $bigString, timeout: 10)), ])); echo $a->stdout, $b->stdout, $b->exitCode;
API
async\process\run(array|string $command, ?string $cwd = null, ?array $env = null, ?string $stdin = null, float $timeout = 0): Resultruns the command to completion.$commandis as forproc_open(): an argv array (no shell) or a string run by/bin/sh -c.$envnull inherits the environment.$stdinis written to the child, then its stdin is closed; with null the child gets an empty, closed stdin.ResulthasexitCode,stdout,stderrandsignal(the terminating signal, or null if the process exited normally;exitCodeis then -1).async\process\start(...)takes the same arguments and returns aProcess:async\await($p)yields theResult(await it as often as you like, it settles once),$p->pidis the child's pid (the/bin/shfor a string command),$p->kill(int $signal = SIGTERM)signals it. Outsiderun()the process has already finished whenstart()returns.$timeout > 0: when it expires the process gets SIGTERM, and SIGKILLProcess::GRACE(2) seconds later if still alive; thenasync\process\TimeoutExceptionis thrown. It implementsasync\TimeoutException, and its$resultholds the output captured until the kill.
How it waits
Inside run(), all three pipes are non-blocking and each gets its own task: stdin is written on
async\writable(), stdout and stderr are drained on async\readable() concurrently. So a child that
fills its stdout/stderr pipe while we are still feeding its stdin cannot deadlock (the classic
proc_open() bug; tested with 4 MB each way). Outside run() the same is done with
stream_select() over the pipes.
Process exit has no file descriptor PHP can wait on, so it is detected by polling
proc_get_status() with async\sleep() between polls, backing off from 1 ms to 10 ms. Exit is
therefore noticed up to 10 ms late; no CPU is burnt meanwhile. The fix belongs in php-src: expose a
pidfd (Linux pidfd_open(), or a kqueue EVFILT_PROC equivalent) from proc_get_status() or a
proc_get_fd(), so the exit can be awaited with async\readable() like any socket.
Limits
- Output is buffered in memory: no streaming access to stdout while the process runs.
- On timeout, pipes still held open by a grandchild (e.g. a background job of the shell) are
abandoned
GRACE + 0.5s after the deadline; a string command thatexecs its last program avoids this. $p->kill()signals only the direct child (proc_terminate()), not its process group.- Under Swoole,
SWOOLE_HOOK_ALLincludesSWOOLE_HOOK_PROC, which makesproc_open()itself coroutine-aware; this package's path waits onasync\readable()either way and passes the same suite.
Tests
No external services are needed. FI_RUNTIME=revolt|amp|react|phasync|swoole picks the adapter:
FI_RUNTIME=amp vendor/bin/phpunit
FI_RUNTIME=phasync php -d extension=phasync.so vendor/bin/phpunit