2

I would like to use IPC::Run to communicate with child via child's STDIN, STDOUT and STDERR (start, pump, finish). It seems to work.

I would like to know how to detect

  • premature child exit (e.g. caused by errors)
  • pipes closed by the child
AnFi
  • 10,493
  • 3
  • 23
  • 47

2 Answers2

3

The pump throws a die on errors, or writes its message to STDERR if "called after all harnessed activities have completed." See right before ROUTINES section and pump itself. The second case can come about if the child exited. So wrap the pump call in eval, and also convert warnings to die to catch both cases

if ($talk_to_child) 
{
    eval {
        local $SIG{__WARN__} = sub { die "pump WARNING: @_" };
        pump $harness;
    };
    if ($@) { 
        print $@;
        $talk_to_child = 0;
    }; 
}
# ... and eval {} for finish()

But this alone won't cut it: when a parent tries to write to a child that exited it gets a SIGPIPE, which outright terminates the process. The same goes when a child closes streams and the parent attempts to write. So also install a signal handler for SIGPIPE

$SIG{PIPE} = sub { 
    say "$_[0]: $!";
    $talk_to_child = 0;  # global
};

so that the parent survives the SIGPIPE. Consider local-izing the change to the global %SIG by doing local $SIG{PIPE} = ... instead, a good practice even just on general principle. On the other hand, there's good sense in globally handling a signal that can terminate you out of blue (even in cases where the handler may decide to exit).

The eval is still needed even as $SIG{PIPE} is handled since pump throws, too.

These together take care of all tests I came up with, practically as they stand. Still, some processing in the handler and in eval is needed to distinguish cases of interest if that is wanted.

If this adds up to too much another way is to check before each call. See this post for one-line checks (wrapped in subs) of: (1) whether a child is running, using result, and (2) whether "there are open I/O channels or active processes", using pumpable.

I think that you want both, and also throw in the SIGPIPE handler. That should cover it.

I cannot be more specific here since the question doesn't provide specifics.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • Nice catch of `SIGPIPE`. I think that was what I was missing :) – Håkon Hægland Jun 27 '17 at 09:39
  • @HåkonHægland Right, the `SIGPIPE` is the show stopper. But this is still complicated. It may be simpler to check before each call using a combination of `result` and `pumpable` (plus `SIGPIPE` handler). – zdim Jun 28 '17 at 06:47
  • @AndrzejA.Filip I updated the answer with a comment on checking directly using `result` + `pumpable` (+ `SIGPIPE` handler). I also edited a few comments for accuracy. I confirmed in my tests that the main approach in the answer does handle all cases, practically as it stands. Let us know how it works for you. The answer by @Håkon also got updated. – zdim Jun 28 '17 at 06:54
  • Yes this is indeed complictated and also interesting. So another thing is that if we get SIGPIPE, and the child has turned off autoflush for its STDOUT (see my `child2.pl` example) the output from the child is simply lost. In this case, I think we also need to conect the child to a pseudo terminal, in order to make it turn on autoflushing again. – Håkon Hægland Jun 28 '17 at 18:21
1

Update: Thanks to @zdim for reminding me to check the SIGPIPE signal. Here is an update of my answer that also checks SIGPIPE:

I did a simple test using start, pump, and finish. Here is the main script p.pl that I used:

use feature qw(say);
use strict;
use warnings;
use IPC::Run;

my $child_in;
my $child_out;
my $child_err;
my $child_name = shift;

my $harness = eval {
    IPC::Run::start [ $child_name ], \$child_in, \$child_out, \$child_err;
};
if ( $@ ) {
    chomp $@;
    die "Caught exception: '$@'";
}
for (1..2) {
    $child_in = "Joe$_\n";
    say "Parent sleeping for 1 second..";
    sleep 1;
    eval {
        local $SIG{PIPE} = sub { 
            die "Parent received SIGPIPE. "
              . "Child is either dead or has closed its input pipe\n";
        };
        say "Sending data to child..";
        my $result = $harness->pump;
        say "IPC::Run::pump() returned: ", $result ? "TRUE" : "FALSE";
    };
    if ( $@ ) {
        chomp $@;
        say "IPC::Run::pump() failed: '$@'";
        last;
    }
    say "\$child_in = '$child_in'";
    say "\$child_out = '$child_out'";
}
say "Finishing harness..";
my $res = eval {
    local $SIG{PIPE} = sub { 
        die "Parent received SIGPIPE. "
          . "Child is either dead or has closed its input pipe\n";
    };
    $harness->finish;
};
if ( $@ ) {
    chomp $@;
    die "IPC::Run::finish() failed: '$@'\n";
}
printf "IPC::Run::finish() returned: '%s'\n", $res ? "TRUE" : "FALSE";
chomp $child_out;
say "STDOUT from child: '$child_out'";
chomp $child_err;
say "STDERR from child: '$child_err'";
say "Child returned exit code: ", $harness->result;
say "Parent exited normally.."

I used three different child scripts:

child.pl:

#! /usr/bin/env perl    
use feature qw(say);
use strict;
use warnings;

my $reply = <STDIN>;
chomp $reply;
say "Hello $reply";
my $reply2 = <STDIN>;
chomp $reply2;
say "Got second reply: $reply2";
exit 0;

and output:

$ p.pl child.pl
Parent sleeping for 1 second..
Sending data to child..
IPC::Run::pump() returned: TRUE
$child_in = ''
$child_out = ''
Parent sleeping for 1 second..
Sending data to child..
IPC::Run::pump() returned: TRUE
$child_in = ''
$child_out = ''
Finishing harness..
IPC::Run::finish() returned: 'TRUE'
STDOUT from child: 'Hello Joe1
Got second reply: Joe2'
STDERR from child: ''
Child returned exit code: 
Parent exited normally..

child2.pl:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

my $reply = <STDIN>;
chomp $reply;
say "Hello $reply";
die "Child exception\n";

and output:

$ p.pl child2.pl
Parent sleeping for 1 second..
Sending data to child..
IPC::Run::pump() returned: TRUE
$child_in = ''
$child_out = ''
Parent sleeping for 1 second..
Sending data to child..
IPC::Run::pump() failed: 'Parent received SIGPIPE. Child is either dead or has closed its input pipe'
Finishing harness..
IPC::Run::finish() failed: 'Parent received SIGPIPE. Child is either dead or has closed its input pipe'

child3.pl:

#! /usr/bin/env perl
use strict;
use warnings;

close \*STDIN;
close \*STDOUT;
close \*STDERR;
sleep 5;
exit 2;

and output:

$ p.pl child3.pl 
Parent sleeping for 1 second..
Sending data to child..
IPC::Run::pump() failed: 'ack Parent received SIGPIPE. Child is either dead or has closed its input pipe'
Finishing harness..
IPC::Run::finish() failed: 'Parent received SIGPIPE. Child is either dead or has closed its input pipe'

So for these tests, it seems that the SIGPIPE signal can be used to check if a child is a alive or has closed its input pipe. Note that if you try to call pump() after a child has exited, the previous output from the child is lost, see the child2.pl example.

Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
  • I am interested in case of **long** running child with big number of "command-response" sequences. I would like to avoid pushing commands to child with closed STDIN or pumping replies from child with closed STDOUT. – AnFi Jun 26 '17 at 16:59