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

I'm trying to write a generalized function that I can use to execute a program and capture it's STDOUT, STDERR, and exit code. In the process of developing this function I have found a problem that I can't understand.

When I attempt to execute a program which doesn't exist, I simply want to trap that and return an error from the function to the user stating that. In the following program the final print statement appears to be printing twice. Once to print my value of $error, and once to print the value that open3 dies with.

#!/usr/bin/perl -w use IPC::Open3; use IO::Select; use POSIX ":sys_wait_h"; use Symbol; sub run { my ($WRITE, $READ, $ERROR); $ERROR=gensym(); my $command="not_a_command"; my $error=""; my $output=""; eval { $pid=open3($WRITE, $READ, $ERROR, "$command"); }; if($@) { $error="Error, Could not execute $command: $!"; } else { close($WRITE); my $selector=IO::Select->new(); $selector->add($READ, $ERROR); while(@ready=$selector->can_read) { foreach (@ready) { if(fileno($_)==fileno($READ)) { $bytes=sysread($READ, $text, 1024); if($bytes==0) { $selector->remove($_); } else { $output.=$text; } } elsif(fileno($_)==fileno($ERROR)) { $bytes=sysread($ERROR, $text, 1024); if($bytes==0) { $selector->remove($_); } else { $error.=$text; } } } } waitpid($pid, 0); } return($output, $error); } my ($output, $error); ($output, $error)=run(); print "$output, $error\n";
The output:
jlm17 5660> ./test_eval.pl , Error, Could not execute not_a_command: No such file or directory , Can't exec "not_a_command": No such file or directory at /usr/share/ +perl/5.8/IPC/Open3.pm line 168.
And what is odd is that if I modify the final print statment of the program to this:

print "$error\n";

Then it only prints one line. It prints two lines when I have it printing the $output also, even though there is nothing to print out.

What is happening here, and more importantly, how do I make it stop?

