Fugu
view release on metacpan or search on metacpan
lib/Fugu/Process.pm view on Meta::CPAN
}
close $in_w;
open STDIN, '<&', $in_r
or _fail( $exec_w, "Cannot redirect stdin: $!" );
} );
close $in_r;
if ($error) {
close $in_w;
return _run_error($error);
}
{
local $SIG{PIPE} = 'IGNORE';
print {$in_w} $input if defined $input && length $input;
}
close $in_w;
my $timed_out = 0;
if ( defined $timeout ) {
unless ( $class->wait_exit( $pid, $timeout ) ) {
$class->terminate(
$pid,
grace_period => 1,
group => $new_session
);
$timed_out = 1;
}
}
waitpid $pid, 0;
my $code = $class->exit_code($?);
return {
success => ( !$timed_out && $code == 0 ) ? 1 : 0,
stdout => '',
stderr => '',
exit_code => $code,
timed_out => $timed_out,
};
}
# $class->exit_code($status):
# Map a raw system() or $? wait status to a 0-255 exit code. The
# low byte encodes the terminating signal. The high byte encodes
# the exit code. system() returns -1 when it cannot start the
# child at all.
#
# A caller that passes the raw status on to exit() turns a remote
# exit code of 1 into exit(256), which the kernel truncates to 0.
# That silently reports a failed command as a success.
sub exit_code ( $class, $status )
{
return EXIT_ERROR if $status == -1;
return 128 + ( $status & 0x7f ) if $status & 0x7f;
return $status >> 8;
}
# $class->is_alive($pid):
# Check if the process is alive (not dead, not a zombie).
# The method returns 1 if the process is alive. It returns 0 if
# the process is dead, a zombie, or does not exist.
#
# The check reaps: a zombie child of the caller is collected
# here, and the answer is 0. A caller that needs the exit status
# uses run, or waits itself.
sub is_alive ( $class, $pid )
{
return 0 unless defined $pid;
return 0 unless $pid =~ /^\d+$/;
# First check if the process exists
return 0 unless kill( 0, $pid );
# Do not try to wait on the current process
return 1 if $pid == $$;
# Try to reap zombies without blocking
my $result = waitpid( $pid, WNOHANG );
# If waitpid returns the PID, the process was a zombie.
# waitpid has now reaped it.
return 0 if $result == $pid;
# If waitpid returns -1, the process is not a child of the
# caller. kill(0) already proved that it exists, so it is alive.
return 1;
}
# $class->terminate($pid, %args):
# Stop a process gracefully. Use force if necessary.
# The method returns 1 if the process is killed or dead. It
# returns 0 on failure.
#
# %args:
# grace_period => $seconds # Time to wait after TERM before KILL (default: 5)
# on_kill => sub() # Runs after a successful kill
# group => 0|1 # Signal the process group of $pid
#
# The wait polls with sub-second granularity, so a child that
# stops at once does not cost a whole second.
#
# With group each signal goes to the process group of $pid, and
# $pid must be the pid of a process-group leader. The liveness
# test is then kill 0 on the group, because a group can outlive
# its leader, so the group form must not return early on a dead
# leader. The method cannot wait for a member that is not its
# child. A member that init has yet to reap can therefore still
# answer for a moment.
sub terminate ( $class, $pid, %args )
{
return $class->_terminate_group( $pid, %args ) if $args{group};
return 1 unless $class->is_alive($pid);
my $grace_period = $args{grace_period} // 5;
my $on_kill = $args{on_kill};
# Send SIGTERM
my $killed = kill 'TERM', $pid;
unless ($killed) {
# The process is already dead, or there is no
# permission
return $class->is_alive($pid) ? 0 : 1;
}
$class->wait_exit( $pid, $grace_period );
# If the process is still alive, kill it with force
if ( $class->is_alive($pid) ) {
kill 'KILL', $pid;
$class->wait_exit( $pid, 1 );
# Final check
return 0 if $class->is_alive($pid);
}
$on_kill->() if $on_kill;
return 1;
}
# $class->_terminate_group($pid, %args):
# The group form of terminate. Each signal goes to the process
# group of $pid, with a negative pid on kill. The method returns
# 1 when no member answers kill 0 on the group. It returns 0
# when a member still answers after the KILL.
#
# The guard on $pid is a safety boundary. kill with the group id
# 0 signals the group of the caller, and kill with the group id
# 1 can reach far outside the caller. A bad $pid must therefore
# signal nothing.
sub _terminate_group ( $class, $pid, %args )
{
return 1 unless defined $pid && $pid =~ /^\d+$/ && $pid > 1;
return 1 unless _group_alive($pid);
my $grace_period = $args{grace_period} // 5;
my $on_kill = $args{on_kill};
# Send SIGTERM to the whole group
my $killed = kill 'TERM', -$pid;
unless ($killed) {
# Every member is already dead, or there is no
# permission
return _group_alive($pid) ? 0 : 1;
}
_wait_group_exit( $pid, $grace_period );
# If a member is still alive, kill the group with force
if ( _group_alive($pid) ) {
kill 'KILL', -$pid;
_wait_group_exit( $pid, 1 );
# Final check
return 0 if _group_alive($pid);
}
$on_kill->() if $on_kill;
return 1;
}
# _group_alive($pid):
# Report if a member of the process group of $pid still answers
# kill 0. The check reaps each child member first, so a zombie
# child of the caller does not count as a live member.
sub _group_alive ($pid)
{
_reap_group($pid);
return 1 if kill 0, -$pid;
# A member can turn into a zombie between the reap above and
# the check. Reap once more, so a zombie child never outlives
# the answer "gone".
_reap_group($pid);
return 0;
}
# _reap_group($pid):
# Reap each zombie child of the caller in the process group of
# $pid. The leader comes first, by its own pid: the Darwin
# kernel can detach a zombie from its process group, and the
# group sweep below then misses it.
sub _reap_group ($pid)
{
waitpid( $pid, WNOHANG );
1 while waitpid( -$pid, WNOHANG ) > 0;
return;
}
# _wait_group_exit($pid, $timeout):
# Wait until no member of the process group of $pid answers, or
# until the timeout ends. The method returns 1 when the group is
# gone. It returns 0 on timeout.
sub _wait_group_exit ( $pid, $timeout )
{
my $deadline = time + $timeout;
while ( time < $deadline ) {
return 1 unless _group_alive($pid);
select undef, undef, undef, POLL_INTERVAL;
}
# Final check
return _group_alive($pid) ? 0 : 1;
}
# $class->wait_exit($pid, $timeout):
# Wait for the process to exit.
# The method returns 1 if the process exits. It returns 0 on
# timeout.
sub wait_exit ( $class, $pid, $timeout = 30 )
{
my $deadline = time + $timeout;
while ( time < $deadline ) {
return 1 unless $class->is_alive($pid);
select undef, undef, undef, POLL_INTERVAL;
}
# Final check
return $class->is_alive($pid) ? 0 : 1;
}
# $class->spawn_perl(%args):
# Spawn a Perl subprocess that inherits the parent's @INC paths.
# This is a convenience wrapper around spawn_command() to run
# Perl code.
#
# %args:
# code => $string # Required: the Perl code to execute
# args => \@args # Optional: arguments for the code
# The method passes all other args to spawn_command().
#
# Example:
# Fugu::Process->spawn_perl(
# code => 'use MyModule; MyModule->run(@ARGV)',
# args => [$port, $dir],
# daemonize => 1,
# );
sub spawn_perl ( $class, %args )
{
my $code = delete $args{code}
or return { success => 0, error => 'No code specified' };
my $extra_args = delete $args{args} // [];
# Build the -I flags for all non-default @INC paths
my @inc_flags = map { "-I$_" } _custom_inc_paths();
$args{cmd} = [ $^X, @inc_flags, '-e', $code, @$extra_args ];
return $class->spawn_command(%args);
}
# _fork_exec($cmd, $cwd, $env, $redirect):
# The shared fork-and-exec step. Fork the child and run
# $redirect in it to set up the standard handles; failures go
# through _fail. Move the child into $cwd, and give it the
# environment that $env names. Then exec the command over the
# close-on-exec failure pipe. Return ($pid, undef) when the exec
# resolved, or (undef, $error) when the machinery or the exec
# failed - the child is already reaped in that case.
sub _fork_exec ( $cmd, $cwd, $env, $redirect )
{
my ( $exec_r, $exec_w ) = _exec_pipe();
return ( undef, "Cannot create pipe: $!" ) unless $exec_r;
my $pid = fork;
unless ( defined $pid ) {
close $exec_r;
close $exec_w;
return ( undef, "Cannot fork: $!" );
}
if ( $pid == 0 ) {
# Child process
$DB::inhibit_exit = 0;
close $exec_r;
$redirect->($exec_w);
_chdir_or_fail( $exec_w, $cwd );
( run in 0.664 second using v1.01-cache-2.11-cpan-14f38c9f855 )