#+##############################################################################
#                                                                              #
# File: No/Worries/Proc.pm                                                     #
#                                                                              #
# Description: process handling without worries                                #
#                                                                              #
#-##############################################################################

#
# module definition
#

package No::Worries::Proc;
use strict;
use warnings;
use 5.005; # need the four-argument form of substr()
our $VERSION  = "1.6";
our $REVISION = sprintf("%d.%02d", q$Revision: 1.26 $ =~ /(\d+)\.(\d+)/);

#
# used modules
#

use Config qw(%Config);
use IO::Select qw();
use No::Worries qw($_IntegerRegexp $_NumberRegexp);
use No::Worries::Die qw(dief);
use No::Worries::Dir qw(dir_change);
use No::Worries::Export qw(export_control);
use Params::Validate qw(validate validate_with :types);
use POSIX qw(:sys_wait_h :errno_h setsid);
use Time::HiRes qw();

#
# global variables
#

our(@SigName, $Transient);

#
# check a command to be executed
#

sub _chk_cmd (@) {
    my(@cmd) = @_;
    my($path);

    if ($cmd[0] =~ /\//) {
        dief("invalid command: %s", $cmd[0]) unless -f $cmd[0] and -x _;
    } else {
        $path = $ENV{PATH} || "/usr/bin:/usr/sbin:/bin:/sbin";
        foreach my $dir (split(/:/, $path)) {
            next unless length($dir) and -d $dir;
            next unless -f "$dir/$cmd[0]" and -x _;
            $cmd[0] = "$dir/$cmd[0]";
            last;
        }
        dief("command not found: %s", $cmd[0]) unless $cmd[0] =~ /\//;
    }
    return(\@cmd);
}

#
# definition of the process structure
#

my $nbre = "(\\d+\\.)?\\d+"; # fractional number pattern
my $ksre = "([A-Z]+\\/${nbre}\\s+)*[A-Z]+\\/${nbre}"; # kill spec. pattern

my %proc_structure = (
    # public
    command => { optional => 0, type => ARRAYREF },
    pid     => { optional => 0, type => SCALAR, regex => $_IntegerRegexp },
    start   => { optional => 0, type => SCALAR, regex => $_NumberRegexp },
    stop    => { optional => 1, type => SCALAR, regex => $_NumberRegexp },
    status  => { optional => 1, type => SCALAR, regex => qr/^-?\d+$/ },
    timeout => { optional => 1, type => SCALAR, regex => $_NumberRegexp },
    # private
    kill    => { optional => 1, type => SCALAR, regex => qr/^${ksre}$/ },
    maxtime => { optional => 1, type => SCALAR, regex => $_NumberRegexp },
    fhin    => { optional => 1, type => GLOBREF },
    fhout   => { optional => 1, type => GLOBREF },
    fherr   => { optional => 1, type => GLOBREF },
    bufin   => { optional => 1, type => SCALAR },
    cbout   => { optional => 1, type => CODEREF },
    cberr   => { optional => 1, type => CODEREF },
);

sub _chk_proc ($) {
    my($proc) = @_;

    validate_with(
        params  => $proc,
        spec    => \%proc_structure,
        on_fail => sub { dief("invalid process structure: %s", $_[0]) },
    );
    return(); # so that validate_with() is called in void context
}

#
# close a file handle used for IPC
#

sub _close ($$$$) {
    my($proc, $fh, $what, $ios) = @_;

    $ios->remove($fh) if $ios;
    close($fh) or dief("cannot close(): %s", $!);
    delete($proc->{"fh$what"});
    delete($proc->{"cb$what"});
}

#
# try to read from a dead process in case we called _is_alive() on it
# before all its output pipes got emptied...
#

