#!/usr/bin/perl -w
use strict; # $Id: monotifierd 41 2017-11-30 11:26:30Z abalama $
use feature qw/ say /;

=head1 NAME

monotifierd - a notification agent (daemon)

=head1 VERSION

Version 1.00

=head1 SYNOPSIS

    monotifierd -k start

    monotifierd -k status

    monotifierd -k stop

=head1 OPTIONS

=over 8

=item B<-c CONFIG_FILE, --config=CONFIG_FILE>

Use CONFIG_FILE as configuration file

=item B<-D DIR, --datadir=DIR, --workdir=DIR>

Use DIR as DataDir directory

=item B<-d, --debug>

Print debug information on STDOUT

=item B<-h, --help>

Show short help information and quit

=item B<-H, --longhelp>

Show long help information and quit

=item B<-k SIGNAL>

Runs one of the following LSB commands: start, restart, stop, reload, status

=item B<-s SECS, --step=SECS>

Step time (interval between getting jobs) in secs

=item B<-v, --verbose>

Verbose option. Include Verbose debug data in the STDOUT and to error-log output

=item B<-V, --version>

Print the version number of the program and quit

=back

=head1 DESCRIPTION

Monotifier agent (daemon)

See C<README> file

=head1 HISTORY

=over 8

=item B<1.00 / Mon Oct 30 12:03:13 2017 GMT>

Init version

=back

See C<CHANGES> file

=head1 DEPENDENCIES

L<CTK>

=head1 TO DO

See C<TODO> file

=head1 BUGS

Coming soon

=head1 SEE ALSO

C<perl>, L<CTK>

=head1 DIAGNOSTICS

The usual warnings if it can't read or write the files involved.

=head1 AUTHOR

Sergey Lepenkov (Serz Minus) L<http://www.serzik.com> E<lt>abalama@cpan.orgE<gt>

=head1 COPYRIGHT

Copyright (C) 1998-2017 D&D Corporation. All Rights Reserved

=head1 LICENSE

This program is distributed under the GNU GPL v3.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

See C<LICENSE> file

=cut

use constant {
    PROJECT   => 'monotifierd',
    PREFIX    => 'monotifier',
    PIDFILE   => 'monotifierd.pid',
    LOGFILE   => 'monotifierd.log',
    CFGFILE   => 'monotifier.conf',
    SIGNAL    => 'status',
    KILL_TIMEOUT => 3, # For killing
    FORKS     => 3,
    MIN_FORKS => 1,
    MAX_FORKS => 255,

};

use sigtrap qw/ die INT TERM HUP QUIT /;

use vars qw/ %OPT /;

use Getopt::Long;
use Pod::Usage;
use File::Spec qw//;
use POSIX qw/ :sys_wait_h /;

# CTK Packages
use CTK::Util;
use CTK::ConfGenUtil;
use CTK::FilePid;

# App
use App::MonM::Notifier;
use App::MonM::Notifier::Log;
use App::MonM::Notifier::Daemon;

$| = 1;  # autoflush

# http://www.4answered.com/questions/view/c021b1/perl-fork-amp-exec
#$SIG{CHLD} = sub {
#    while ((my $child = waitpid(-1, WNOHANG)) > 0) {
#        $Kid_Status{$child} = $?;
#    }
#};

Getopt::Long::Configure ("bundling");
GetOptions(\%OPT,

    #
    # NoUsed keys map:
    #
    # a A b B   C     e E
    #   F g G     i I j J
    # k K l L m M n N o O
    # p P q Q r R   S t T
    # u U     w W x X y Y
    # z Z
    #

    # Help
    "help|usage|h|?",       # Show short help page
    "longhelp|H",           # Show long help page
    "version|ver|V",        # Show VERSION

    # Debugging modes
    "debug|d",              # Debug mode
    "verbose|v",            # Verbose mode. Default: Silent mode

    # App level
    "cfgfile|config|c=s",   # CfgFile
    "datadir|workdir|D=s",  # DataDir
    "forks|f=i",            # Forks
    "kill|k=s",             # Signals
    "step|interval|s=i",    # Step time (interval between getting jobs)

) || pod2usage(-exitval => 1, -verbose => 0, -output => \*STDERR);
pod2usage(-exitval => 0, -verbose => 1) if $OPT{help};
pod2usage(-exitval => 0, -verbose => 2) if $OPT{longhelp};
say(App::MonM::Notifier->VERSION) && exit(0) if $OPT{version};

