NAME

Fugu::Process - child process management

SYNOPSIS

use Fugu::Process;

my $result = Fugu::Process->spawn_command(
    cmd       => [ '/usr/local/bin/mydaemon', '-f' ],
    daemonize => 1,
    stdout    => '/var/log/mydaemon.log',
);

Fugu::Process->terminate($result->{pid}) if $result->{success};

my $r = Fugu::Process->run(cmd => [ 'rcctl', 'check', 'mydaemon' ]);
print $r->{stdout} if $r->{success};

DESCRIPTION

Fugu::Process does the parts of child-process control that are easy to get wrong. It shows the difference between a child that runs and a child that exits immediately. It sends SIGTERM first and sends SIGKILL after a delay. It reaps zombies and does not block.

Two methods start a child. spawn_command() leaves it running. run() waits for it and captures what it wrote. Both report a failed execve(2) exactly, over a close-on-exec pipe, and never by a wait-and-guess sleep(3). Neither one runs a shell: the command is a list, so no argument needs quoting and no argument can become a shell operator.

The module keeps no state. Every method is a class method.

spawn_command

spawn_command(%args) forks, redirects the standard descriptors, and runs a command.

These are the arguments:

cmd

An array reference that holds the command and its arguments. This argument is necessary and must not be empty.

daemonize

If this argument is true, the child calls setsid(2) before exec. The default is false.

stdin, stdout, stderr

The paths for the child's standard descriptors. The default for each path is /dev/null.

The parent always waits for the execve(2) to resolve. The child holds the write end of a close-on-exec pipe. A successful execve(2) closes that end, and the parent reads end-of-file. A failure leaves a message in the pipe, and the parent returns it in error. Thus a command that does not exist reports its own reason at once.

run

run(%args) runs a command to completion and captures what it wrote.

These are the arguments:

cmd

An array reference that holds the command and its arguments. This argument is necessary and must not be empty.

timeout

The number of seconds to wait before the method stops the child. The default is no limit.

stdin

A string to feed to the child on its standard input.

cwd

A directory to run the child in. The child calls chdir(2) after the fork and before the execve(2), so the working directory of the caller does not change. A chdir(2) in the caller would change the meaning of every other relative path in the program, and a second call that ran at the same time would race it.

A directory that the child cannot enter is a startup failure with the reason in error, not a silent run in the wrong place.

passthrough

Let the child write straight to the caller's terminal. stdout and stderr then come back empty.

The method reads standard output and standard error at the same time. A reader that took them in sequence would deadlock: a child that fills one pipe blocks until someone drains it.

exit_code

exit_code($status) maps a raw waitpid(2) status, or the return value of system, to an exit code between 0 and 255. The low byte holds the terminating signal. The high byte holds the exit code. A value of -1 means the child never started.

A caller that gives a raw status 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.

is_alive

is_alive($pid) reports if a process exists and is not a zombie. The check reaps a zombie child as a side effect and then reports it as not alive. A caller that needs the exit status uses run(), or waits itself.

terminate

terminate($pid, %args) sends SIGTERM, waits, and sends SIGKILL if the process continues to run.

These are the arguments:

grace_period

The number of seconds to wait between the two signals. The default is 5.

on_kill

A code reference that the method calls when the process is gone.

The wait polls with sub-second granularity. Thus a child that stops at once does not cost a whole second.

wait_exit

wait_exit($pid, $timeout) polls until the process exits or until the timeout ends. The default for $timeout is 30 seconds.

spawn_perl

spawn_perl(%args) runs Perl code in a child process. It gives the child the parent's -I paths. The parent gets these paths from -I, use lib or PERL5LIB. Thus the child sees the same modules.

code is the program text. args is an array reference of arguments for the program. The method gives all other arguments to spawn_command().

RETURN VALUES

spawn_command() and spawn_perl() return a hash reference. On success, the hash holds success set to 1 and pid. On failure, success is 0 and error gives the cause.

run() returns a hash reference that holds success, stdout, stderr, exit_code and timed_out. On a failure to start the child, it also holds error. success is 1 only when the child exited with code 0 and did not time out.

exit_code() returns a number between 0 and 255.

is_alive() returns 1 or 0.

terminate() returns 1 if the process is gone. It returns 0 if the process continues after SIGKILL.

wait_exit() returns 1 if the process exits in the timeout period. If not, it returns 0.

EXAMPLES

This example runs a helper and then stops it:

my $r = Fugu::Process->spawn_command(
    cmd => [ 'mdnsctl', 'publish', $name, '_hap', 'tcp', $port ],
);

Fugu::Process->terminate($r->{pid}, grace_period => 10)
    if $r->{success};

ERRORS

No method dies. The methods report failures through the hash reference or the boolean value that they return.

SEE ALSO

execve(2), kill(2), setsid(2), waitpid(2), Fugu::Log, Fugu::Pidfile

AUTHORS

Dick Olsson <hi@senzilla.io>

CAVEATS

run() holds the whole output of the child in memory. Do not use it for a command that writes without a bound.

is_alive() calls waitpid(2). That call reaps only the children of the caller. For all other processes, it uses kill(2) with signal 0. This signal cannot show the difference between a live process and a zombie.

The operating system uses process IDs again for new processes. The module cannot show the difference between the initial process and a later process with the same number.