sub _read_zombie ($$$) {
    my($proc, $iosr, $iosw) = @_;
    my($fh, $buf, $done);

    foreach my $what (qw(in)) {
        next unless $proc->{"fh$what"} and $proc->{"cb$what"};
        $fh = $proc->{"fh$what"};
        # no write, simply close
        _close($proc, $fh, $what, $iosw);
    }
    foreach my $what (qw(out err)) {
        next unless $proc->{"fh$what"} and $proc->{"cb$what"};
        $fh = $proc->{"fh$what"};
        # read until EOF then close
        $done = 1;
        while ($done) {
            last if $iosr and not grep($fh eq $_, $iosr->can_read(1));
            $buf = "";
            $done = sysread($fh, $buf, 8192);
            dief("cannot sysread(): %s", $!) unless defined($done);
            $proc->{"cb$what"}($proc, $buf);
        }
        _close($proc, $fh, $what, $iosr);
    }
}

#
# check if a process is alive, record its status if not
#

sub _is_alive ($$$) {
    my($proc, $iosr, $iosw) = @_;

    # check if it recently died
    if (waitpid($proc->{pid}, WNOHANG) == $proc->{pid}) {
        $proc->{status} = $?;
        $proc->{stop} = Time::HiRes::time();
        delete($proc->{maxtime});
        delete($proc->{kill});
        _read_zombie($proc, $iosr, $iosw);
        return(0); # no
    }
    # check if we can kill it
    if (kill(0, $proc->{pid}) or $! == EPERM) {
        return(1); # yes
    }
    # ooops
    return(); # don't know
}

#
# prepare I/O before creating a process
#

sub _prepare_stdin ($$) {
    my($proc, $stdin) = @_;
    my($ref, $rdrin, $wrtin);

    return() unless defined($stdin);
    $ref = ref($stdin);
    if ($ref eq "") {
        if ($stdin eq "") {
            dief("unexpected stdin: empty string");
        } else {
            ## no critic 'InputOutput::RequireBriefOpen'
            open($rdrin, "<", $stdin)
                or dief("cannot open(<, %s): %s", $stdin, $!);
        }
    } elsif ($ref eq "SCALAR") {
        pipe($rdrin, $wrtin)
            or dief("cannot pipe(): %s", $!);
        $proc->{fhin} = $wrtin;
        $proc->{bufin} = ${ $stdin };
    } else {
        dief("unexpected stdin: ref(%s)", $ref);
    }
    return($rdrin, $wrtin);
}

sub _prepare_stdout ($$) {
    my($proc, $stdout) = @_;
    my($ref, $rdrout, $wrtout);

    return() unless defined($stdout);
    $ref = ref($stdout);
    if ($ref eq "") {
        if ($stdout eq "") {
            dief("unexpected stdout: empty string");
        } else {
            ## no critic 'InputOutput::RequireBriefOpen'
            open($wrtout, ">", $stdout)
                or dief("cannot open(>, %s): %s", $stdout, $!);
        }
    } elsif ($ref eq "CODE" or $ref eq "SCALAR") {
        pipe($rdrout, $wrtout)
            or dief("cannot pipe(): %s", $!);
        $proc->{fhout} = $rdrout;
        if ($ref eq "CODE") {
            $proc->{cbout} = $stdout;
        } else {
            ${ $stdout } = "";
            $proc->{cbout} = sub {
                my($_proc, $_buf) = @_;
                ${ $stdout } .= $_buf;
            };
        }
    } else {
        dief("unexpected stdout: ref(%s)", $ref);
    }
    return($rdrout, $wrtout);
}

sub _prepare_stderr ($$) {
    my($proc, $stderr) = @_;
    my($ref, $rdrerr, $wrterr, $merge);

    return() unless defined($stderr);
    $ref = ref($stderr);
    if ($ref eq "") {
        if ($stderr eq "") {
            # special case: stderr will be merged with stdout
            $merge = 1;
        } else {
            ## no critic 'InputOutput::RequireBriefOpen'
            open($wrterr, ">", $stderr)
                or dief("cannot open(>, %s): %s", $stderr, $!);
        }
    } elsif ($ref eq "CODE" or $ref eq "SCALAR") {
        pipe($rdrerr, $wrterr)
            or dief("cannot pipe(): %s", $!);
        $proc->{fherr} = $rdrerr;
        if ($ref eq "CODE") {
            $proc->{cberr} = $stderr;
        } else {
            ${ $stderr } = "";
            $proc->{cberr} = sub {
                my($_proc, $_buf) = @_;
                ${ $stderr } .= $_buf;
            };
        }
    } else {
        dief("unexpected stderr: ref(%s)", $ref);
    }
    return($rdrerr, $wrterr, $merge);
}

