Piping between processes

resource proc_open ( string cmd, array descriptorspec, array pipes)

resource proc_close ( resource process)

The process control functions we've looked at thus far work to a point, but aren't very powerful. Of course, that "powerful" should have a little asterisk next to it with some small text saying "difficult", because "powerful" and "difficult" often go hand in hand! There is a much more powerful* method that we're going to look at, which allows us to create a process and set up a read pipe and a write pipe so that we can communicate bi-directionally with it. This is quite a step up, so read carefully!

The functions we'll be using to open and close the process handle are proc_open() and proc_close(), which work together. That is, proc_open() opens the process and returns a handle to it, then when you're done you pass that handle to proc_close() to clean up. Of the two, proc_open() is the most difficult, so let's go over that first.

As you can see from the function definition, proc_open() returns a resource, which, if everything was successful, is a running process. For parameter one, send in the program name you want to run. In the second parameter, you need to specify how the parent process (our script) and the child (the process specified in parameter one) are going to communicate. This is where it gets complicated: it needs to be a two-dimensional array with at least one entry in order to provide at least one-way communication between parent and child (which is eerily like most households I know!). Each element in this array is itself an array with two elements: the first is a string set either to "pipe" (communicate through a pipe) or "file" (communicate through a file), and the second needs to either be "r" for "read", "w" for "write", or a filename if "file" was specified in the first parameter.

If that weren't complicated enough, there's more to it - if you've ever done advanced console work, you'll have seen commands like this:

grep "My Text" * 2>1

That would redirect the errors from the grep program to standard output (stdout). For grep, errors usually involve permissions - if it can't read a file to search inside it, it will emit errors. The reason for this is because standard input (stdin) is given the number 0 in the grand scheme of things, stdout is 1, and standard error (stderr) is 2. We need to specify these same numbers in our array as the keys need to be these numbers as they relate to the child process . I've italicised that last part because it's important: 0 means "stdin", so that's the pipe the child process will read from and therefore it's one the parent will write to. So, to set up a write-only pipe - a pipe that the parent can only write to, meaning that the child would read from - we'd use this:

$descriptorspec = array(
    0 => array("pipe", "r")
);

Similarly, to set up a pipe for the child to write to (parent read-only) and a file where the child should save its errors, we'd use this:

$descriptorspec = array(
    0 => array("pipe", "r"),
    2 => array("file", "/tmp/myerror.log", "a")
);

The second element sets up our error log as a file because it has 2 as the array key, which is stdout, and the value is an array with "file" and the filename as its values. There's an extra parameter in that specifies how the file should be worked with, and is the same as the parameters used in the fopen() function - "r" is for reading, "a" for appending, etc. The last parameter to proc_open() is an array where the created pipes can be stored, so just pass in a fresh variable.

That's an awful lot to understand at one time, I know, but this next piece of code should clarify the way things work:

<?php
    $descriptors = array(
        0 => array("pipe", "r")
    );

    $process = proc_open("php", $descriptors, $pipes);
    
    if (is_resource($process)) {
        fwrite($pipes[0], "<?php\n");
        fwrite($pipes[0], "  \$rand = rand(1,2);\n");
        fwrite($pipes[0], "  if (\$rand == 1) {\n");
        fwrite($pipes[0], "    echo \"Hello, World!\n\";\n");
        fwrite($pipes[0], "  } else {");
        fwrite($pipes[0], "    echo \"Goodbye, World!\n\";\n");
        fwrite($pipes[0], "  }");
        fwrite($pipes[0], "?>");
        fclose($pipes[0]);
    
        $return_value = proc_close($process);
    }
?>

What that script does is create a child PHP process that has one pipe for reading, then sends a complete PHP script down that pipe for execution. Note that the return value of proc_open() is checked immediately: if we get a resource back, the call has succeeded so we can go ahead and send the script. The script is sent entirely through our pipe using fwrite(), but note that we write to the first element of our $pipes array - again, remember that this was marked as "r" for the child, which means it's writeable for the script. After the script has been sent, fclose() needs to be called on the pipe to clean up the resources.

The last step is using proc_close() to close the process, but it's important that you only do this if you've already used fclose() to close all your pipes. Failure to do so may cause your script to hang!

Once you've grasped that basic script, it's easy to move to bi-directional communication. In order to read data back we need a second pipe alongside the write pipe, and also need to call fread() once we've finished writing. To get the second pipe, change the $descriptors array to this:

$descriptors = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w")
);

Try running the script again and see what's changed. All being well you should find that the child process no longer prints out "Hello, world!" or "Goodbye, world!" - this is because its output is now being sent back to the parent. Let's do something with that - add this code after the call to fclose():

while (!feof($pipes[1])) {
    echo fgets($pipes[1]);
}

fclose($pipes[1]);

Once we've written to the pipe, we now use feof() to read back everything available from the child. Each time there's more to read, fgets() is called to read in that line and echo it out, reproducing the original behaviour of the script. Once the loop finishes, the pipe is closed, leaving the way clear for the proc_close() call.

If you run the script now it should work as before: the text produced by the child gets printed out, albeit in a roundabout way. However, having the text passed through the parent means we can perform some post-processing on the child's output, like this:

$output = "";
while (!feof($pipes[1])) {
    $output .= fgets($pipes[1]);
}

$output = strtoupper($output);
echo $output; fclose($pipes[1]);

That script now uppercases all the text sent back from the child - a spurious use of pipes, to be sure, but it should hopefully serve as an example of what's possible!

 

Want to learn PHP 7?

Hacking with PHP has been fully updated for PHP 7, and is now available as a downloadable PDF. Get over 1200 pages of hands-on PHP learning today!

If this was helpful, please take a moment to tell others about Hacking with PHP by tweeting about it!

Next chapter: POSIX functions >>

Previous chapter: Running programs in the current process space

Jump to:

 

Home: Table of Contents

Copyright ©2015 Paul Hudson. Follow me: @twostraws.