File Coverage

blib/lib/IO/Async/Internals/ChildManager.pm
Criterion Covered Total %
statement 174 223 78.0
branch 95 156 60.9
condition 12 20 60.0
subroutine 18 21 85.7
pod 0 5 0.0
total 299 425 70.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2007-2024 -- leonerd@leonerd.org.uk
5              
6             package IO::Async::Internals::ChildManager 0.805;
7              
8 41     41   542 use v5.14;
  41         142  
9 41     41   225 use warnings;
  41         82  
  41         3123  
10              
11             # Not a notifier
12              
13 41     41   12900 use IO::Async::Stream;
  41         134  
  41         1584  
14              
15 41     41   235 use IO::Async::OS;
  41         63  
  41         1060  
16              
17 41     41   191 use Carp;
  41         87  
  41         2826  
18 41     41   254 use Scalar::Util qw( weaken );
  41         68  
  41         3061  
19              
20 41     41   342 use POSIX qw( _exit dup dup2 nice );
  41         76  
  41         413  
21              
22 41     41   5961 use constant LENGTH_OF_I => length( pack( "I", 0 ) );
  41         94  
  41         29487  
23              
24             # Writing to variables of $> and $) have tricky ways to obtain error results
25             sub setuid
26             {
27 0     0 0 0 my ( $uid ) = @_;
28              
29 0         0 $> = $uid; my $saved_errno = $!;
  0         0  
30 0 0       0 $> == $uid and return 1;
31              
32 0         0 $! = $saved_errno;
33 0         0 return undef;
34             }
35              
36             sub setgid
37             {
38 0     0 0 0 my ( $gid ) = @_;
39              
40 0         0 $) = $gid; my $saved_errno = $!;
  0         0  
41 0 0       0 $) == $gid and return 1;
42              
43 0         0 $! = $saved_errno;
44 0         0 return undef;
45             }
46              
47             sub setgroups
48             {
49 0     0 0 0 my @groups = @_;
50              
51 0         0 my $gid = $)+0;
52             # Put the primary GID as the first group in the supplementary list, because
53             # some operating systems ignore this position, expecting it to indeed be
54             # the primary GID.
55             # See
56             # https://rt.cpan.org/Ticket/Display.html?id=65127
57 0         0 @groups = grep { $_ != $gid } @groups;
  0         0  
58              
59 0         0 $) = "$gid $gid " . join " ", @groups; my $saved_errno = $!;
  0         0  
60              
61             # No easy way to detect success or failure. Just check that we have all and
62             # only the right groups
63 0         0 my %gotgroups = map { $_ => 1 } split ' ', "$)";
  0         0  
