lgauthie has asked for the wisdom of the Perl Monks concerning the following question:

Greetings, honorable monks...
I come today to seek your wisdom...

I wrote a package from which a sub named '_runReadWrite' can be called to run processes on the server. After searching for the best solution, I finally chose to implement it using IPC::Open3, in a basic way that was taken from the Camel book:

# declare needed variables and connect to process local (*CHILD_IN, *CHILD_OUT, *CHILD_ERR); my ($childpid, @outlines, @errlines); $childpid = open3(*CHILD_IN, *CHILD_OUT, *CHILD_ERR, $cmd); # feed input to process, then close input print CHILD_IN $input; close (CHILD_IN); # save output and errors, close channels and process chomp(@outlines = <CHILD_OUT>); close (CHILD_OUT); chomp(@errlines = <CHILD_ERR>); close (CHILD_ERR); waitpid($childpid, 0); # return the result of the call return { OUTPUT => \@outlines , ERRORS => \@errlines };
Until today, any call to this sub worked perfectly. But unfortunately, things have changed...

The problem is that I now have a script that has, at some point, to close STDOUT, in order to unhook a child fork from the browser in a CGI script.

Now, later in that process, I make a call to my '_runReadWrite' function, which lamentably fails capturing the launched process' output.

What I would like to do, in the sub that runs open3, is to add some branching and/or additional tests so it still works when STDOUT is closed in the calling process. In other words, I would like it to continue working as it did so far when STDOUT is open, but use another way when that channel is closed. I have tried many things, searched for answers everywhere, but I did not manage to find a complete solution so far, and this is why I today address your wisdom...

Can anyone help me (and maybe others) with this ?
A proud initiate :)

