#!/usr/bin/perl package POE::Component::Supervisor::Handle::Proc; use MooseX::POE; use POSIX qw(WIFSIGNALED WIFEXITED WEXITSTATUS WTERMSIG); use POE::Wheel::Run; use namespace::clean -except => 'meta'; with qw( POE::Component::Supervisor::Handle POE::Component::Supervisor::LogDispatch ); use Time::HiRes qw(time); use Scalar::Util (); has wheel_parameters => ( isa => "HashRef", is => "ro", auto_deref => 1, default => sub { +{ } }, ); has enable_nested_poe => ( isa => "Bool", is => "ro", default => 1, ); has start_nested_poe => ( isa => "Bool", is => "ro", default => 1, ); has [map { "std${_}_callback" } qw(out err in)] => ( isa => "CodeRef", is => "rw", required => 0, ); has program => ( isa => 'ArrayRef|CodeRef', is => "ro", required => 1, ); has until_term => ( isa => "Num|Undef", is => "ro", default => 0.1, ); has until_kill => ( isa => "Num|Undef", is => "ro", default => 10, ); has wait_for => ( isa => "Num|Undef", is => "ro", lazy => 1, predicate => "has_wait_for", default => sub { my $self = shift; 5 + ( $self->until_kill || $self->until_term || 0 ); }, ); has _wheel => ( isa => "POE::Wheel::Run", is => "rw", init_arg => undef, clearer => "_clear_wheel", ); has pid => ( isa => "Int", is => "ro", init_arg => undef, writer => "_pid", ); has exited => ( isa => "Int", is => "rw", init_arg => undef, required => 0, predicate => "has_exited", writer => "_exited", ); has exite_code => ( isa => "Int", is => "rw", init_arg => undef, required => 0, predicate => "has_exit_code", writer => "_exit_code", ); has exit_signal => ( isa => "Int", is => "rw", init_arg => undef, required => 0, predicate => "has_exit_signal", writer => "_exit_signal", ); has _last_sent_signal => ( isa => "Int", ); sub STOP { $_[OBJECT]->logger->debug("stopping child handle session $_[SESSION]"); } sub START { my ( $self, $kernel ) = @_[OBJECT, KERNEL]; $kernel->refcount_increment( $self->get_session_id, __PACKAGE__ ); my $program = $self->_wrapped_program; my $wheel = POE::Wheel::Run->new( StderrEvent => "stderr", StdoutEvent => "stdout", StdinEvent => "stdin", $self->wheel_parameters, Program => $program, ); my $pid = $wheel->PID; $self->_wheel($wheel); $self->_pid($pid); $self->notify_spawn( pid => $pid ); $kernel->sig_child( $wheel->PID, "child_exit" ); } sub _wrapped_program { my ( $self, $program ) = @_; $program ||= $self->program; if ( ref($program) eq 'CODE' ) { if ( $self->enable_nested_poe ) { my $also_start = $self->start_nested_poe; return sub { my @args = @_; $poe_kernel->stop; $program->(@args); $poe_kernel->run if $also_start; }, } } return $program; } foreach my $event qw(stdout stderr stdin) { my $cb_name = "${event}_callback"; event $event => sub { if ( my $cb = $_[OBJECT]->$cb_name ) { $cb->(@_); } }; } event child_exit => sub { my ( $self, $exit ) = @_[OBJECT, ARG2]; my $exit_code = WIFEXITED($exit) ? WEXITSTATUS($exit) : undef; my $exit_signal = WIFSIGNALED($exit) ? WTERMSIG($exit) : undef; $self->_exited($exit); $self->_exit_code($exit_code) if defined $exit_code; $self->_exit_signal($exit_signal) if defined($exit_signal); $self->logger->info("child exited with status " . ($exit_code || "undef") . " ($exit), notifying supervisor"); $self->notify_stop( pid => $self->pid, exit => $exit, exit_code => $exit_code, exit_signal => $exit_signal, ); $self->call("_cleanup"); }; event _cleanup => sub { my ( $self, $kernel ) = @_[OBJECT, KERNEL]; if ( my $wheel = $self->_wheel ) { $wheel->shutdown_stdin; $self->_clear_wheel; } $kernel->alarm_remove_all(); $kernel->refcount_decrement( $self->get_session_id, __PACKAGE__ ); }; sub stop { my $self = shift; $self->call("_stop_child"); } event _stop_child => sub { my ( $self, $kernel, $heap ) = @_[OBJECT, KERNEL, HEAP]; $self->call("_close_stdin"); my $now = time; my ( $until_term, $until_kill ) = ( $self->until_term, $self->until_kill ); my $start_term = defined($until_term) && $now + $until_term; my $start_kill = defined($until_kill) && $now + $until_kill; my $give_up = $self->has_wait_for && $now + $self->wait_for; $kernel->alarm_set( _term_loop => $start_term, $start_kill || $give_up ) if $start_term; $kernel->alarm_set( _kill_loop => $start_kill, $give_up ) if $start_kill; $kernel->alarm_set( _couldnt_kill => $give_up ) if $give_up; }; event _close_stdin => sub { my ( $self, $kernel ) = @_[OBJECT, KERNEL]; $self->logger->info("closing child stdin"); if ( my $wheel = $self->_wheel ) { $wheel->shutdown_stdin; } }; foreach my $sig qw(term kill) { my $SIG = uc($sig); my $event = "_${sig}_loop"; event $event => sub { my ( $self, $kernel, $until, $iter ) = @_[OBJECT, KERNEL, ARG0 .. $#_]; $iter ||= 0; my $delay = 2 ** $iter / 10; # exponential back off my $next_attempt = time() + $delay; if ( !defined($until) or $next_attempt < $until ) { $kernel->alarm_set( $event, $next_attempt, $until, $iter + 1 ); } else { undef $delay; } $self->logger->info("sending SIG$SIG, attempt #" . ( $iter + 1) . ( $delay ? ", next attempt in $delay" : " (last attempt)" )); $self->_wheel->kill($SIG); }; } event _couldnt_kill => sub { die "couldn't kill child"; }; sub is_running { my $self = shift; not $self->has_exited; } __PACKAGE__ __END__ =pod =head1 NAME POE::Component::Supervisor::Handle::Proc - A supervisor child handle for a POSIXish process. =head1 SYNOPSIS # created by POE::Component::Supervisor::Supervised::Proc =head1 DESCRIPTION These objects manage a real UNIX process (signalling, monitoring) within a L. =head1 SIGNALLING In order to kill a child process first the child's standard input is closed, then the C signal is sent, and after a wait period the C signal is sent. If the child has not died by the time the C loop times out then an error is thrown (this happens under weird OS scenarios and shouldn't happen normally). The attributes C, C and C determine the durations of these loops. Initially inputs will be closed. Then, after C seconds have passed the C sending loop will start, sending the C signal with an exponential backoff. When C seconds have passed, from the time of the C method being called, the C loop will be stopped, and instead the C signal will be sent, also with an exponential backoff. From the time of the C method being called the handle will wait for a maximum of C seconds before giving up on the child process. ANy of these attributes may be set to C to disable their corresponding behaviors (suppress sending of a certain signal, or wait indefinitely). =head1 ATTRIBUTES B: All the attributes are generally passsed in by L, the factory for this class. They are documented here because that is where their behavior is defined. L will borrow all the attributes from this class that have an C, and as such they should be passed to L, while this class is never instantiated directly.. =over 4 =item until_term The time to wait after closing inputs, and before sending the C signal. Defaults to one tenth of a second. Set to C to disable sending the C signal. =item until_kill The time to wait after closing inputs, and before sending the C signal. Defaults to 10 seconds. Set to C to disable sending the C signal. =item wait_for How long to keep sending exit signals for. Defaults to 5 + ( $self->until_kill || $self->until_term || 0 ) =item enable_nested_poe Whether or not to call L in the child program, before the callback. Only applies to code references. This allows a nested POE kernel to be started in the forked environment without needing to C a new program. Defaults to true. =item start_nested_poe Whether or not to call L in the child program, after the callback. Only applies to code references. Defaults to true. =item program A coderef or an array ref. Passed as the C parameter to the wheel, but may be wrapped depending on the values of C and C if it's a code ref. Required. =item wheel_parameters Additional parameters to pass to L. =item stdin_callback =item stdout_callback =item stderr_callback Callbacks to be fired when the corresponding L events are handled. This only affects the default event handlers, if you ovverride those by passing your own C these callbacks will never take effect. The arguments are passed through as is, see L for the details. Not required. =item pid Read only attribute containing the process ID. =item exited =item exit_code =item exit_signal After the process has exited these read only attributes are filled in with the exit information. C is the raw value of C<$?>, and C and C are the values of applying C and C to that value. See L for details. =item use_logger_singleton Changes the default value of the original L attribute to true. =back =head1 METHODS =over 4 =item new Never called directly, but called by L. =item stop Stop the running process =item is_running Check whether or not the process is still running. =back =head1 EVENTS All L events supported by this object are currently internal, and as such the session corresponding to this object provides no useful L interface. =cut