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.
Three methods start a child. spawn_command() leaves it running. run() waits for it and captures what it wrote. spawn_peer() connects it to the caller over a socketpair(2), for the OpenBSD pattern of a privileged parent with unprivileged children. Each one reports a failed execve(2) exactly, over a close-on-exec pipe, and never by a wait-and-guess sleep(3). None of them 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
%ENVdoes 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 withoutPATHmakes execvp(3) fall back to the default path of the system. Give an absolute path, or namePATHinenv.HOME,TERM,TZandLC_ALLcan matter to a given child, and the module adds none of them. inherit-
An array reference of open handles that the child keeps across the execve(2). The default is the empty list.
Perl sets
FD_CLOEXECon each descriptor above$^F, which is 2, so a descriptor that the parent opened does not survive an exec on its own. The child clears the flag on each named descriptor. It then closes every other descriptor from 3 upward, and it keeps a named descriptor, a standard descriptor, and the pipe that reports an exec failure. The sweep runs on every call. Perl already closes each flagged descriptor at the exec. The sweep therefore changes the outcome only for a descriptor that stayed inheritable. A library can leave one open, and a caller can clear the flag itself. Name such a descriptor ininheritwhen the child must keep it.On Linux and Darwin the sweep walks the open-descriptor list of the process, from /proc/self/fd or /dev/fd, so a large descriptor limit costs nothing. Where neither list opens, and on OpenBSD, the sweep is a loop to
_SC_OPEN_MAX. The OpenBSD limit is small, and the loop reads no file, so it needs no pledge(2) promise beyond what the caller holds.The method keeps each named descriptor at its own number. A caller that must tell the child a number reads
filenobefore the call, and puts the number incmd.The method dies when the value is not an array reference, and when a member has no descriptor. Both are programming errors.
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.
spawn_peer
spawn_peer(%args) starts a peer child over a socketpair(2). A daemon that follows the OpenBSD parent-and-children pattern uses it: the parent creates the pair, forks, and execs its own program with a role flag, and the child inherits its end of the pair.
These are the arguments:
cmd-
An array reference that holds the command and its arguments. This argument is necessary and must not be empty. The method has no default command: a program can rewrite
$0, and a relative$0breaks after a chdir(2), so the caller names its own program and its own role flag. fd-
The descriptor number that the child receives. The default is 3. The method dies when the number is below 3, and when it collides with an
inheritdescriptor. Both are programming errors. inherit-
Extra handles for the child, exactly as on
spawn_command(). env-
The environment of the child, exactly as on
spawn_command(). A peer child of the privilege-separation pattern holds no secret of the parent environment when the caller names an exact one.
The method creates the pair with socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC), forks, and execs. In the child it closes the parent end, puts the child end on fd with FD_CLOEXEC clear, and sweeps every other descriptor from 3 upward, as under inherit above. In the parent it closes the child end and returns the parent end.
The child opens its end with open($peer, '+<&=', $fd), so it needs no descriptor argument. The parent end comes back as a plain handle, and the caller wraps it when it wants framing, for example with Fugu::Imsg->new(fh => $handle). The module holds no transport.
The method makes no session and no process group. A peer child stays in the group of the parent, so one signal can reach the whole set. The child holds the privileges of the parent at the exec: the privilege drop and the pledge(2) belong to Fugu::Privdrop and Fugu::Sandbox, and the child calls them after the exec.
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.
stdoutandstderrthen 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_sessioncannot read the terminal and cannot hold the foreground. Do not combinenew_sessionwith a command that prompts on the terminal underpassthrough.
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.$pidmust be the pid of a process-group leader.run()withnew_sessionandspawn_command()withdaemonizeeach make one. The method sendsSIGTERMto the group, waits for the grace period, and sendsSIGKILLto 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.
spawn_peer() returns the same shape, and the success hash also holds socket, the parent end of the pair.
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 for a failure of the system: the methods report such a failure through the hash reference or the boolean value that they return.
A programming error dies. These are the cases: an inherit value that is not an array reference, an inherit member with no descriptor, an fd below 3, and an fd that collides with an inherit descriptor.
SEE ALSO
execve(2), kill(2), setsid(2), socketpair(2), waitpid(2), Fugu::Imsg, 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.