#!/usr/bin/perl
#
# Copyright (c) 2004-2007 - Consultas, PKG.fr
#
# This file is part of A2P.
#
# A2P 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 2 of the License, or
# (at your option) any later version.
#
# A2P 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.
#
# You should have received a copy of the GNU General Public License
# along with A2P; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# $Id: a2p-status.pl 3 2007-10-18 16:20:19Z g $
#
# Simple service to synchronize Job STATUS with a DB
#

BEGIN {
    use strict ;
    use warnings ;
    use POSIX 'setsid' ;
    use A2P::Globals ;

    our $VERSION = sprintf "%s", q$Rev: 1185 $ =~ /(\d[0-9.]+)\s+/ ;

    if (defined($ENV{JAVA_HOME}) and $ENV{JAVA_HOME}) {
        # Don't fork if launch by Eclipse, script launcher unset JAVA_HOME
        $Progname = "a2p-eclipse" ;
        $MUSTQUIT = 1 ;

    } else {
        # Daemonize early
        my $forkpid = 0 ;
        our $argument = $ARGV[0] || "" ;
        chdir '/'                      or die "Can't chdir to /: $!";
        open *STDIN , '<', '/dev/null' or die "Can't read from /dev/null: $!";
        open *STDOUT, '>', $StdoutFile or die "Can't write to $StdoutFile: $!";
           defined($forkpid = fork)       or die "Can't fork: $!";

        # Eventually save son PID in system file provided as argument and exit
        if ( $forkpid > 0 ) {
            if ( length($argument) > 0 ) {
                open *PID, '>', $argument
                    or die "Can't open PID file '$argument': $!";
                print PID $forkpid ;
                close(PID);
            }
            exit(0);
        }
    }
}
our $VERSION  ;
our $argument ;
our $Progname ;
our $MUSTQUIT ;

#-------------------------------------------------------------------------------
# Here we import any needed modules only after we are in background
#-------------------------------------------------------------------------------

use strict   ;
use warnings ;
use integer  ;
use POSIX qw( :signal_h :sys_wait_h setsid );
use Time::HiRes qw( usleep gettimeofday tv_interval );
use A2P::Init   ;
use A2P::Globals;
use A2P::Syslog ;
use A2P::Signal ;
use A2P::Thread ;
use A2P::JobStatus qw( sync_dbm_to_db get_status );
use A2P::Signal 'LogSigMessage' ;
use A2P::DB ;
use A2P::DB qw( CheckJobsStatusTables CheckJobsQueueTable get_serviceid_info );
use A2P::Infos ;
use A2P::AfpJob ;

&Debug(" Module load error: $! $? $@") if ( $! or $? or $@ );
&Debug(" =============================START==============================");

&setsid                     or die "Can't set a new session: $!";
open *STDERR, '>&', *STDOUT or die "Can't redirect STDERR to STDOUT: $!";

#-------------------------------------
# Here we are in a daemonized process
#-------------------------------------

&Debug("Current progname = $0, version $VERSION, started");
&Debug("Setting service name to $Progname");
# Update system name of this daemon to be pretty with ps command
$0 = $Progname ;

# Reset error code
$! = 0 ;

################################################################################
sub InitCheck {

    # Read conf
    &Init ;

    # Check needed folders
    my %Folders = (
        SERVICE_TMP  =>  $SERVICE_TMP,
        SHMDIR       =>  $SHMDIR
        );

    while ( my ($var, $Path) = each(%Folders) ) {
        unless ( -d $Path ) {
            &Info("Creating $Path folder as $var");
            my $Folder = "" ;
            foreach my $tmp_path ( split(m{[/]+}, $Path) ) {
                $Folder .= "/$tmp_path" ;
                mkdir $Folder , oct(775) unless ( -d $Folder );
                &Error("Can't create '$Path' folder as $var: $!"), last
                    unless ( -d $Folder );
            }
        }
    }

    # Initialize SERVICE_TMP folder
    if ( -d $SERVICE_TMP ) {
        unless ( system("touch $SERVICE_TMP/init_$Progname") == 0 ) {
            &Error("Can't initialize $SERVICE_TMP folder: $!");
            $MUSTQUIT = 1 ;
        }

    } else {
        &Error("Can't initialize unavailable $SERVICE_TMP folder: $!");
        $MUSTQUIT = 1 ;
    }

    # Test opening logfile in case of file logging
    if ( $LOGFILENAME and ( $LOGFILE_VS_SYSLOG or $DEBUG_IN_FILE )) {
        $! = 0 ;
        unless (open(*LOG, '>', $LOGFILENAME)) {
            $LOGFILE_VS_SYSLOG = 0 ;
            $DEBUG_IN_FILE     = 0 ;
            return &Error("Can't open '$LOGFILENAME': $!");
        }

        if ( $LOGFILE_PURGE ) {
            &UPSTAT('PURGE-LOG');
            truncate LOG , 0 ;
            $LOGFILE_PURGE = 0 ;
        }

        close(LOG);
    }

    # Reset Debug comportement
    &ResetDebug() ;

    &Info("Configuration reloaded") if ($do_init);
    $do_init = 0 ;
}

