#!/usr/bin/perl -w

#
# Fsdb::Support::Freds.pm
# Copyright (C) 2013-2019 by John Heidemann <johnh@ficus.cs.ucla.edu>
#
# This program is distributed under terms of the GNU general
# public license, version 2.  See the file COPYING
# in $dblib for details.
#

package Fsdb::Support::Freds;


=head1 NAME

Fsdb::Support::Freds - an abstraction over fork and/or ithreads

=head1 SYNOPSIS

    use Fsdb::Support::Freds;
    my $fred = new Fsdb::Support::Freds('new thread to do foo');
    # or
    my $fred = new Fsdb::Support::Freds('demo_fred', 
	sub { child_stuff(); exit 0; },
	sub { say "child is done\n"; } );
    $fred->join();
    # or 
    $fred->detach();

This package provides an abstraction over fork that is something
like Perl's ithreads.  Our goal is to abstract process creation
and collection, but none of the shared data like ithreads.

(Why "Freds"?  Because it's fork-based thread-like things,
and "Tasks" seems too generic.)

=cut
#'

@ISA = ();
($VERSION) = 1.0;

use strict;

use POSIX ":sys_wait_h";

# keep track of all of them, for reaping
our %freds;

=head2 new

    $fsdb = new Fsdb::Support::Freds($description, $child_sub, $ending_sub);

For a process, labeling it with optional $DESCRIPTION
then running optional $CHILD_SUB in the subprocess,
then running optional $ENDING_SUB in the parent process when it exits.

$ENDING_SUB is passed three arguments, the fred,
the shell exit code (typically 0 for success or non-zero for failure),
and the wait return code (the shell exit code shifted, plus signal number).

It is the job of the $CHILD_SUB to exit if it wants.
Otherwise this function returns to the caller.

=cut

sub new(;$$$) {
    my($class, $desc, $child_sub, $ending_sub) = @_;
    my $self = bless {
	_description => $desc // "no description",
	_error => undef,
	_exit_code => undef,
	_wait_code => undef,
	_parent => $$,
	_active => 1,
	_ending_sub =>  $ending_sub,
    }, $class;
    my $pid = fork();
    if (!defined($pid)) {
	$self->{_error} = 'cannot fork';
	$self->{_active} = undef;
	return $self;
    };
    $self->{_pid} = $pid;
    $freds{$pid} = $self;
    if ($pid == 0 && defined($child_sub)) {
	&$child_sub();
    }
    return $self;
}

=head2 is_child

    $fred->is_child();

Are we the child?  Returns undef if parent.

=cut

sub is_child($) {
    my $self = shift @_;
    return $self->{_pid} == 0;
}

=head2 info

    $info = $fred->info();

Return a string description of the Fred.

=cut

sub info($) {
    my $self = shift @_;
    return $self->{_description} . "/" . $self->{_pid};
}

=head2 error

    $fred->error();

Non-zero if in error state.

=cut

sub error($) {
    my $self = shift @_;
    return $self->{_error};
}

=head2 exit_code

    $fred->exit_code($full);

Exit code of a termintated fred.
With $FULL, turn the full version (including errors).
Typically "0" means success.

=cut

sub exit_code($) {
    my($self, $full) = @_;
    return $full ? $self->{_wait_code} : $self->{_exit_code};
}

=head2 _post_join

    $fred->_post_join();

Internal cleanup after $FRED is terminated.

=cut

sub _post_join($$) {
    my($self, $wait_code) = @_;
    $wait_code //= 0;
    my $exit_code = ($wait_code >> 8);

    # assert(pid has terminated)

    return -1 if ($self->{_parent} != $$);

    $self->{_active} = 0;
    $self->{_exit_code} = $exit_code;
    $self->{_wait_code} = $wait_code;

    delete $freds{$self->{_pid}};

    if ($self->{_ending_sub}) {
	&{$self->{_ending_sub}}($self, $exit_code, $wait_code);
    };

    return $exit_code;
}

=head2 join

    $fred->join();

Join a fred (wait for the process to finish).
Returns -1 on error
(Including if not in the parent.)


=cut

sub join() {
    my($self) = @_;
    return -1 if ($self->{_parent} != $$);
    return $self->{_exit_code} if (!$self->{_active});

    waitpid $self->{_pid}, 0;
    return $self->_post_join($?);
}

=head2 join_any

    my $fred = Fsdb::Support::Freds::join_any($BLOCK);

Join on some pending fred,
without blocking (default) or blocking (if $BLOCK) is set.
Returns -1 on error.
Returns 0 if something is running but not finished.

Returns the $FRED that ends.

=cut

sub join_any(;$) {
    my($block) = @_;

    my $pid = waitpid(-1, ($block ? 0 : WNOHANG));
    return $pid if ($pid == -1 || $pid == 0);

    # find it
    my $fred = $freds{$pid};
    return 0 if (!defined($fred));   # not ours

    $fred->_post_join();
    return $fred;
}

=head2 join_all

    my $fred = Fsdb::Support::Freds::join_all();

Reap all pending threads.

=cut

sub join_all() {
    for(;;) {
	my $fred = Fsdb::Support::Freds::join_any();
	last if (ref($fred) eq '');
    };
}

=head2 END

Detect any non-repeaed processes.

=cut

END {
    my $fred;
    my $old_exit = $?;
    Fsdb::Support::Freds::join_all();
    foreach $fred (values (%freds)) {
	next if (!$fred->{_active});
	next if ($fred->{_parent} != $$);  # not my problem
	warn "Fsdb::Support::Freds: ending, but running process: " . $fred->{_description} . "\n";
    };
    $? = $old_exit;
}

1;
