diff options
Diffstat (limited to 'lib/IO/Async/ChildManager.pm')
-rw-r--r-- | lib/IO/Async/ChildManager.pm | 705 |
1 files changed, 705 insertions, 0 deletions
diff --git a/lib/IO/Async/ChildManager.pm b/lib/IO/Async/ChildManager.pm new file mode 100644 index 0000000..48ac108 --- /dev/null +++ b/lib/IO/Async/ChildManager.pm @@ -0,0 +1,705 @@ +# You may distribute under the terms of either the GNU General Public License +# or the Artistic License (the same terms as Perl itself) +# +# (C) Paul Evans, 2007-2014 -- leonerd@leonerd.org.uk + +package IO::Async::ChildManager; + +use strict; +use warnings; + +our $VERSION = '0.67'; + +# Not a notifier + +use IO::Async::Stream; + +use IO::Async::OS; + +use Carp; +use Scalar::Util qw( weaken ); + +use POSIX qw( _exit dup dup2 nice ); + +use constant LENGTH_OF_I => length( pack( "I", 0 ) ); + +=head1 NAME + +C<IO::Async::ChildManager> - facilitates the execution of child processes + +=head1 SYNOPSIS + +This object is used indirectly via an C<IO::Async::Loop>: + + use IO::Async::Loop; + + my $loop = IO::Async::Loop->new; + + ... + + $loop->run_child( + command => "/bin/ps", + + on_finish => sub { + my ( $pid, $exitcode, $stdout, $stderr ) = @_; + my $status = ( $exitcode >> 8 ); + print "ps [PID $pid] exited with status $status\n"; + }, + ); + + $loop->open_child( + command => [ "/bin/ping", "-c4", "some.host" ], + + stdout => { + on_read => sub { + my ( $stream, $buffref, $eof ) = @_; + while( $$buffref =~ s/^(.*)\n// ) { + print "PING wrote: $1\n"; + } + return 0; + }, + }, + + on_finish => sub { + my ( $pid, $exitcode ) = @_; + my $status = ( $exitcode >> 8 ); + ... + }, + ); + + my ( $pipeRd, $pipeWr ) = IO::Async::OS->pipepair; + $loop->spawn_child( + command => "/usr/bin/my-command", + + setup => [ + stdin => [ "open", "<", "/dev/null" ], + stdout => $pipeWr, + stderr => [ "open", ">>", "/var/log/mycmd.log" ], + chdir => "/", + ] + + on_exit => sub { + my ( $pid, $exitcode ) = @_; + my $status = ( $exitcode >> 8 ); + print "Command exited with status $status\n"; + }, + ); + + $loop->spawn_child( + code => sub { + do_something; # executes in a child process + return 1; + }, + + on_exit => sub { + my ( $pid, $exitcode, $dollarbang, $dollarat ) = @_; + my $status = ( $exitcode >> 8 ); + print "Child process exited with status $status\n"; + print " OS error was $dollarbang, exception was $dollarat\n"; + }, + ); + +=head1 DESCRIPTION + +This module extends the functionality of the containing C<IO::Async::Loop> to +manage the execution of child processes. It acts as a central point to store +PID values of currently-running children, and to call the appropriate +continuation handler code when the process terminates. It provides useful +wrapper methods that set up filehandles and other child process details, and +to capture the child process's STDOUT and STDERR streams. + +=cut + +# Writing to variables of $> and $) have tricky ways to obtain error results +sub setuid +{ + my ( $uid ) = @_; + + $> = $uid; my $saved_errno = $!; + $> == $uid and return 1; + + $! = $saved_errno; + return undef; +} + +sub setgid +{ + my ( $gid ) = @_; + + $) = $gid; my $saved_errno = $!; + $) == $gid and return 1; + + $! = $saved_errno; + return undef; +} + +sub setgroups +{ + my @groups = @_; + + my $gid = $)+0; + # Put the primary GID as the first group in the supplementary list, because + # some operating systems ignore this position, expecting it to indeed be + # the primary GID. + # See + # https://rt.cpan.org/Ticket/Display.html?id=65127 + @groups = grep { $_ != $gid } @groups; + + $) = "$gid $gid " . join " ", @groups; my $saved_errno = $!; + + # No easy way to detect success or failure. Just check that we have all and + # only the right groups + my %gotgroups = map { $_ => 1 } split ' ', "$)"; + + $! = $saved_errno; + $gotgroups{$_}-- or return undef for @groups; + keys %gotgroups or return undef; + + return 1; +} + +# Internal constructor +sub new +{ + my $class = shift; + my ( %params ) = @_; + + my $loop = delete $params{loop} or croak "Expected a 'loop'"; + + my $self = bless { + loop => $loop, + }, $class; + + weaken( $self->{loop} ); + + return $self; +} + +=head1 METHODS + +When active, the following methods are available on the containing C<Loop> +object. + +=cut + +=head2 $pid = $loop->spawn_child( %params ) + +This method creates a new child process to run a given code block or command. +The C<%params> hash takes the following keys: + +=over 8 + +=item command => ARRAY or STRING + +Either a reference to an array containing the command and its arguments, or a +plain string containing the command. This value is passed into perl's +C<exec> function. + +=item code => CODE + +A block of code to execute in the child process. It will be called in scalar +context inside an C<eval> block. + +=item setup => ARRAY + +A reference to an array which gives file descriptors to set up in the child +process before running the code or command. See below. + +=item on_exit => CODE + +A continuation to be called when the child processes exits. It will be invoked +in the following way: + + $on_exit->( $pid, $exitcode, $dollarbang, $dollarat ) + +The second argument is passed the plain perl C<$?> value. + +=back + +Exactly one of the C<command> or C<code> keys must be specified. + +If the C<command> key is used, the given array or string is executed using the +C<exec> function. + +If the C<code> key is used, the return value will be used as the C<exit(2)> +code from the child if it returns (or 255 if it returned C<undef> or thows an +exception). + + Case | ($exitcode >> 8) | $dollarbang | $dollarat + --------------+------------------------+-------------+---------- + exec succeeds | exit code from program | 0 | "" + exec fails | 255 | $! | "" + $code returns | return value | $! | "" + $code dies | 255 | $! | $@ + +It is usually more convenient to use the C<open_child> method in simple cases +where an external program is being started in order to interact with it via +file IO, or even C<run_child> when only the final result is required, rather +than interaction while it is running. + +=cut + +sub spawn_child +{ + my $self = shift; + my %params = @_; + + my $command = delete $params{command}; + my $code = delete $params{code}; + my $setup = delete $params{setup}; + my $on_exit = delete $params{on_exit}; + + if( %params ) { + croak "Unrecognised options to spawn: " . join( ",", keys %params ); + } + + defined $command and defined $code and + croak "Cannot pass both 'command' and 'code' to spawn"; + + defined $command or defined $code or + croak "Must pass one of 'command' or 'code' to spawn"; + + my @setup = defined $setup ? $self->_check_setup_and_canonicise( $setup ) : (); + + my $loop = $self->{loop}; + + my ( $readpipe, $writepipe ); + + { + # Ensure it's FD_CLOEXEC - this is a bit more portable than manually + # fiddling with F_GETFL and F_SETFL (e.g. MSWin32) + local $^F = -1; + + ( $readpipe, $writepipe ) = IO::Async::OS->pipepair or croak "Cannot pipe() - $!"; + } + + if( defined $command ) { + my @command = ref( $command ) ? @$command : ( $command ); + + $code = sub { + no warnings; + exec( @command ); + return; + }; + } + + my $kid = $loop->fork( + code => sub { + # Child + close( $readpipe ); + $self->_spawn_in_child( $writepipe, $code, \@setup ); + }, + ); + + # Parent + close( $writepipe ); + return $self->_spawn_in_parent( $readpipe, $kid, $on_exit ); +} + +=head2 C<setup> array + +This array gives a list of file descriptor operations to perform in the child +process after it has been C<fork(2)>ed from the parent, before running the code +or command. It consists of name/value pairs which are ordered; the operations +are performed in the order given. + +=over 8 + +=item fdI<n> => ARRAY + +Gives an operation on file descriptor I<n>. The first element of the array +defines the operation to be performed: + +=over 4 + +=item [ 'close' ] + +The file descriptor will be closed. + +=item [ 'dup', $io ] + +The file descriptor will be C<dup2(2)>ed from the given IO handle. + +=item [ 'open', $mode, $file ] + +The file descriptor will be opened from the named file in the given mode. The +C<$mode> string should be in the form usually given to the C<open> function; +such as '<' or '>>'. + +=item [ 'keep' ] + +The file descriptor will not be closed; it will be left as-is. + +=back + +A non-reference value may be passed as a shortcut, where it would contain the +name of the operation with no arguments (i.e. for the C<close> and C<keep> +operations). + +=item IO => ARRAY + +Shortcut for passing C<fdI<n>>, where I<n> is the fileno of the IO +reference. In this case, the key must be a reference that implements the +C<fileno> method. This is mostly useful for + + $handle => 'keep' + +=item fdI<n> => IO + +A shortcut for the C<dup> case given above. + +=item stdin => ... + +=item stdout => ... + +=item stderr => ... + +Shortcuts for C<fd0>, C<fd1> and C<fd2> respectively. + +=item env => HASH + +A reference to a hash to set as the child process's environment. + +Note that this will entirely set a new environment, completely replacing the +existing one. If you want to simply add new keys or change the values of some +keys without removing the other existing ones, you can simply copy C<%ENV> +into the hash before setting new keys: + + env => { + %ENV, + ANOTHER => "key here", + } + +=item nice => INT + +Change the child process's scheduling priority using C<POSIX::nice>. + +=item chdir => STRING + +Change the child process's working directory using C<chdir>. + +=item setuid => INT + +=item setgid => INT + +Change the child process's effective UID or GID. + +=item setgroups => ARRAY + +Change the child process's groups list, to those groups whose numbers are +given in the ARRAY reference. + +On most systems, only the privileged superuser change user or group IDs. +C<IO::Async> will B<NOT> check before detaching the child process whether +this is the case. + +If setting both the primary GID and the supplementary groups list, it is +suggested to set the primary GID first. Moreover, some operating systems may +require that the supplementary groups list contains the primary GID. + +=back + +If no directions for what to do with C<stdin>, C<stdout> and C<stderr> are +given, a default of C<keep> is implied. All other file descriptors will be +closed, unless a C<keep> operation is given for them. + +If C<setuid> is used, be sure to place it after any other operations that +might require superuser privileges, such as C<setgid> or opening special +files. + +=cut + +sub _check_setup_and_canonicise +{ + my $self = shift; + my ( $setup ) = @_; + + ref $setup eq "ARRAY" or croak "'setup' must be an ARRAY reference"; + + return () if !@$setup; + + my @setup; + + my $has_setgroups; + + foreach my $i ( 0 .. $#$setup / 2 ) { + my ( $key, $value ) = @$setup[$i*2, $i*2 + 1]; + + # Rewrite stdin/stdout/stderr + $key eq "stdin" and $key = "fd0"; + $key eq "stdout" and $key = "fd1"; + $key eq "stderr" and $key = "fd2"; + + # Rewrite other filehandles + ref $key and eval { $key->fileno; 1 } and $key = "fd" . $key->fileno; + + if( $key =~ m/^fd(\d+)$/ ) { + my $fd = $1; + my $ref = ref $value; + + if( !$ref ) { + $value = [ $value ]; + } + elsif( $ref eq "ARRAY" ) { + # Already OK + } + elsif( $ref eq "GLOB" or eval { $value->isa( "IO::Handle" ) } ) { + $value = [ 'dup', $value ]; + } + else { + croak "Unrecognised reference type '$ref' for file descriptor $fd"; + } + + my $operation = $value->[0]; + grep { $_ eq $operation } qw( open close dup keep ) or + croak "Unrecognised operation '$operation' for file descriptor $fd"; + } + elsif( $key eq "env" ) { + ref $value eq "HASH" or croak "Expected HASH reference for 'env' setup key"; + } + elsif( $key eq "nice" ) { + $value =~ m/^\d+$/ or croak "Expected integer for 'nice' setup key"; + } + elsif( $key eq "chdir" ) { + # This isn't a purely watertight test, but it does guard against + # silly things like passing a reference - directories such as + # ARRAY(0x12345) are unlikely to exist + -d $value or croak "Working directory '$value' does not exist"; + } + elsif( $key eq "setuid" ) { + $value =~ m/^\d+$/ or croak "Expected integer for 'setuid' setup key"; + } + elsif( $key eq "setgid" ) { + $value =~ m/^\d+$/ or croak "Expected integer for 'setgid' setup key"; + $has_setgroups and carp "It is suggested to 'setgid' before 'setgroups'"; + } + elsif( $key eq "setgroups" ) { + ref $value eq "ARRAY" or croak "Expected ARRAY reference for 'setgroups' setup key"; + m/^\d+$/ or croak "Expected integer in 'setgroups' array" for @$value; + $has_setgroups = 1; + } + else { + croak "Unrecognised setup operation '$key'"; + } + + push @setup, $key => $value; + } + + return @setup; +} + +sub _spawn_in_parent +{ + my $self = shift; + my ( $readpipe, $kid, $on_exit ) = @_; + + my $loop = $self->{loop}; + + # We need to wait for both the errno pipe to close, and for waitpid + # to give us an exit code. We'll form two closures over these two + # variables so we can cope with those happening in either order + + my $dollarbang; + my ( $dollarat, $length_dollarat ); + my $exitcode; + my $pipeclosed = 0; + + $loop->add( IO::Async::Stream->new( + notifier_name => "statuspipe,kid=$kid", + + read_handle => $readpipe, + + on_read => sub { + my ( $self, $buffref, $eof ) = @_; + + if( !defined $dollarbang ) { + if( length( $$buffref ) >= 2 * LENGTH_OF_I ) { + ( $dollarbang, $length_dollarat ) = unpack( "II", $$buffref ); + substr( $$buffref, 0, 2 * LENGTH_OF_I, "" ); + return 1; + } + } + elsif( !defined $dollarat ) { + if( length( $$buffref ) >= $length_dollarat ) { + $dollarat = substr( $$buffref, 0, $length_dollarat, "" ); + return 1; + } + } + + if( $eof ) { + $dollarbang = 0 if !defined $dollarbang; + if( !defined $length_dollarat ) { + $length_dollarat = 0; + $dollarat = ""; + } + + $pipeclosed = 1; + + if( defined $exitcode ) { + local $! = $dollarbang; + $on_exit->( $kid, $exitcode, $!, $dollarat ); + } + } + + return 0; + } + ) ); + + $loop->watch_child( $kid => sub { + ( my $kid, $exitcode ) = @_; + + if( $pipeclosed ) { + local $! = $dollarbang; + $on_exit->( $kid, $exitcode, $!, $dollarat ); + } + } ); + + return $kid; +} + +sub _spawn_in_child +{ + my $self = shift; + my ( $writepipe, $code, $setup ) = @_; + + my $exitvalue = eval { + # Map of which handles will be in use by the end + my %fd_in_use = ( 0 => 1, 1 => 1, 2 => 1 ); # Keep STDIN, STDOUT, STDERR + + # Count of how many times we'll need to use the current handles. + my %fds_refcount = %fd_in_use; + + # To dup2() without clashes we might need to temporarily move some handles + my %dup_from; + + my $max_fd = 0; + my $writepipe_clashes = 0; + + if( @$setup ) { + # The writepipe might be in the way of a setup filedescriptor. If it + # is we'll have to dup2 it out of the way then close the original. + foreach my $i ( 0 .. $#$setup/2 ) { + my ( $key, $value ) = @$setup[$i*2, $i*2 + 1]; + $key =~ m/^fd(\d+)$/ or next; + my $fd = $1; + + $max_fd = $fd if $fd > $max_fd; + $writepipe_clashes = 1 if $fd == fileno $writepipe; + + my ( $operation, @params ) = @$value; + + $operation eq "close" and do { + delete $fd_in_use{$fd}; + delete $fds_refcount{$fd}; + }; + + $operation eq "dup" and do { + $fd_in_use{$fd} = 1; + + my $fileno = fileno $params[0]; + # Keep a count of how many times it will be dup'ed from so we + # can close it once we've finished + $fds_refcount{$fileno}++; + + $dup_from{$fileno} = $fileno; + }; + + $operation eq "keep" and do { + $fds_refcount{$fd} = 1; + }; + } + } + + foreach ( IO::Async::OS->potentially_open_fds ) { + next if $fds_refcount{$_}; + next if $_ == fileno $writepipe; + POSIX::close( $_ ); + } + + if( @$setup ) { + if( $writepipe_clashes ) { + $max_fd++; + + dup2( fileno $writepipe, $max_fd ) or die "Cannot dup2(writepipe to $max_fd) - $!\n"; + undef $writepipe; + open( $writepipe, ">&=$max_fd" ) or die "Cannot fdopen($max_fd) as writepipe - $!\n"; + } + + foreach my $i ( 0 .. $#$setup/2 ) { + my ( $key, $value ) = @$setup[$i*2, $i*2 + 1]; + + if( $key =~ m/^fd(\d+)$/ ) { + my $fd = $1; + my( $operation, @params ) = @$value; + + $operation eq "dup" and do { + my $from = fileno $params[0]; + + if( $from != $fd ) { + if( exists $dup_from{$fd} ) { + defined( $dup_from{$fd} = dup( $fd ) ) or die "Cannot dup($fd) - $!"; + } + + my $real_from = $dup_from{$from}; + + POSIX::close( $fd ); + dup2( $real_from, $fd ) or die "Cannot dup2($real_from to $fd) - $!\n"; + } + + $fds_refcount{$from}--; + if( !$fds_refcount{$from} and !$fd_in_use{$from} ) { + POSIX::close( $from ); + delete $dup_from{$from}; + } + }; + + $operation eq "open" and do { + my ( $mode, $filename ) = @params; + open( my $fh, $mode, $filename ) or die "Cannot open('$mode', '$filename') - $!\n"; + + my $from = fileno $fh; + dup2( $from, $fd ) or die "Cannot dup2($from to $fd) - $!\n"; + + close $fh; + }; + } + elsif( $key eq "env" ) { + %ENV = %$value; + } + elsif( $key eq "nice" ) { + nice( $value ) or die "Cannot nice($value) - $!"; + } + elsif( $key eq "chdir" ) { + chdir( $value ) or die "Cannot chdir('$value') - $!"; + } + elsif( $key eq "setuid" ) { + setuid( $value ) or die "Cannot setuid('$value') - $!"; + } + elsif( $key eq "setgid" ) { + setgid( $value ) or die "Cannot setgid('$value') - $!"; + } + elsif( $key eq "setgroups" ) { + setgroups( @$value ) or die "Cannot setgroups() - $!"; + } + } + } + + $code->(); + }; + + my $writebuffer = ""; + $writebuffer .= pack( "I", $!+0 ); + $writebuffer .= pack( "I", length( $@ ) ) . $@; + + syswrite( $writepipe, $writebuffer ); + + return $exitvalue; +} + +=head1 AUTHOR + +Paul Evans <leonerd@leonerd.org.uk> + +=cut + +0x55AA; |