################################################################################

&InitCheck( $do_init = 0 );

################################################################################
### Local variables
################################################################################
my @MarkTimer = &gettimeofday() ;

# Status objects hash ref
my $STATUS = {} ;
my $factor = 0 ;
my $mode   = 0 ;
my $afpjob = undef ;
my $try_connect = 1 ;
my $connected = 0 ;
my $not_connected_timer = undef ;
my $delay = 60 ;
my %lockid_list = () ;
my $event_timer = undef ;

# Initialization from DB
my $services_infos ;
$services_infos = &get_serviceid_info()
    if (&a2p_db_connect() and &CheckJobsStatusTables());

&Info("$Progname service v" . A2P_RPM_VERSION . " started");
$STATS{'A2P-STATUS-VERSION'} = A2P_RPM_VERSION ;

# Main loop
while ( ! $MUSTQUIT ) {
    &TIMESTAT('MainLoop');

    &LogSigMessage();

    A2P::Thread::self_TEST() if $do_test ;

    @MarkTimer = &gettimeofday() , &Info("---MARK---")
        unless ( &tv_interval(\@MarkTimer) < 1800 );

    ##################################################################
    # 1. Get a DB Access and check db tables
    ##################################################################
    $connected = 1
        if (( ! $connected or $try_connect-- > 0 ) and &a2p_db_connect());

    if ( $connected > 0 and &CheckJobsStatusTables and &CheckJobsQueueTable ) {
        #############################
        # 2. Read all status objects
        #############################
        &sync_dbm_to_db ;

        ###########################################################
        # 3. Check for events in jobs_queue table for current mode
        ###########################################################
        my @list = keys(%lockid_list) ;
        if ( defined($services_infos) and @list ) {
            my $count = 5 ;
            my $lockid = shift @list ;
            while ( $count-- ) {
                my ( $serviceid, $name ) = @{$lockid_list{$lockid}} ;
                #&Debug("Getting events for $name service")
                #    if ($ADVANCED_DEBUGGING) ;

                if (defined($afpjob) and $afpjob) {
                    $afpjob = $afpjob->get_next_from_db($serviceid) ;
                    if (ref($afpjob) =~ /^A2P::AfpJob/) {
                        my $job = $afpjob->jobname ;
                        # Got an event
                        &UPSTAT('A2P-STATUS-GOT-EVENT-'.$lockid);
                        &Debug("Got event for $name service on $job")
                            if ($ADVANCED_DEBUGGING) ;

                        # Update the Status from our memory
                        my $status = &get_status($job);
                        $afpjob->set_status_object($status) if ($status);

                        # Update afpspool/service name
                        my @service = ( $lockid, $name ) ;
                        my $spool = $services_infos->get_afpspool_of(@service);
                        my @spools = $services_infos->get_spools_of(@service) ;
                        &Debug("Setting afpspool to $spool on $job")
                            if ($ADVANCED_DEBUGGING) ;
                        $afpjob->afpspool($spool);
                        $afpjob->otherspools(@spools);
                        $afpjob->service($name);

                        # Handle event getting if deleting (chained events case)
                        my $neg_is_deleted = $afpjob->handle_event
                            or &Warn("Bad event handling on " . $job .
                                " A2P::AfpJob object");

                        # Synchronize us in jobs_queue table unless deleted
                        &Warn("Bad event synchro on $job A2P::AfpJob")
                            unless ( $neg_is_deleted < 0
                            or $afpjob->sync_with_jobs_queue );

                    } else {
                        # Select next mode and/or next lockid to check
                        if ($mode) {
                            &UPSTAT('A2P-STATUS-ADV-EVENT-CHECKED-'.$lockid);
                            $mode = 0 ;
                            delete $lockid_list{$lockid} ;
                            last unless @list ; # All possible event checked
                            $lockid = shift @list ;

                        } else {
                            &UPSTAT('A2P-STATUS-USER-EVENT-CHECKED-'.$lockid);
                            $mode = 1 ;
                        }

                        $afpjob = new A2P::AfpJob($mode) ;
                        &Debug("Getting events for $name service with mode " .
                            $mode ) if ($ADVANCED_DEBUGGING) ;

                        $count ++ ;
                        next ;
                    }

                } else {
                    # Initialize with a fake afpjob
                    $afpjob = new A2P::AfpJob($mode) ;
                    $count ++ ;
                    next ;
                }
           }

        } elsif ( ref($services_infos) !~ /^A2P::Infos/i
        and ( ! defined($event_timer) or &tv_interval($event_timer) > 2 )) {
            # Reset event check timer
            $event_timer = [ &gettimeofday() ];

            $services_infos = &get_serviceid_info ;

        } elsif ( ! defined($event_timer) or &tv_interval($event_timer) > 2 ) {
            # Reset lockid list to check for events
            foreach my $lockid ($services_infos->get_lockid_list()) {
                my $sname = $services_infos->get_servicename_of($lockid) || '' ;
                next unless $sname ;
                $lockid_list{$lockid} = [
                    $services_infos->get_serviceid_of($lockid), $sname ];
            }

            # Control list
            @list = keys(%lockid_list) ;
            &Debug("Checking events for services: " .
                join( ', ', map { "$_ (@{$lockid_list{$_}})" } @list ))
                if ($ADVANCED_DEBUGGING);

            # Reset event check timer
            $event_timer = [ &gettimeofday() ];
        }

        # TODO Check SHMDIR for not finished work

        # Check if still connected to DB, this reconnect also to DB if a DB
        # error is detected or if a reconnection DB timer is set
        unless (&a2p_db_connected) {
            &Info("Just disconnected from DB", "Retrying DB connection");
            $connected = 0 ;
        }

    } elsif ( $connected > 0 ) {
        # Log any pending warning from DBI API
        &LogSigMessage();
        # Not a connection error but a checktable error just warn and retry
        # connection in 10 minutes
        &Warn("Disconnecting from DB for 10 minutes");
        &a2p_db_disconnect ;
        $not_connected_timer = [ &gettimeofday() ];
        $connected = $try_connect = -10 ;

    } elsif ( ! $connected ) {
        &Info("Disconnected from DB");
        $not_connected_timer = [ &gettimeofday() ];
        $connected-- ;
        # Anyway reset delay to one minute
        $delay = 60 ;

    } elsif ( $connected <= 10 ) {
        # Retry only after 10 min when more than 10 connections have failed
        $delay = 600 ;
    }

    # Check disconnection timing when not connected
    if ( $connected < 0 and &tv_interval($not_connected_timer) >= $delay ) {
        my $delta = abs($connected) ;
        $delta += 9 * ($connected - 10) if ( $delta >= 10 );
        &Info("Retrying DB connection after $delta minutes disconnection");
        $try_connect = 1 ;
        $not_connected_timer = [ &gettimeofday() ];
        $connected-- ;
    }

    #######################
    # Check flags
    #######################

    # true after QUIT/TERM signal has been received
    &TIMESTAT('MainLoop'), last if ( $do_quit );

    # true after HUP signal has been received
    &InitCheck() if ( $do_init );

    # Check to do last commits
    &check_commit() if ( $connected > 0 );

    # Adapt USLEEP factor to be 1 when JobStatus has been found
    # and grow up to 10 otherwise
    if (keys(%{$STATUS})) {
        $factor = 1 ;

    } elsif ( $factor < 10 ) {
        $factor ++ ;
    }

    # Avoid zombies sub-processes
    waitpid(-1,&WNOHANG);

    &TIMESTAT('MainLoop');
    usleep $factor * $USLEEP ;
    &UPSTAT('SLEEP-IN-MAINLOOP');
}

# We can safely activate debugging when quitting is requested
$NO_SYSLOG_DEBUG = 0 if $SERVICE_DEBUG ;

&a2p_db_disconnect();
&LogSigMessage ;

&Info("$Progname service is stopping");

# Wait until every child has quit
while ( my $pid = wait ) {
    last if ( $pid < 0 );
    &Debug("T$pid has terminated");
}

&Debug("PID file = '$argument'");

if (defined($argument)) {
    # Delete our PID in PID file
    open *RET , '>', $argument or die "Can't open PID file '$argument': $!";
    print RET "0" ;
    close(RET);
}

&LogSigMessage ;
&Info("$Progname service stopped");

exit(0);

END {
    # Output statistics when quitting from a diagnostic service
    A2P::Thread::self_TEST() if ($Progname =~ /^diagnostics/);

    &Debug("=============================QUIT===============================");
    &LogSigMessage ;
}