64              
65 0         0 $! = $saved_errno;
66 0   0     0 $gotgroups{$_}-- or return undef for @groups;
67 0 0       0 keys %gotgroups or return undef;
68              
69 0         0 return 1;
70             }
71              
72             # Internal constructor
73             sub new
74             {
75 41     41 0 96 my $class = shift;
76 41         160 my ( %params ) = @_;
77              
78 41 50       251 my $loop = delete $params{loop} or croak "Expected a 'loop'";
79              
80 41         139 my $self = bless {
81             loop => $loop,
82             }, $class;
83              
84 41         280 weaken( $self->{loop} );
85              
86 41         258 return $self;
87             }
88              
89             sub spawn_child
90             {
91 338     338 0 707 my $self = shift;
92 338         1559 my %params = @_;
93              
94 338         925 my $command = delete $params{command};
95 338         893 my $code = delete $params{code};
96 338         698 my $setup = delete $params{setup};
97 338         751 my $on_exit = delete $params{on_exit};
98              
99 338 100       1091 if( %params ) {
100 4         1368 croak "Unrecognised options to spawn: " . join( ",", keys %params );
101             }
102              
103 334 100 100     3120 defined $command and defined $code and
104             croak "Cannot pass both 'command' and 'code' to spawn";
105              
106 330 100 100     4778 defined $command or defined $code or
107             croak "Must pass one of 'command' or 'code' to spawn";
108              
109 326 100       2661 my @setup = defined $setup ? $self->_check_setup_and_canonicise( $setup ) : ();
110              
111 324         1095 my $loop = $self->{loop};
112              
113 324         937 my ( $readpipe, $writepipe );
114              
115             {
116             # Ensure it's FD_CLOEXEC - this is a bit more portable than manually
117             # fiddling with F_GETFL and F_SETFL (e.g. MSWin32)
118 324         698 local $^F = -1;
  324         2648  
119              
120 324 50       4173 ( $readpipe, $writepipe ) = IO::Async::OS->pipepair or croak "Cannot pipe() - $!";
121 324         3154 $readpipe->blocking( 0 );
122             }
123              
124 324 100       1438 if( defined $command ) {
125 123 100       2719 my @command = ref( $command ) ? @$command : ( $command );
126              
127             $code = sub {
128 41     41   362 no warnings;
  41         249  
  41         94657  
129 28     28   0 exec( @command );
130 0         0 return;
131 123         848 };
132             }
133              
134             my $kid = $loop->fork(
135             code => sub {
136             # Child
137 29     29   5464 close( $readpipe );
138 29         2093 $self->_spawn_in_child( $writepipe, $code, \@setup );
139             },
140 324         3639 );
141              
142             # Parent
143 295         47301 close( $writepipe );
144 295         24867 return $self->_spawn_in_parent( $readpipe, $kid, $on_exit );
145             }
146              
147             sub _check_setup_and_canonicise
148             {
149 303     303   741 my $self = shift;
150 303         699 my ( $setup ) = @_;
151              
152 303 100       1740 ref $setup eq "ARRAY" or croak "'setup' must be an ARRAY reference";
153              
154 302 100       1108 return () if !@$setup;
155              
156 249         540 my @setup;
157              
158             my $has_setgroups;
159              
160 249         1612 foreach my $i ( 0 .. $#$setup / 2 ) {
161 431         1770 my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
162              
163             # Rewrite stdin/stdout/stderr
164 431 100       1338 $key eq "stdin" and $key = "fd0";
165 431 100       1189 $key eq "stdout" and $key = "fd1";
166 431 100       1072 $key eq "stderr" and $key = "fd2";
167              
168             # Rewrite other filehandles
169 431 100 66     1319 ref $key and eval { $key->fileno; 1 } and $key = "fd" . $key->fileno;
  127         634  
  127         891  
170              
171 431 100       3558 if( $key =~ m/^fd(\d+)$/ ) {
    100          
    100          
    100          
    100          
    100          
    100          
172 419         1611 my $fd = $1;
173 419         1170 my $ref = ref $value;
174              
175 419 100 66     1410 if( !$ref ) {
    100          
    50          
176 128         321 $value = [ $value ];
177             }
178             elsif( $ref eq "ARRAY" ) {
179             # Already OK
180             }
181 2         37 elsif( $ref eq "GLOB" or eval { $value->isa( "IO::Handle" ) } ) {
182 38         166 $value = [ 'dup', $value ];
183             }
184             else {
185 0         0 croak "Unrecognised reference type '$ref' for file descriptor $fd";
186             }
187              
188 419         838 my $operation = $value->[0];
189 419 50       1010 grep { $_ eq $operation } qw( open close dup keep ) or
  1676         3716  
190             croak "Unrecognised operation '$operation' for file descriptor $fd";
191             }
192             elsif( $key eq "env" ) {
193 3 50       46 ref $value eq "HASH" or croak "Expected HASH reference for 'env' setup key";
194             }
195             elsif( $key eq "nice" ) {
196 1 50       23 $value =~ m/^\d+$/ or croak "Expected integer for 'nice' setup key";
197             }
198             elsif( $key eq "chdir" ) {
199             # This isn't a purely watertight test, but it does guard against
200             # silly things like passing a reference - directories such as
201             # ARRAY(0x12345) are unlikely to exist
202 1 50       37 -d $value or croak "Working directory '$value' does not exist";
203             }
204             elsif( $key eq "setuid" ) {
205 2 50       13 $value =~ m/^\d+$/ or croak "Expected integer for 'setuid' setup key";
206             }
207             elsif( $key eq "setgid" ) {
208 2 50       22 $value =~ m/^\d+$/ or croak "Expected integer for 'setgid' setup key";
209 2 50       7 $has_setgroups and carp "It is suggested to 'setgid' before 'setgroups'";
210             }
211             elsif( $key eq "setgroups" ) {
212 2 50       9 ref $value eq "ARRAY" or croak "Expected ARRAY reference for 'setgroups' setup key";
213 2   33     39 m/^\d+$/ or croak "Expected integer in 'setgroups' array" for @$value;
214 2         8 $has_setgroups = 1;
215             }
216             else {
217 1         218 croak "Unrecognised setup operation '$key'";
218             }
219              
220 430         1847 push @setup, $key => $value;
221             }
222              
223 248         981 return @setup;
224             }
225              
226             sub _spawn_in_parent
227             {
228 295     295   7812 my $self = shift;
229 295         3922 my ( $readpipe, $kid, $on_exit ) = @_;
230              
231 295         1566 my $loop = $self->{loop};
232              
233             # We need to wait for both the errno pipe to close, and for waitpid
234             # to give us an exit code. We'll form two closures over these two
235             # variables so we can cope with those happening in either order
236              
237 295         1242 my $dollarbang;
238 295         2310 my ( $dollarat, $length_dollarat );
239 295         0 my $exitcode;
240 295         1454 my $pipeclosed = 0;
241              
242             $loop->add( IO::Async::Stream->new(
243             notifier_name => "statuspipe,kid=$kid",
244              
245             read_handle => $readpipe,
246              
247             on_read => sub {
248 670     670   1863 my ( $self, $buffref, $eof ) = @_;
249              
250 670 100       2354 if( !defined $dollarbang ) {
    100          
251 288 100       1275 if( length( $$buffref ) >= 2 * LENGTH_OF_I ) {
252 194         1073 ( $dollarbang, $length_dollarat ) = unpack( "II", $$buffref );
253 194         680 substr( $$buffref, 0, 2 * LENGTH_OF_I, "" );
254 194         716 return 1;
255             }
256             }
257             elsif( !defined $dollarat ) {
258 191 50       633 if( length( $$buffref ) >= $length_dollarat ) {
259 191         914 $dollarat = substr( $$buffref, 0, $length_dollarat, "" );
260 191         765 return 1;
261             }
262             }
263              
264 285 50       1251 if( $eof ) {
265 285 100       908 $dollarbang = 0 if !defined $dollarbang;
266 285 100       1062 if( !defined $length_dollarat ) {
267 94         573 $length_dollarat = 0;
268 94         615 $dollarat = "";
269             }
270              
271 285         589 $pipeclosed = 1;
272              
273 285 100       1045 if( defined $exitcode ) {
274 91         1914 local $! = $dollarbang;
275 91         574 $on_exit->( $kid, $exitcode, $!, $dollarat );
276             }
277             }
278              
279 285         3579 return 0;
280             }
281 295         72019 ) );
282              
283             $loop->watch_process( $kid => sub {
284 286     286   1276 ( my $kid, $exitcode ) = @_;
285              
286 286 100       966 if( $pipeclosed ) {
287 192         2494 local $! = $dollarbang;
288 192         1036 $on_exit->( $kid, $exitcode, $!, $dollarat );
289             }
290 295         17075 } );
291              
292 295         63650 return $kid;
293             }
294              
295             sub _spawn_in_child
296             {
297 29     29   3080 my $self = shift;
298 29         923 my ( $writepipe, $code, $setup ) = @_;
299              
300 29         21493 my $exitvalue = eval {
301             # Map of which handles will be in use by the end
302 29         2019 my %fd_in_use = ( 0 => 1, 1 => 1, 2 => 1 ); # Keep STDIN, STDOUT, STDERR
303              
304             # Count of how many times we'll need to use the current handles.
305 29         1049 my %fds_refcount = %fd_in_use;
306              
307             # To dup2() without clashes we might need to temporarily move some handles
308 29         252 my %dup_from;
309              
310 29         411 my $max_fd = 0;
311 29         230 my $writepipe_clashes = 0;
312              
313 29 100       698 if( @$setup ) {
314             # The writepipe might be in the way of a setup filedescriptor. If it
315             # is we'll have to dup2 it out of the way then close the original.
316 22         504 foreach my $i ( 0 .. $#$setup/2 ) {
317 41         607 my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
318 41 50       1902 $key =~ m/^fd(\d+)$/ or next;
319 41         566 my $fd = $1;
320              
321 41 100       1547 $max_fd = $fd if $fd > $max_fd;
322 41 50       5033 $writepipe_clashes = 1 if $fd == fileno $writepipe;
323              
324 41         614 my ( $operation, @params ) = @$value;
325              
326 41 50       2873 $operation eq "close" and do {
327 0         0 delete $fd_in_use{$fd};
328 0         0 delete $fds_refcount{$fd};
329             };
330              
331 41 100       585 $operation eq "dup" and do {
332 39         154 $fd_in_use{$fd} = 1;
333              
334 39         314 my $fileno = fileno $params[0];
335             # Keep a count of how many times it will be dup'ed from so we
336             # can close it once we've finished
337 39         1557 $fds_refcount{$fileno}++;
338              
339 39         459 $dup_from{$fileno} = $fileno;
340             };
341              
342 41 100       2901 $operation eq "keep" and do {
343 2         31 $fds_refcount{$fd} = 1;
344             };
345             }
346             }
347              
348 29         6175 foreach ( IO::Async::OS->potentially_open_fds ) {
349 437 100       1739 next if $fds_refcount{$_};
350 309 100       834 next if $_ == fileno $writepipe;
351 280         4911 POSIX::close( $_ );
352             }
353              
354 29 100       618 if( @$setup ) {
355 22 50       995 if( $writepipe_clashes ) {
356 0         0 $max_fd++;
357              
358 0 0       0 dup2( fileno $writepipe, $max_fd ) or die "Cannot dup2(writepipe to $max_fd) - $!\n";
359 0         0 undef $writepipe;
360 0 0       0 open( $writepipe, ">&=$max_fd" ) or die "Cannot fdopen($max_fd) as writepipe - $!\n";
361             }
362              
363 22         280 foreach my $i ( 0 .. $#$setup/2 ) {
364 41         225 my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
365              
366 41 50       476 if( $key =~ m/^fd(\d+)$/ ) {
    0          
    0          
    0          
    0          
    0          
    0          
367 41         760 my $fd = $1;
368 41         391 my( $operation, @params ) = @$value;
369              
370 41 100       477 $operation eq "dup" and do {
371 39         585 my $from = fileno $params[0];
372              
373 39 50       283 if( $from != $fd ) {
374 39 50       354 if( exists $dup_from{$fd} ) {
375 0 0       0 defined( $dup_from{$fd} = dup( $fd ) ) or die "Cannot dup($fd) - $!";
376             }
377              
378 39         231 my $real_from = $dup_from{$from};
379              
380 39         543 POSIX::close( $fd );
381 39 50       422 dup2( $real_from, $fd ) or die "Cannot dup2($real_from to $fd) - $!\n";
382             }
383              
384 39         284 $fds_refcount{$from}--;
385 39 50 33     760 if( !$fds_refcount{$from} and !$fd_in_use{$from} ) {
386 39         332 POSIX::close( $from );
387 39         152 delete $dup_from{$from};
388             }
389             };
390              
391 41 50       395 $operation eq "open" and do {
392 0         0 my ( $mode, $filename ) = @params;
393 0 0       0 open( my $fh, $mode, $filename ) or die "Cannot open('$mode', '$filename') - $!\n";
394              
395 0         0 my $from = fileno $fh;
396 0 0       0 dup2( $from, $fd ) or die "Cannot dup2($from to $fd) - $!\n";
397              
398 0         0 close $fh;
399             };
400             }
401             elsif( $key eq "env" ) {
402 0         0 %ENV = %$value;
403             }
404             elsif( $key eq "nice" ) {
405 0 0       0 nice( $value ) or die "Cannot nice($value) - $!";
406             }
407             elsif( $key eq "chdir" ) {
408 0 0       0 chdir( $value ) or die "Cannot chdir('$value') - $!";
409             }
410             elsif( $key eq "setuid" ) {
411 0 0       0 setuid( $value ) or die "Cannot setuid('$value') - $!";
412             }
413             elsif( $key eq "setgid" ) {
414 0 0       0 setgid( $value ) or die "Cannot setgid('$value') - $!";
415             }
416             elsif( $key eq "setgroups" ) {
417 0 0       0 setgroups( @$value ) or die "Cannot setgroups() - $!";
418             }
419             }
420             }
421              
422 29         289 $code->();
423             };
424              
425 0           my $writebuffer = "";
426 0           $writebuffer .= pack( "I", $!+0 );
427 0           $writebuffer .= pack( "I", length( $@ ) ) . $@;
428              
429 0           syswrite( $writepipe, $writebuffer );
430              
431 0           return $exitvalue;
432             }
433              
434             0x55AA;