#
# redirect I/O after creating a process
#

sub _redirect_io ($$$$) {
    my($rdrin, $wrtout, $wrterr, $merge) = @_;
    my($fd);

    # handle stdin
    if ($rdrin) {
        $fd = fileno($rdrin);
        if (fileno(*STDIN) != $fd) {
            open(*STDIN, "<&=$fd")
                or dief("cannot redirect stdin: %s", $!);
        }
    }
    # handle stdout
    if ($wrtout) {
        $fd = fileno($wrtout);
        if (fileno(*STDOUT) != $fd) {
            open(*STDOUT, ">&=$fd")
                or dief("cannot redirect stdout: %s", $!);
        }
    }
    # handle stderr
    if ($wrterr or $merge) {
        $fd = $merge ? fileno(*STDOUT) : fileno($wrterr);
        if (fileno(*STDERR) != $fd) {
            open(*STDERR, ">&=$fd")
                or dief("cannot redirect stderr: %s", $!);
        }
    }
}

#
# fork a new process, setup its environment and exec() the command
#

my %proc_create_options = (
    command => { optional => 0, type => ARRAYREF },
    cwd     => { optional => 1, type => SCALAR },
    timeout => { optional => 1, type => SCALAR, regex => $_NumberRegexp },
    kill    => { optional => 1, type => SCALAR, regex => qr/^${ksre}$/ },
    stdin   => { optional => 1, type => SCALAR | SCALARREF },
    stdout  => { optional => 1, type => SCALAR | SCALARREF | CODEREF },
    stderr  => { optional => 1, type => SCALAR | SCALARREF | CODEREF },
);

sub proc_create (@) {
    my(%option, %proc, $merge);
    my($rdrin, $wrtin, $rdrout, $wrtout, $rdrerr, $wrterr);

    #
    # preparation
    #

    %option = validate(@_, \%proc_create_options);
    $proc{command} = _chk_cmd(@{ $option{command} });
    # check the "current working directory" option
    if (defined($option{cwd})) {
        dief("invalid directory: %s", $option{cwd}) unless -d $option{cwd};
    }
    # prepare I/O
    ($rdrin, $wrtin) = _prepare_stdin(\%proc, $option{stdin});
    ($rdrout, $wrtout) = _prepare_stdout(\%proc, $option{stdout});
    ($rdrerr, $wrterr, $merge) = _prepare_stderr(\%proc, $option{stderr});
    # fork
    $proc{pid} = fork();
    dief("cannot fork(): %s", $!) unless defined($proc{pid});

    #
    # handle the child
    #

    unless ($proc{pid}) {
        # we are about to exec() or die()
        $Transient = 1;
        # handle the "current working directory"
        dir_change($option{cwd}) if defined($option{cwd});
        # make sure the STD* file handles are "normal"
        foreach my $glob (*STDIN, *STDOUT, *STDERR) {
            next unless tied($glob);
            no warnings qw(untie);  ## no critic 'ProhibitNoWarnings'
            untie($glob);
        }
        # handle the pipe ends to close
        foreach my $fh ($wrtin, $rdrout, $rdrerr) {
            next unless $fh;
            close($fh) or dief("cannot close pipe: %s", $!);
        }
        # redirect I/O
        _redirect_io($rdrin, $wrtout, $wrterr, $merge);
        # execute the command
        exec({ $proc{command}[0] } @{ $proc{command} })
            or dief("cannot execute %s: %s", $proc{command}[0], $!);
        exit(-1);
    }

    #
    # handle the father
    #

    # record the "start" time
    $proc{start} = Time::HiRes::time();
    # record the maximum running time
    if (defined($option{timeout})) {
        $proc{maxtime} = $proc{start} + $option{timeout};
    }
    # record the kill specification
    $proc{kill} = $option{kill} if $option{kill};
    # handle the pipe ends to close
    foreach my $fh ($rdrin, $wrtout, $wrterr) {
        next unless $fh;
        close($fh) or dief("cannot close pipe: %s", $!);
    }
    # so far so good
    return(\%proc);
}