sub debug { $OPT{debug} ? say(@_) : 1 }

# App Main Variables
my $ident = sprintf("%s_v%s", PREFIX, App::MonM::Notifier->VERSION);
my $pidfile = catfile(rundir(), PIDFILE);
my $logfile = catfile(syslogdir(), LOGFILE);
my $cfgroot = catdir(sysconfdir(), PREFIX);
my $cfgfile = defined($OPT{cfgfile}) ? $OPT{cfgfile} : catfile($cfgroot, CFGFILE);
my $cfgdir  = catdir($cfgroot, 'conf');
my $datadir = defined($OPT{datadir}) ? $OPT{datadir} : catdir(sharedstatedir(), PREFIX);
my $forkers = $OPT{forks} || FORKS;
   $forkers = MAX_FORKS if $forkers > MAX_FORKS;
   $forkers = MIN_FORKS if $forkers < MIN_FORKS;
preparedir([ rundir(), $datadir ]);

my $pidf = new CTK::FilePid({ file => $pidfile });
my $pidstat = $pidf->running;
#say sprintf "Daemon PID: %d", $pidstat;

my %LOCAL_SIG = (
        HUP     => undef,
        TERM    => undef,
        INT     => undef,
        KILL    => undef,
        QUIT    => undef,
    );

sub sigproxy {
    my $sname = shift;
    $LOCAL_SIG{$sname} = 1;
    #say sprintf("Proxying signal: %s", $sname);
}
sub lsbstop {
    if ($pidstat) {
        foreach my $sg (qw(TERM TERM INT KILL)) {
            debug("Sending $sg signal to pid $pidstat..." );
            kill $sg => $pidstat;
            for (1..KILL_TIMEOUT)
            {
                # abort early if the process is now stopped
                debug("Checking if pid $pidstat is still running...");
                last unless $pidf->running;
                sleep 1;
            }
            last unless $pidf->running;
        }
        if ( $pidf->running ) {
            warn("Failed to Stop");
            return 1;
        }
        my $tpid = $pidf->_get_pid_from_file;
        unlink($pidfile) if $tpid && (-e $pidfile) && $pidstat == $tpid;
        say("Stopped");
    } else {
        say("Not Running");
    }
    $pidstat = 0;
    return 0;
}
sub myfork { # See Proc::Daemon::Fork
    my $lpid;
    my $loop = 0;

    FORK: {
        $lpid = fork;
        return $lpid if defined($lpid);
        if ( $loop < 6 && ( $! == POSIX::EAGAIN() ||  $! == POSIX::ENOMEM() ) ) {
            $loop++; sleep 5;
            redo FORK;
        }
    }

    warn "Can't fork: $!";
    return undef;
}

# LSB kill's process (signal)
my $signal = $OPT{'kill'} || SIGNAL;
if ($signal eq 'start') {
    # NOOP
} elsif ($signal eq 'restart') {
    exit(1) if lsbstop;
} elsif ($signal eq 'stop') {
    exit(lsbstop)
} elsif ($signal eq 'reload') {
    if ($pidstat) {
        kill "HUP" => $pidstat;
        say("Reloaded");
    } else {
        say("Not Running");
        exit(1);
    }
    exit(0);
} elsif ($signal eq 'status') {
    if ($pidstat) {
        say("Running");
    } else {
        say("Not Running");
        exit(3)
    }
    exit(0)
} else {
    warn(sprintf("Incorrect signal name: %s", $signal));
    exit(1);
}

