Search code examples
perlexceptionevaldie

Why does Perl's IO::Pipe exception behave differently than croak or die in eval block?


I noticed in my program that an exception raised from IO::Pipe was behaving oddly, and I cannot figure out what it's doing (let alone how it's doing it). I've boiled it down to a simple example program:

use strict;
use warnings;

use Carp;
use IO::Pipe;

my($path) = shift;
my($bad) = shift || "";

eval {
    if ($path =~ m{pipe}i) {
        my($bin) = ($bad ? "/bin/lsddd" : "/bin/ls");

        my($pipe) = IO::Pipe->new();
        $pipe->reader("$bin -l .");
        print "$_" while <$pipe>;
        $pipe->close;
    }
    elsif ($path =~ m{croak}i) {
        croak "CROAKED" if $bad;
    }
    else {
        die "DIED" if $bad;
    }
};

if ($@) {
    my($msg) = $@;

    die "Caught Exception: $msg\n";
}

die "Uh-oh\n" if $bad;

print "Made it!\n";

The example program takes two arguments, one to indicate which code path to go down inside the eval block, and the second to indicate whether or not to generate an error (anything that evaluates to false will not generate an error). All three paths behave as expected when no error is requested; they all print Made it! with no error messages.

When asking for an error and running through the croak or die paths, it also behaves as I expect: the exception is caught, reported, and the program terminates.

$ perl example.pl die foo
Caught Exception: DIED at example.pl line 23.

and

$ perl example.pl croak foo
Caught Exception: CROAKED at example.pl line 11.
    eval {...} called at example.pl line 10

When I send an error down the IO::Pipe path, though, it reports an error, but the program execution continues until the outer die is reached:

$ perl example.pl pipe foo
Caught Exception: IO::Pipe: Cannot exec: No such file or directory at example.pl line 15.

Uh-oh

The first question is why -- why does the program report the "Caught Exception" message but not terminate? The second question is how do I prevent this from happening? I want the program to stop executing if the program can't be run.


Solution

  • There are two processes running after the eval in the case of interest. You can see this by adding a print statement before if ($@). One drops through eval and thus gets to the last die.

    The reader forks when used with an argument, to open a process. That process is exec-ed in the child while the parent returns, with its pid. The code for this is in _doit internal subroutine

    When this fails the child croaks with the message you get. But the parent returns regardless as it has no IPC with the child, which is expected to just disappear via exec. So the parent escapes and makes its way down the eval. That process has no $@ and bypasses if ($@).

    It appears that this is a hole in error handling, in the case when reader is used to open a process.

    There are ways to tackle this. The $pipe is an IO::Handle and we can check it and exit that extra process if it's bad (but simple $pipe->error turns out to be the same in both cases). Or, since close is involved, we can go to $? which is indeed non-zero when error happens

    # ...
    $pipe->close;
    exit if $? != 0;
    

    (or rather first examine it). This is still a "fix," which may not always work. Other ways to probe the $pipe, or to find PID of the escapee, are a bit obscure (or worse, digging into class internals).

    On the other hand, a simple way to collect the output and exit code from a program is to use a module for that. A nice pick is Capture::Tiny. There are others, like IPC::Run and IPC::Run3, or core but rather low-level IPC::Open3.

    Given the clarifications, the normal open should also be adequate.