phasync
Processes / Running processes

Processes

Running processes

Run external programs without blocking a worker.

phasync\Util\Process::run() starts a child process and returns a ProcessInterface whose standard streams you read and write from a coroutine, without blocking the others.

phasync::run(function () {
    $process = phasync\Util\Process::run('echo', ['hello']);

    echo $process->read();                 // hello
    while ($process->isRunning()) {
        phasync::sleep(0.01);
    }
    var_dump($process->getExitCode());      // int(0)
});

Nothing goes through a shell: $command is searched for in PATH (or taken as a path if it contains a slash), and the arguments are passed as they are, so no quoting is needed even for arguments with spaces or special characters in them.

Several at once#

Each process waits in its own coroutine, so starting a few runs them side by side instead of one after another.

phasync::run(function () {
    $jobs = [];
    foreach (['a.csv', 'b.csv', 'c.csv'] as $file) {
        $jobs[$file] = phasync::go(function () use ($file) {
            $process = phasync\Util\Process::run('gzip', ['-c', $file]);
            $output  = '';
            while ('' !== ($chunk = $process->read())) {
                $output .= $chunk;
            }

            return $output;
        });
    }
    foreach ($jobs as $file => $job) {
        \file_put_contents("$file.gz", phasync::await($job));
    }
});

Streaming and talking back#

read() waits until the process has written something and returns it; it returns '' only once, at the end of the stream. write() sends to standard input the same way. A process that answers each line it is given:

$child = phasync\Util\Process::run(PHP_BINARY, ['-r', 'while (false !== ($l = fgets(STDIN))) { echo strtoupper($l); }']);

$child->write("hello\n");
echo $child->read();    // HELLO
$child->stop();

Standard error is read the same way, with ProcessInterface::STDERR:

echo $process->read(phasync\Util\ProcessInterface::STDERR);

getStream(int $fd) returns the underlying stream, for stream_select() or phasync::readable()/writable() directly, when read()'s own waiting does not fit.

Timing out and killing it#

read() and write() wait as long as it takes; there is no timeout argument on them. To give a process a deadline, run it in a coroutine and await that coroutine with a timeout, then stop the process if it is reached:

phasync::run(function () {
    $process = phasync\Util\Process::run('sleep', ['10']);

    $job = phasync::go(function () use ($process) {
        return $process->read();     // sleep never writes anything
    });

    try {
        phasync::await($job, 0.2);   // give it 200 ms
    } catch (phasync\TimeoutException) {
        $process->stop();            // SIGTERM, then SIGKILL after one second
    }
});

stop() returns once the process is no longer running, or false if it still is after its five seconds. sendSignal(int $signal = 15) sends any signal directly and returns false without sending it once the process has already ended; on Windows the signal cannot be chosen, and any call ends the process at once. getExitCode() is the exit code, or false while it still runs; a process ended by a signal reports -1.

A read() or write() on a stream whose process has ended, once that has been noticed by an isRunning() or another status call, throws phasync\IOException: read what you need before asking.

See also#

  • ProcessRunnerThe implementation behind Process::run(): starts a child process and controls it.