#
# "Start" process begins from here
#

if ($pidstat) {
    warn(sprintf("PID STATE (%s): ALREADY EXISTS (PID: %d)", $pidf->file(), $pidstat));
    exit(0);
}

START: debug "-"x16, sprintf(" START  DAEMON %s [%d] ", PROJECT, $$), scalar(localtime(time()))," ","-"x16;
my $logger;
if ($OPT{debug}) {
    $logger = new App::MonM::Notifier::Log($ident);
    $logger->log_debug("START DAEMON %s [%s]", PROJECT, $$);
}
#########################
### START
#########################

# Create a daemons
my $save_pid = $$;
#say "> $$";

my $pid = myfork();
if ($pid && $OPT{debug}) {
    debug sprintf("Master process (PID=%d) is running...", $pid);
    $logger->log_info("Master process (PID=%d) is running...", $pid);
}
if ( defined $pid && $pid == 0 ) { # The first child runs here.
    $pidf->pid(isostype('Windows') ? $save_pid : $$);
    $pidf->write;

    # Detach the child from the terminal (no controlling tty), make it the
    # session-leader and the process-group-leader of a new process group.
    unless (isostype('Windows')) {
        die "Cannot detach from controlling terminal" if POSIX::setsid() < 0;
    }

    # Catching the signals
    $SIG{$_} = \&sigproxy for keys %LOCAL_SIG;

    # Second fork. See Proc::Daemon
    my (@pids, %pidh);
    for (my $j = 1; $j <= $forkers; $j++) {
        my $cpid = myfork();
        if ( defined $cpid && $cpid == 0 ) { # Here the second child is running.
            # Close all file handles and descriptors the user does not want to preserve.
            my $devnull = File::Spec->devnull;
            unless ($OPT{debug} && isostype('Windows')) {
                open( STDIN, "<", $devnull ) or die "Failed to open STDIN to $devnull: $!";;
                open( STDOUT, ">>", $devnull ) or die "Failed to open STDOUT to $devnull: $!";
                open( STDERR, ">>", $devnull ) or die "Failed to open STDERR to $devnull: $!";
            }

            # CODE
            exit worker(
                j       => $j,      # Worker number (as indent)
                ident   => $ident,    # Indent for syslog subsystem
                debug   => $OPT{debug},
                verbose => $OPT{verbose},
                cfgfile => $cfgfile,
                logfile => $logfile,
                datadir => $datadir,
                step    => $OPT{step},
            );

            # Return the childs own PID (= 0)
            #POSIX::_exit(0);
            #exit (0);
        }

        # First child (= second parent) runs here.
        if ($cpid) {
            $pidh{$cpid} = 0;
            #say ">>> $j -> $cpid";
        }
    }

    #waitpid $_, 0 for @pids;
    while (grep {$_ == 0} values %pidh) {
        @pids = grep {$pidh{$_} == 0} keys %pidh;
        foreach my $k (grep {$LOCAL_SIG{$_}} keys %LOCAL_SIG) {
            foreach my $p (@pids) {
                #say "========> Send $k to $p";
                kill $k => $p;
            }
            $LOCAL_SIG{$k} = 0;
        }
        foreach my $p (@pids) {
            $pidh{$p} = 1 if waitpid $p, WNOHANG;
        }
    } continue {
        sleep 1;
        #say "Continue...";
    }
    #print("Terminated!\n");

    $logger->log_info("Master process (PID=%d) successfully terminated.", $$) if $OPT{debug};
    $pidf->remove;
    POSIX::_exit(0);
}
#waitpid($pid, 0);

#########################
### FINISH
#########################
FINISH: debug "-"x16, sprintf(" FINISH DAEMON %s [%d] ", PROJECT, $$), scalar(localtime(time()))," ","-"x16;
$logger->log_debug("FINISH DAEMON %s [%d]", PROJECT, $$) if $OPT{debug};
exit(0);

1;
__END__