Replies are listed 'Best First'.
Re: IPC::Open3 & closed STDOUT
by ptkdb (Monk) on Nov 18, 2003 at 14:03 UTC
    You haven't listed _runReadWrite, so how can we comment on modifying its inner workings? For that matter you say 'it fails'. How??? Does it die, hang, segfault?

      Hi ptkdb, here is the full sub listing...
      sub _run_ReadWrite { my ($invocant, $cmd, $input) = @_; # declare needed variables and connect to process local (*CHILD_IN, *CHILD_OUT, *CHILD_ERR); my ($childpid, @outlines, @errlines); $childpid = open3(*CHILD_IN, *CHILD_OUT, *CHILD_ERR, $cmd); # feed input to process, then close input print CHILD_IN $input; close (CHILD_IN); # save output and errors, close those channels and the process chomp(@outlines = <CHILD_OUT>); close (CHILD_OUT); chomp(@errlines = <CHILD_ERR>); close (CHILD_ERR); waitpid($childpid, 0); # return the result of the call return { OUTPUT => \@outlines , ERRORS => \@errlines }; }
      Now like I said, calls to this sub return exactly what I expect, as long as I pass a valid $cmd and $input.

      The problem is when a call to this function is made from a script that has, for some reason, closed STDOUT, the @outlines array ends out empty. The process doesn't hang or fail, in fact everything works fine, its just that the launched process' output "gets lost" somewhere. I want to be able to catch it !

      Is that clearer ?

      Thanks for your help
        Next Question: What '$cmd' is not giving you output? Does it work for simple things like 'ls' but not for something else?
Re: IPC::Open3 & closed STDOUT
by etcshadow (Priest) on Nov 19, 2003 at 05:45 UTC
    The problem is this:
    chomp(@outlines = <CHILD_OUT>); close (CHILD_OUT); chomp(@errlines = <CHILD_ERR>); close (CHILD_ERR);
    You are making the assumption (maybe you don't realize that you are) that your subprocess is working in a very 1, 2, 3 fashion. That is, it reads STDIN (completely), and then it writes STDOUT (completely) and then it is done (never writing STDERR... except of course, unless it explicitly closes its STDOUT before doing its writes to STDERR, which I'm sure it doesn't).

    If it writes anything to STDERR, then everything will hang. The reason is that the two processes are deadlocked. The first is waiting on the second to write STDOUT, and the second is waiting on the first to read STDERR. One thing you may have missed in the IPC::open3 documentation is the fact that these pipes are synchronized between the parent and child. If the child is trying to write STDERR, then the parent must be reading the child's STDERR. If not, then the child will wait until the parent is ready to read. Likewise, if the parent is reading the child's STDOUT, then it will wait until the child writes STDOUT.

    So how do you deal with this? You either do it the hard way, which is to learn about IO::Select and non-blocking I/O. (That is, instead of having the parent say "read the child's STDOUT", say "try to read the child's STDOUT, if the child is trying to write STDOUT, else try to read the child's STDERR.... etc"). Or, you do it the easy, somewhat hackish way, which is to either ignore STDERR or lump it together with STDOUT. You can look into IPC::open2 for that tack. Of course, this all, also, relies on the assumption that the subprocess will do all of its reading before it does any of its writing.


    ------------
    :Wq
    Not an editor command: Wq

      Excellent explanation.

      Another alternative is to write STDOUT and/or STDERR to a File::Temp.

      Stricly speaking, the system buffer size for pipes enters into the pictures so you can squeak by if the program reads all but 512 bytes (for example) of its input before it writes more than 512 bytes of output. But this usually just leads one to think that they have a robust solution when they really don't rather than being truely helpful.

                      - tye
Re: IPC::Open3 & closed STDOUT
by ysth (Canon) on Nov 18, 2003 at 14:45 UTC
    You should open STDOUT to something else (e.g. open STDOUT, '>/dev/null') immediately after closing it, so the next open of any file doesn't get file descriptor 1.
Re: IPC::Open3 & closed STDOUT
by lgauthie (Novice) on Nov 27, 2003 at 13:42 UTC
    Hello again, dear monks !

    First of all, I would like to thank everyone who took time to answer my question. Your wisdom greatly inspired me !

    The first (and only) thing I needed to do to fix my problem was to follow ysth's advice. Indeed, instead of coldly close STDOUT in the calling CGI script, I just redirect it to null. This way, the process still gets "unhooked" from the browser AND calls to IPC::Open3 work as if STDOUT was never altered.

    However, it would have been to bad not to follow the other advices. I decided to try to enhance my method, using IO::Select. I found a very good node to help me in that process. So in the end, here is how I reimplemented it:

    use strict; use IPC::Open3; use IO::Select; use Symbol; sub _run_ReadWrite { my ($invocant, $cmd, $input) = @_; # Declare needed variables my ($child_pid, $output, $errors, $select, $fh, @ready); my ($child_in, $child_out, $child_err) = (gensym, gensym, gensym); # Try to connect to specified process eval { $child_pid = open3($child_in, $child_out, $child_err, $cmd) }; # ..... throw error if $@ ..... # Feed input to process, then close input print $child_in $input; close ($child_in); # Create a select object and add fhs $select = IO::Select->new(); $select->add($child_out, $child_err); # This loop will block until data is available while ( @ready = $select->can_read ) { # We now have a list of readable fhs foreach $fh ( @ready ) { # Read 4096 bytes of data my $data; my $len = sysread $fh, $data, 4096; # There was a read error if ( !defined $len ) { # ..... throw error ..... } # Current filehandle is empty, remove from select object elsif ( $len == 0 ) { $select->remove($fh) and next } # Data was read from a proper fh, add it to proper var elsif ( $fh == $child_out ) { $output .= $data and next } elsif ( $fh == $child_err ) { $errors .= $data and next } # There was an unexpected error else { # ..... throw error ..... } } } # Process can be reaped waitpid($child_pid, 0); # Return collected results return { OUTPUT => $output , ERRORS => $errors }; }
    I hope this can help someone else someday !
    Until next time, have a good one and greetings from Paris, fellow monks !

    Luc Gauthier