#
# terminate a process
#

my %proc_terminate_options = (
    kill  => { optional => 1, type => SCALAR, regex => qr/^${ksre}$/ },
    _iosr => { optional => 1, type => UNDEF|OBJECT },
    _iosw => { optional => 1, type => UNDEF|OBJECT },
);

sub proc_terminate ($@) {
    my($proc, %option, $pid, $sig, $grace, $maxtime);

    # setup
    $proc = shift(@_);
    if (ref($proc) eq "") {
        dief("unexpected pid: %s", $proc) unless $proc =~ /^\d+$/;
        $proc = { pid => $proc };
    } elsif (ref($proc) eq "HASH") {
        _chk_proc($proc);
    } else {
        dief("unexpected process: %s", $proc);
    }
    %option = validate(@_, \%proc_terminate_options) if @_;
    $option{kill} ||= $proc->{kill} || "TERM/1 INT/1 QUIT/1";
    $pid = $proc->{pid};
    # gentle kill
    foreach my $spec (split(/\s+/, $option{kill})) {
        if ($spec =~ /^([A-Z]+)\/(${nbre})$/) {
            ($sig, $grace) = ($1, $2);
        } else {
            dief("unexpected kill specification: %s", $spec);
        }
        unless (kill($sig, $pid)) {
            dief("cannot kill(%s, %d): %s", $sig, $pid, $!) unless $! == ESRCH;
        }
        $maxtime = Time::HiRes::time() + $grace;
        while (Time::HiRes::time() < $maxtime) {
            return unless _is_alive($proc, $option{_iosr}, $option{_iosw});
            Time::HiRes::sleep(0.01);
        }
        return unless _is_alive($proc, $option{_iosr}, $option{_iosw});
    }
    # hard kill
    $sig = "KILL";
    unless (kill($sig, $pid)) {
        dief("cannot kill(%s, %d): %s", $sig, $pid, $!) unless $! == ESRCH;
    }
}

#
# setup monitoring
#

sub _monitor_setup ($) {
    my($procs) = @_;
    my(%process, %map, $iosr, $iosw, $fh);

    # store the processes to monitor in a hash
    foreach my $proc (@{ $procs }) {
        _chk_proc($proc);
        $process{$proc->{pid}} = $proc;
    }
    # record the file handles to monitor
    $iosr = IO::Select->new();
    $iosw = IO::Select->new();
    foreach my $proc (values(%process)) {
        foreach my $what (qw(in out err)) {
            $fh = $proc->{"fh$what"};
            next unless $fh;
            if ($what eq "in") {
                $iosw->add($fh);
            } else {
                $iosr->add($fh);
            }
            $map{"$fh"} = [ $proc->{pid}, $what ];
        }
    }
    $iosr = undef unless $iosr->count();
    $iosw = undef unless $iosw->count();
    return(\%process, \%map, $iosr, $iosw);
}

#
# monitor I/O
#

sub _monitor_reading ($$$$$) {
    my($process, $map, $iosr, $bufsize, $timeout) = @_;
    my($buf, $done, $proc, $what);

    foreach my $fh ($iosr->can_read($timeout)) {
        $timeout = 0;
        $buf = "";
        $done = sysread($fh, $buf, $bufsize);
        dief("cannot sysread(): %s", $!) unless defined($done);
        $proc = $process->{$map->{"$fh"}[0]};
        $what = $map->{"$fh"}[1];
        $proc->{"cb$what"}($proc, $buf);
        unless ($done) {
            _close($proc, $fh, $what, $iosr);
        }
    }
    return($timeout);
}

