Skip to content

Commit 2128df3

Browse files
committed
Retry _read() on EINTR, instead of losing pipe contents.
By not retrying, EINTR behaved like EOF. In other words, EINTR had consequences like the write side (kid side) closing the pipe early. Symptoms depended on the _read() caller. When $pipe_reader conflated EINTR with pipe closure, symptoms were application-specific. When _spawn reading the internal "sync pipe" conflated EINTR with pipe closure, the parent would fail to report exec failure. Add a test for the loss of exec failure report. The $pipe_reader problem is much tougher to test, because $pipe_reader only tries read() if select() found data available. Hence, the signal needs to arrive in the narrow window after read() enters the kernel, before read() returns already-available data. The issue report reproduced that on macOS only. Fixes #176
1 parent 85ed0b9 commit 2128df3

File tree

2 files changed

+34
-4
lines changed

2 files changed

+34
-4
lines changed

lib/IPC/Run.pm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,8 +1545,11 @@ sub _pty {
15451545
sub _read {
15461546
confess 'undef' unless defined $_[0];
15471547
my $s = '';
1548-
my $r = POSIX::read( $_[0], $s, 10_000 );
1549-
croak "$!: read( $_[0] )" if not($r) and !$!{EINTR};
1548+
my $r;
1549+
do {
1550+
$r = POSIX::read( $_[0], $s, 10_000 );
1551+
} while ( !defined($r) && $!{EINTR} );
1552+
croak "$!: read( $_[0] )" unless defined($r);
15501553
$r ||= 0;
15511554
_debug "read( $_[0] ) = $r chars '$s'" if _debugging_data;
15521555
return $s;
@@ -1567,6 +1570,13 @@ sub _spawn {
15671570
croak "$! during fork" unless defined $kid->{PID};
15681571

15691572
unless ( $kid->{PID} ) {
1573+
if ( $self->{_sigusr1_after_fork} ) {
1574+
1575+
# sleep 10ms to improve chance of parent starting read() before it
1576+
# handles the signal we're about to send.
1577+
select undef, undef, undef, 0.01;
1578+
kill 'USR1', getppid;
1579+
}
15701580
## _do_kid_and_exit closes sync_reader_fd since it closes all unwanted and
15711581
## unloved fds.
15721582
$self->_do_kid_and_exit($kid);

t/eintr.t

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
=head1 NAME
66
7-
eintr.t - Test select() failing with EINTR
7+
eintr.t - Test select() and read() failing with EINTR
88
99
=cut
1010

@@ -35,7 +35,7 @@ if ( $got_usr1 != 1 ) {
3535
plan skip_all => "can't deliver a signal on this platform";
3636
}
3737

38-
plan tests => 3;
38+
plan tests => 5;
3939

4040
# A kid that will send SIGUSR1 to this process and then produce some output.
4141
my $kid_perl = qq[sleep 1; kill 'USR1', $$; sleep 1; print "foo\n"; sleep 180];
@@ -59,3 +59,23 @@ is $got_usr1, 2, 'got USR1 from the kid';
5959

6060
$harness->kill_kill;
6161
$harness->finish;
62+
63+
# Have kid send SIGUSR1 while we're in read of sync pipe. That pipe conveys any
64+
# exec failure to us.
65+
SKIP: {
66+
if ( IPC::Run::Win32_MODE() ) {
67+
skip "Can't really exec() $^O", 2;
68+
}
69+
70+
my $expected = 'exec failed';
71+
my $h = eval {
72+
start(
73+
[ $^X, "-e", 1 ],
74+
_sigusr1_after_fork => 1,
75+
_simulate_exec_failure => 1
76+
);
77+
};
78+
my $got = $@ =~ $expected ? $expected : $@ || "";
79+
is $got_usr1, 3, 'got USR1 from the _simulate_exec_failure kid';
80+
is( $got, $expected, "reported exec failure despite USR1" );
81+
}

0 commit comments

Comments
 (0)