proc_open: Extending file descriptor numbers to enable "status" feedback from a Perl script

perl, php, proc-open

Solution

Say you want to monitor the progress of a hello-world program, where each step is a dot written to the designated file descriptor.

#! /usr/bin/env perl

use warnings;
use strict;

die "Usage: $0 progress-fd\n" unless @ARGV == 1;

my $fd = shift;
open my $progress, ">&=", $fd or die "$0: dup $fd: $!";

# disable buffering on both handles
for ($progress, *STDOUT) {
  select $_;
  $| = 1;
}

my $output = "Hello, world!\n";

while ($output =~ s/^(.)(.*)\z/$2/s) {
  my $next = $1;
  print $next;
  print $progress ".";
  sleep 1;
}

Using bash syntax to open fd 3 on `/tmp/progress` and connect it to the program is

$ (exec 3>/tmp/progress; ./hello-world 3)
Hello, world!

$ cat /tmp/progress
..............

(It’s more amusing to watch live.)

To also see the dots on your terminal as they emerge, you could open your progress descriptor and effectively `dup2` it onto the standard error—again using bash syntax and more fun in real time.

$ (exec 17>/dev/null; exec 17>&2; ./hello-world 17)
H.e.l.l.o.,. .w.o.r.l.d.!.
.

You could of course skip the extra step with

$ (exec 17>&2; ./hello-world 17)

to get the same effect.

If your Perl program dies with an error such as

$ ./hello-world 333
./hello-world: dup 333: Bad file descriptor at ./hello-world line 9.

then the write end of your pipe on the PHP side probably has its close-on-exec flag set.

Problem

PHP's proc_open manual states: The file descriptor numbers are not limited to 0, 1 and 2 - you may specify any valid file descriptor number and it will be passed to the child process. This allows your script to interoperate with other scripts that run as "co-processes". In particular, this is useful for passing passphrases to programs like PGP, GPG and openssl in a more secure manner. It is also useful for reading status information provided by those programs on auxiliary file descriptors. What Happens: I call a Perl script in a PHP-based web application and pass parameters in the call. I have no future need to send data to the script. Through stdout [1] I receive from the Perl script json_encoded data that I use in my PHP application. What I would like to add: The Perl script is progressing through a website collecting information depending on the parameters passed in it's initial call. I would like to send back to the PHP application a text string that I could use to display as a sort of Progress Bar. How I think I should do it: I would expect to poll (every 1-2 seconds) the channel that has been setup for that "progression" update. I would use Javascript / jQuery to write into an html div container for the user to view. I do not think I should mix the "progress" channel with the more critical "json_encode(data)" channel as I would then need to decipher the stdout stream. (Is this thought logical, practical?) My Main Question: How do you use additional "file descriptors?" I would image the setup of additional channels to be straightforward, such as the 3 => ... in the below: ``` $tunnels = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w'), 3 => array('pipe', 'w') ); $io = array(); $resource = proc_open("perl file/tomy/perl/code.pl $param1 $param2 $param3", $tunnels, $io); if(!is_resource($resource)) { $error = "No Resource"; } fclose($io[0]); $perlOutput = stream_get_contents($io[1]); $output = json_decode($perlOutput); $errors = stream_get_contents($io[2]); print "$errors<p>"; fclose($io[1]); fclose($io[2]); $result = proc_close($resource); if($result != 0) { echo "you returned a $result result on proc_close"; } ``` But, in the Perl script I simply write to the stdout like: ``` my $json_terms = encode_json(\@terms); print $json_terms; ``` If my understanding of setting up an additional channel is correct (above, the 3 => ...), then how would I write to it within the Perl script? Thanks

Original source