sub _monitor_writing ($$$$$) {
    my($process, $map, $iosw, $bufsize, $timeout) = @_;
    my($buf, $done, $proc, $what);

    foreach my $fh ($iosw->can_write($timeout)) {
        $timeout = 0;
        $proc = $process->{$map->{"$fh"}[0]};
        $what = $map->{"$fh"}[1];
        $buf = $proc->{"buf$what"};
        if (length($buf)) {
            $done = syswrite($fh, $buf, length($buf));
            dief("cannot syswrite(): %s", $!) unless defined($done);
            substr($proc->{"buf$what"}, 0, $done, "");
        } else {
            _close($proc, $fh, $what, $iosw);
        }
    }
    return($timeout);
}

#
# monitor termination (death and timeout)
#

sub _monitor_termination ($$$$) {
    my($process, $iosr, $iosw, $timeout) = @_;
    my($now);

    # check if some processes finished
    foreach my $proc (grep(!defined($_->{status}), values(%{ $process }))) {
        next if _is_alive($proc, $iosr, $iosw);
        $timeout = 0;
    }
    # check if some processes timed out
    $now = Time::HiRes::time();
    foreach my $proc (grep($_->{maxtime}, values(%{ $process }))) {
        next unless $now > $proc->{maxtime};
        $timeout = 0;
        delete($proc->{maxtime});
        $proc->{timeout} = $now;
        proc_terminate($proc, _iosr => $iosr, _iosw => $iosw);
    }
    return($timeout);
}

#
# monitor one or more processes
#

my %proc_monitor_options = (
    timeout => { optional => 1, type => SCALAR, regex => $_NumberRegexp },
    bufsize => { optional => 1, type => SCALAR, regex => $_IntegerRegexp },
    deaths  => { optional => 1, type => SCALAR, regex => $_IntegerRegexp },
);

sub proc_monitor ($@) {
    my($procs, %option, $process, $map, $iosr, $iosw);
    my($maxtime, $timeout, $zombies);

    #
    # preparation
    #

    $procs = shift(@_);
    if (ref($procs) eq "HASH") {
        $procs = [ $procs ];
    } elsif (ref($procs) ne "ARRAY") {
        dief("unexpected processes: %s", $procs);
    }
    %option = validate(@_, \%proc_monitor_options) if @_;
    $option{bufsize} ||= 8192;
    ($process, $map, $iosr, $iosw) = _monitor_setup($procs);
    # count the number of processes which are already dead
    $zombies = grep(defined($_->{status}), values(%{ $process }));

    #
    # work
    #

    $maxtime = Time::HiRes::time() + $option{timeout}
        if defined($option{timeout});
    while ($iosr or $iosw
           or grep(!defined($_->{status}), values(%{ $process }))) {
        $timeout = 0.01;
        # read what can be read
        $timeout = _monitor_reading($process, $map, $iosr, $option{bufsize},
                                    $timeout) if $iosr;
        # write what can be written
        $timeout = _monitor_writing($process, $map, $iosw, $option{bufsize},
                                    $timeout) if $iosw;
        # check if some processes finished or timed out
        $timeout = _monitor_termination($process, $iosr, $iosw, $timeout);
        # or if we timed out
        last if $maxtime and Time::HiRes::time() > $maxtime;
        # or if enough processes died
        last if $option{deaths}
            and grep(defined($_->{status}), values(%{ $process }))
                >= $zombies + $option{deaths};
        # sleep a bit if needed (= if we have not worked before in the loop)
        Time::HiRes::sleep($timeout) if $timeout;
        # update the IO::Select objects
        $iosr = undef unless $iosr and $iosr->count();
        $iosw = undef unless $iosw and $iosw->count();
    }
}

#
# run the given command
#

sub proc_run (@) {
    my(@args) = @_;
    my($proc);

    # create the process
    $proc = proc_create(@args);
    # monitor it until it ends
    proc_monitor($proc);
    # return what is expected
    return(%{ $proc }) if wantarray();
    return($proc->{status});
}

#
# execute the given command, check its status and return its output
#

sub proc_output (@) {
    my(@command) = @_;
    my($output, $status);

    $output = "";
    $status = proc_run(command => \@command, stdout => \$output);
    dief("%s failed: %d", $command[0], $status) if $status;
    return($output);
}

