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.

The module reads the Perl configuration at compile time, so no method call opens a configuration file. A caller that uses pledge(2) must load the module before the pledge. A require of the module after a pledge without the rpath promise aborts the process, and no code in the module can prevent that.

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.

env

A hash reference that names the environment of the child. The keys are the variable names, and the values are the variable values. The child holds exactly the named variables. The parent %ENV does not change: the child assigns its environment between the fork and the execve(2). An empty hash reference gives the child an empty environment. Without this argument, the child inherits the environment of the parent.

The method checks the argument before the fork, and a bad argument starts nothing. These are the errors:

  • The value is not a hash reference.

  • A name holds no character.

  • A name holds an equals sign or a NUL byte.

  • A value is not defined, or a value is a reference.

  • A value holds a NUL byte.

  • A name or a value holds a character above 255. Such a character cannot reach setenv(3) as one byte, and a silent encoding would give the child bytes the caller never named.

The exec resolves a bare command name through execvp(3), and execvp(3) searches PATH. An environment without PATH makes execvp(3) fall back to the default path of the system. Give an absolute path, or name PATH in env. HOME, TERM, TZ and LC_ALL can matter to a given child, and the module adds none of them.

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.

env

The environment of the child, exactly as on spawn_command().

passthrough

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

new_session

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

The child then leads a new session and a new process group, and its group id equals its pid. On a timeout the method signals the whole group, in the capture form and in the passthrough form alike. A grandchild that holds a pipe open therefore dies with the child, and the read of the pipes ends.

setsid(2) removes the controlling terminal. A child with new_session cannot read the terminal and cannot hold the foreground. Do not combine new_session with a command that prompts on the terminal under passthrough.

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.

group

If this argument is true, each signal goes to the process group of $pid. The default is false.

$pid must be the pid of a process-group leader. run() with new_session and spawn_command() with daemonize each make one. The method sends SIGTERM to the group, waits for the grace period, and sends SIGKILL to the group when a member is still alive.

The liveness test differs between the two forms. The default form asks is_alive($pid), which reaps a zombie child. The group form asks kill(2) with signal 0 on the group, and it reaps each child member first. A group can outlive its leader, so the group form does not return early on a dead leader.

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(), so env works here too.

The paths travel as -I flags in the argument list, not in PERL5LIB. An env argument that clears the environment therefore costs the child no module.

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. In the group form, it returns 1 when no member of the group answers kill(2) with signal 0. It returns 0 when a member answers 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 group form of terminate() cannot wait for a member that is not a child of the caller. A member that init(8) has yet to reap can therefore still answer for a moment after the method returns.

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.