Replies are listed 'Best First'.
Re: Doubled print with eval and open3
by almut (Canon) on Mar 06, 2007 at 00:04 UTC

    I think the key paragraph from the docs is this:

    open3() returns the process ID of the child process. It doesn't return on failure: it just raises an exception matching "/^open3:/". However, "exec" failures in the child are not detected. You'll have to trap SIGPIPE yourself.

    In other words, in case of an invalid command, the exec will fail, but not the fork - so you're left with two processes to handle (the parent with $pid > 0, and the child with $pid == 0).

    The following should take care of this situation:

    #!/usr/bin/perl use warnings; use IPC::Open3; use IO::Select; use POSIX ":sys_wait_h"; use Symbol; sub run { my ($WRITE, $READ, $ERROR); $ERROR=gensym(); my $command="not_a_command"; my $error=""; my $output=""; eval { $pid=open3($WRITE, $READ, $ERROR, "$command"); }; if (!$pid) { # if we get here, the fork succeeded, but the exec (likely) fail +ed... my $err = $@ ? $@ : "unknown error"; # exit child in any case die "Error: Could not execute $command: $err"; } else { close($WRITE); my $selector=IO::Select->new(); $selector->add($READ, $ERROR); while(@ready=$selector->can_read) { foreach (@ready) { if(fileno($_)==fileno($READ)) { $bytes=sysread($READ, $text, 1024); if($bytes==0) { $selector->remove($_); } else { $output.=$text; } } elsif(fileno($_)==fileno($ERROR)) { $bytes=sysread($ERROR, $text, 1024); if($bytes==0) { $selector->remove($_); } else { $error.=$text; } } } } waitpid($pid, 0); } return($output, $error); } my ($output, $error); ($output, $error)=run(); print "$output, $error\n";

    Outputs:

    $ ./test_eval.pl , Error: Could not execute not_a_command: open3: exec of not_a_command + failed at ./test_eval.pl line 22

    To get a clearer understanding of what's going on in your original code, you might want to add a print "\$pid=$pid, \$\$=$$, out=$output, err=$error\n"; before the return in the subroutine... (you'll find that it executes twice - though the output from parent and child will typically be mixed up).

    Also, note that you probably don't want -w here, but rather use warnings, or else you'll get unwanted (duplicated) warnings from the child process...

    Update: actually, I played with this some more, and I think I spoke too soon... the issue seems to be trickier than I thought :)

    The problem with the given code is that the die ... won't actually return the error via the subroutine, but rather print it out directly...   Hmm, so we're left with the problem that the error is occurring in the child (-> $@), but we can't easily communicate it back to the parent, which would have to take care of returning it (at least that's how I understand the issue at the moment). Still trying to find a way around the problem... -- but maybe someone else will post a solution before I do :)

    Update-2: ...ah yes, as ikegami pointed out (thanks!), we can simply print to STDERR, as we've already set this up... i.e.

    ... if (!$pid) { # if we get here, the fork succeeded, but the exec (likely) fail +ed... my $err = $@ ? $@ : "unknown error"; # return error, and exit child print STDERR "Error: Could not execute $command: $err"; POSIX::_exit($!); } ...

    (IOW, the die ... I originally suggested did do the right thing (i.e. print to STDERR)... except for the END blocks and stuff)

      POSIX::_exit would be better than die. We don't want END blocks and other unwinding to occur in the child

        That's right, but it still leaves us with the problem of how to properly return the full error message, as the $@ is only available/set in the child...   Or am I not getting something? (POSIX::_exit($!) doesn't seem to do it for me...)

        (BTW, I'm talking about unix-ish systems - or more specifically Linux)

Re: Doubled print with eval and open3
by GrandFather (Saint) on Mar 05, 2007 at 22:41 UTC

    The open3 doesn't throw an exception so the eval doesn't catch it. Instead test $!:

    $! = undef; $pid=open3($WRITE, $READ, $ERROR, "$command"); if (length $!) { $error="Error, Could not execute $command: $!"; } else {

    If there are exceptions you want to catch that can be thrown then put the eval back in and add a elsif as appropriate.


    DWIM is Perl's answer to Gödel

      The open3 doesn't throw an exception so the eval doesn't catch it.

      If that's true, why is "Error, Could not executed" printed. That's part of the exception handler.

      If that's true, does that mean the following passage from IPC::Open3's docs is wrong?

      open3() returns the process ID of the child process. It doesn't return on failure: it just raises an exception matching /^open3:/. However, exec failures in the child are not detected. You'll have to trap SIGPIPE yourself.

        Hmm, interesting. I ran OP's code under a debugger as a learning exercise, and thought I'd inferred the problem. Maybe I just found a different one:

        If that's true, why is "Error, Could not executed" printed.

        On Windows it's not - there's a clue right there! Actually on Windows the OP's sample code just prints a comma. The following:

        use strict; use warnings; use IPC::Open3; use IO::Select; use POSIX ":sys_wait_h"; use Symbol; my $ERROR=gensym(); my $command="not_a_command"; my ($WRITE, $READ); my $pid; $! = undef; eval {$pid = open3($WRITE, $READ, $ERROR, "$command");}; print "pid is $pid\n" if $pid; if ($@) { print "Trapped error $@"; } elsif (length $!) { print "Detected error: $!"; } else { print "No error"; }

        prints:

        pid is 3412 Detected error: No such file or directory

        on Windows.


        DWIM is Perl's answer to Gödel
Re: Doubled print with eval and open3
by jlm17 (Initiate) on Mar 06, 2007 at 18:06 UTC
    Thanks for the help. This really gives me a lot of traction on the problem. I'm only running POSIX::_exit($!); if !$pid because it appears that open3 already sends some output to STDERR. I don't see how to stop that and send my own custom error message, short of writing my own open3. But this is fine.

    I also still have a check for $@, since I suspect that it would be set if the fork() failed.

      I also still have a check for $@, since I suspect that it would be set if the fork() failed.

      Yes, but *after* if !$pid, because you only want to do that in the parent.