#
# detach ourself and go in the background
#

my %proc_detach_options = (
    callback => { optional => 1, type => CODEREF },
);

sub proc_detach (@) {
    my(%option, $pid, $sid);

    %option = validate(@_, \%proc_detach_options) if @_;
    # change directory to a known place
    dir_change("/");
    # fork and let dad die
    $pid = fork();
    dief("cannot fork(): %s", $!) unless defined($pid);
    if ($pid) {
        # we are about to exit()
        $Transient = 1;
        $option{callback}->($pid) if $option{callback};
        exit(0);
    }
    # create a new session
    $sid = setsid();
    dief("cannot setsid(): %s", $!) if $sid == -1;
    # detach std* from anything but plain files (i.e. allow: cmd --detach > log)
    unless (-f STDIN) {
        open(STDIN, "<", "/dev/null")
            or dief("cannot re-open stdin: %s", $!);
    }
    unless (-f STDOUT) {
        open(STDOUT, ">", "/dev/null")
            or dief("cannot re-open stdout: %s", $!);
    }
    unless (-f STDERR) {
        open(STDERR, ">", "/dev/null")
            or dief("cannot re-open stderr: %s", $!);
    }
}

#
# return a string representation of the process status
#

sub proc_status ($) {
    my($status) = @_;
    my($signum, @list);

    return("ok") unless $status;
    $signum = $status & 127;
    push(@list, sprintf("code=%d", $status >> 8));
    push(@list, sprintf("signal=%s", $SigName[$signum] || $signum))
        if $signum;
    push(@list, "(core dumped)")
        if $status & 128;
    return(join(" ", @list));
}

#
# module initialization
#

@SigName[split(/\s+/, $Config{sig_num})] = split(/\s+/, $Config{sig_name});

#
# export control
#

sub import : method {
    my($pkg, %exported);

    $pkg = shift(@_);
    grep($exported{$_}++,
         map("proc_$_", qw(create detach monitor output run status terminate)));
    export_control(scalar(caller()), $pkg, \%exported, @_);
}

1;

__DATA__

=head1 NAME

No::Worries::Proc - process handling without worries

=head1 SYNOPSIS

  use No::Worries::Proc qw(proc_run proc_create proc_monitor proc_detach);

  # simple interface to execute a command
  $status = proc_run(command => [ "foo", "-x", 7 ]);
  printf("foo exited with %d\n", $status);

  # idem but with output redirection and more information
  %proc = proc_run(command => [ qw(uname -a) ], stdout => \$output);
  printf("process %d output is %s\n", $proc->{pid}, $output);

  # start two process and wait for them to finish
  $p1 = proc_create(
      command => \@cmd1,
      timeout => 5,           # to be killed if still running after 5s
      stderr  => "/dev/null", # discard stderr
  );
  $p2 = proc_create(
      command => \@cmd2,
      stdout  => \$output,    # get stdout+stderr in $output
      stderr  => "",          # merge stderr with stdout
  );
  proc_monitor([ $p1, $p2 ], timeout => 10);
  printf("%d finished\n", $p1->{pid}) if $p1->{stop};
  printf("%d finished\n", $p2->{pid}) if $p2->{stop};

  # detach ourself to run as a daemon
  proc_detach(callback => sub { print("started with pid $_[0]\n")});

=head1 DESCRIPTION

This module eases process handling by providing high level functions to start,
monitor and stop processes. All the functions die() on error.

It also provides the $No::Worries::Proc::Transient variable that indicates,
after a fork(), which process is transient and is about to exec() or exit().
This is useful for instance in an END block:

  END {
      # remove our pid file unless we are transient
      pf_unset($pidfile) unless $No::Worries::Proc::Transient;
  }

=head1 FUNCTIONS

This module provides the following functions (none of them being exported by
default):

=over

=item proc_output(COMMAND...)

execute the given command, capture its output (stdout only), check its exit
code (report an error if it is not zero) and return the captured output; this
is similar to Perl's qx() operator but bypassing the shell and always checking
the exit code

=item proc_create(OPTIONS)

create a new process that will execute the given command and return a hash
reference representing this process (see the L</"PROCESS STRUCTURE"> sections
for more information), to be given to proc_monitor() or proc_terminate()
afterwards; supported options:

=over

=item * C<command>: the command to execute, it must be an array reference

=item * C<cwd>: the current working directory of the new process

=item * C<timeout>: the maximum number of seconds that the process is allowed
to take to run (can be fractional); after this, it may be killed by
proc_monitor()

=item * C<kill>: how to "gently" kill the process, see below

=item * C<stdin>: what to do with stdin, see below

=item * C<stdout>: what to do with stdout, see below

=item * C<stderr>: what to do with stderr, see below

=back

=item proc_terminate(PROC[, OPTIONS])

terminate the given process (PROC can be either a process structure or simply
a process id) by sending signals and waiting for the process to finish;
supported options:

=over

=item * C<kill>: how to "gently" kill the process, see below

=back

=item proc_monitor(PROCS[, OPTIONS])

monitor the given process(es) (as created by proc_create()); PROCS can be
either a single process or a reference to a list of processes; supported
options:

=over

=item * C<timeout>: the maximum number of seconds that proc_monitor() should
take, can be fractional

=item * C<bufsize>: the buffer size to use for I/O operations (default: 8192)

=item * C<deaths>: the minimum number of process deaths that proc_monitor()
will wait for before returning

=back

=item proc_run(OPTIONS)

execute the given process (i.e. create and monitor it until termination) and
return its status (i.e. $?) in scalar context or the whole process structure
in list context; supported options: the ones of proc_create()

=item proc_detach([OPTIONS])

detach the current process so that it becomes a daemon running in the
background (this implies forking and re-opening std*); supported options:

=item proc_status(STATUS)

return a string representation of the given process status (i.e. $?)

=over

=item * C<callback>: code reference that will be executed by the parent
process just before exiting and will be given the child pid

=back

=back

=head1 PROCESS STRUCTURE

The process structure (hash) used in this module has the following fields:

=over

=item * C<command>: the command being executed, as an array reference

=item * C<pid>: the process id

=item * C<start>: the start time, in fractional seconds

=item * C<stop>: the stop time, in fractional seconds

=item * C<status>: the status (i.e. $?)

=item * C<timeout>: true if the process has been killed because of timeout

=back

=head1 FILE DESCRIPTOR REDIRECTION

When using the C<stdin> option of proc_create(), the value can be:

=over

=item * a string: input will be read from the given file name

=item * a scalar reference: input will be the scalar itself

=back

When using the C<stdout> and C<stderr> options of proc_create(), the value can
be:

=over

=item * a string: output will be written to the given file name

=item * a scalar reference: output will be stored in the scalar

=item * a code reference: each time new output is available, the code will be
called with two parameters: the process structure and the new output

=back

In addition, C<stderr> can also be given an empty string that means that
stderr should be merged with stdout.

=head1 KILL SPECIFICATION

Both proc_create() and proc_terminate() can be given a C<kill> option that
specifies how the process should be killed.

The specification is a string containing a space separated list of
I<signal>/I<grace> couples, meaning: send the given signal and wait a bit for
the process to finish.

If not specified, the default is C<TERM/1 INT/1 QUIT/1>, meaning:

=over

=item * send SIGTERM and wait up to 1 second for the process to finish

=item * if the process is still alive, send SIGINT and wait up to 1 second

=item * if the process is still alive, send SIGQUIT and wait up to 1 second

=item * if the process is still alive, send SIGKILL (implicit)

=back

=head1 GLOBAL VARIABLES

This module uses the following global variables (none of them being exported):

=over

=item $Transient

true if the process is about to exec() or exit(), there is usually no need to
perform any cleanup (e.g. in an END block) for this kind of process

=back

=head1 SEE ALSO

L<No::Worries>.

=head1 AUTHOR

Lionel Cons L<http://cern.ch/lionel.cons>

Copyright (C) CERN 2012-2019
