#!/usr/bin/perl

package HaCi::HaCi;

use warnings;
use strict;

local $|	= 1;

use FindBin;
use lib "$FindBin::Bin/../modules";
chdir "$FindBin::Bin/..";
my $workDir = qx/pwd/;
chomp($workDir);
$ENV{workdir} = $workDir;

use Log::LogLite;
use Data::Dumper;
use POSIX;
use Getopt::Std;
use HaCi::Conf;
use HaCi::Mathematics qw/dec2net getBroadcastFromNet dec2ip net2dec getV6BroadcastNet getCidrFromDec getCidrFromNetv6Dec/;
use HaCi::Utils qw/
	getConfig getPlugins getTable getNetworksForPlugin getPluginConfValues updatePluginLastRun getMaintInfosFromNet
	initTables finalizeTables netID2Stuff getNextDBNetwork fillHoles getPluginLastRun
/;

$Getopt::Std::STANDARD_HELP_VERSION	= 1;
$main::VERSION											= 0.1;
my $log;
eval {
	$log	= new Log::LogLite($ENV{workdir} . '/logs/HaCid.log',3); $log->template('<date> <message>' . "\n");
};
die "Error while opening logfile. Has the user '$>' write access to the logfile '$ENV{workdir}/logs/HaCid.log'?: $@" if $@;

local $SIG{'__WARN__'}							= sub {&log($_[0], 2)};
local $SIG{'__DIE__'}								= sub {&diel(@_)};

my $progFile			= $workDir . '/bin/HaCid.pl';
my @argv					= @ARGV;
my $running				= 1;
my $plugins				= {};
my $pluginData		= {};
my $nrOfChilds		= 0;
my $maxChilds			= 5;
my $pluginReload	= 300;
my $lastReload		= 0;
my $maxMemCon			= 100000;

our $opts	= {};
getopts('cdr', $opts);

our $conf; *conf		= \$HaCi::Conf::conf;

&HaCi::Conf::init($workDir);
warn "WORKDIR: $workDir\n";
&getConfig();

$maxMemCon	= $conf->{user}->{misc}->{maxmemconsumption} if exists $conf->{user}->{misc}->{maxmemconsumption} && $conf->{user}->{misc}->{maxmemconsumption};

if (my $nrOfProz = &checkProcessList()) {
	if ($opts->{r} && $nrOfProz == 1) {
		&log("Restarting successfull!\n", 3, 1);
	} else {
		&log("$nrOfProz Processes allready running!\n", 1, 1);
		exit 0;
	}
}

my $daemon	= (exists $opts->{c} && $opts->{c}) ? 0 : 1;
my $debug		= (exists $opts->{d} && $opts->{d}) ? 1 : 0;
push @argv, '-r' unless $opts->{r};

&daemonize() if $daemon;
local $SIG{ALRM}	= sub {&log("checking Processes", 3); $nrOfChilds = &checkProcessList();alarm 60}; alarm 60;

&init();
&wrPID();

if ($daemon) {
	while ($running) {
		&checkMemConsumption();
		&loadPlugins() if (time - $lastReload) >= $pluginReload;
		&cleanupPluginData();
		&main();
		sleep 1;
	}
} else {
	&main();
}

&log("Finished!", 2);

&wrPID(1);
exit 0;

#--------------------------------------------
END {
	&finalizeTables();
}

sub init {
	&initTables();
}

sub main {
	&log("starting Main...", 3);

	foreach (keys %{$plugins}) {
		my $pluginID	= $_;
		my $plugin		= $plugins->{$pluginID}->{NAME};

		if ($pluginData->{running}->{$pluginID}) {
			&log("Plugin '$plugin' is allready running!", 3);
			next;
		}
		
		$pluginData->{lastRuns}->{$pluginID}	= &getPluginLastRun($pluginID);
		if (my $child = &daemonize(1)) {
			$pluginData->{running}->{$pluginID}		= 1;
			$pluginData->{PID}->{$child}					= $pluginID;
			next;
		} else {
			&log("Starting Plugin '$plugin'...", 3);
			&processPlugin($pluginID);
			&log("Plugin '$plugin' sucessfully processed", 3);
			exit 0;
		}
	}
}

sub processPlugin {
	my $pluginID	= shift;
	my $plugin		= $plugins->{$pluginID}->{NAME};
	&initTables();

	my $pluginGlobConfig	= &getPluginConfValues($pluginID, -1);
	my $interval					= $pluginGlobConfig->{def_glob_recurrent_interval};
	$interval							= 3600 unless defined $interval;

	unless (defined $interval && $interval > 0) {
		$interval	= 'undef' unless defined $interval;
		&log(" Interval ($interval) for Plugin '$plugin' is not defined or too small!", 1);
		$pluginData->{running}->{$pluginID}	= 0;
		return ;
	}

	my $timeDiff	= time - ($pluginData->{lastRuns}->{$pluginID} || 0);
	if ($timeDiff < $interval) {
		&log(" Interval for Plugin '$plugin' is not reached ($timeDiff < $interval)", 3);
		$pluginData->{running}->{$pluginID}	= 0;
		return;
	}

	&log("  loading Plugin $plugin ($pluginID)...", 3);
	my $package				= $plugins->{$pluginID}->{PACKAGE};
	(my $packageFile	= $package . '.pm') =~ s/::/\//g;
	eval {
		require $packageFile;
	};
	if ($@) {
		&log("  Cannot load Plugin '$plugin': $@", 1);
		$pluginData->{running}->{$pluginID}	= 0;
		return;
	}

	&log("  getting Networks for Plugin $plugin ($pluginID)...", 3);
	my @networkst	= &getNetworksForPlugin($pluginID);
	my $config		= {};
	my $networks	= [];
	foreach (@networkst) {
		my $netID					= $_;
		$config->{$netID}	= &getPluginConfValues($pluginID, $netID);
		my $netInfos			= &getMaintInfosFromNet($netID);

		next unless exists $netInfos->{network};
		my $onlyHosts			= (exists $config->{$netID}->{def_recurrent_onlyHosts} && $config->{$netID}->{def_recurrent_onlyHosts}) ? 1 : 0;

		if (!$onlyHosts || $onlyHosts && 
			(($netInfos->{ipv6} && &getCidrFromNetv6Dec($netInfos->{network}) == 128) || (!$netInfos->{ipv6} && &getCidrFromDec($netInfos->{network}) == 32))
		) {
			$netInfos->{origin}	= $netID;
			push @{$networks}, $netInfos;
		}

		if ($config->{$netID}->{def_recurrent_withSubnets}) {
			my @childs	= &getSubnets(0, $netID, $config->{$netID}->{def_recurrent_maxdepth} || 1, $onlyHosts, $netID);
			foreach (@childs) {
				push @{$networks}, $_;
			}
		}
	}
	&log("   " . ($#{$networks} + 1) . ' Networks found for Plugin ' . $plugin . '!', 2);
	$config->{-1}	= $pluginGlobConfig;

	&log("  initiating Plugin $plugin ($pluginID)...", 3);
	my $plug;
	eval {
		$plug	= $package->new($pluginID, $conf->{var}->{TABLES}->{pluginValue});
	};
	if ($@) {
		&log("  Error while initiating Plugin '$plugin': $@", 1);
		$pluginData->{running}->{$pluginID}	= 0;
		return;
	}
	
	&log("  running Plugin $plugin ($pluginID)...", 2);
	$pluginData->{lastRuns}->{$pluginID}	= time;
	&updatePluginLastRun(
		$pluginID, 
		$pluginData->{lastRuns}->{$pluginID}, 
		0,
		''
	);
	eval {
		$plug->run_recurrent($networks, $config);
	};
	if ($@) {
		&log("  Error while running Plugin '$plugin': $@", 1);
	}
	my $error			= 0;
	my $errorStr	= '';
	if ($plug->can('ERROR')) {
		$error	= $plug->ERROR();
	} else {
		&log("  Plugin has no ERROR Method!", 2);
	}
	if ($plug->can('ERRORSTR')) {
		$errorStr	= $plug->ERRORSTR();
	} else {
		&log("  Plugin has no ERRORSTR Method!", 2);
	}
	my $runtime	= time - $pluginData->{lastRuns}->{$pluginID};
	$pluginData->{running}->{$pluginID}	= 0;
	&log("  Plugin $plugin finished ($runtime Seconds Runtime)", 2);
	&updatePluginLastRun(
		$pluginID, 
		-1, 
		$runtime,
		(($error) ? $errorStr : '')
	);
	undef $plug;
}

sub getSubnets {
	my $level					= shift;
	my $netID					= shift;
	my $maxDepth			= shift;
	my $onlyHosts			= shift;
	my $origin				= shift;
	my $subnets				= [];
	$level++;

	my ($rootID, $networkDec, $ipv6)  = &netID2Stuff($netID);
	my $broadcast											= ($ipv6) ? &getV6BroadcastNet($networkDec, 128) : &net2dec(&dec2ip(&getBroadcastFromNet($networkDec)) . '/32');

	my $networkT	= undef;
	while ($networkT = &getNextDBNetwork($rootID, $ipv6, $networkDec)) {
		last if $networkT->{network} == $networkDec || $networkT->{network} > $broadcast;
		if (!$onlyHosts || $onlyHosts && 
			(($ipv6 && &getCidrFromNetv6Dec($networkT->{network}) == 128) || (!$ipv6 && &getCidrFromDec($networkT->{network}) == 32))
		) {
			$networkT->{origin}	= $origin;
			push @{$subnets}, $networkT;
		}

		push @{$subnets}, &getSubnets($level, $networkT->{ID}, $maxDepth, $onlyHosts, $origin) if $level < $maxDepth;
		$networkDec = ($ipv6) ? &getV6BroadcastNet($networkT->{network}, 128) : &net2dec(&dec2ip(&getBroadcastFromNet($networkT->{network})) . '/32');
	}

	return @{$subnets};
}

sub loadPlugins {
	&initTables();
	$plugins		= &getPlugins();
	$lastReload	= time;
	foreach (keys %{$plugins}) {
		my $pluginID	= $_;
		my $plugin		= $plugins->{$pluginID}->{NAME};
		unless ($plugins->{$pluginID}->{RECURRENT} && $plugins->{$pluginID}->{ACTIVE}) {
			delete $plugins->{$pluginID};
			next;
		}
	}
	&log((scalar keys %{$plugins}) . " recurrent Plugin(s) found\n", 2);
}

sub daemonize {
	my $return	= shift || 0;

	&log("Maximal number of Childs reached ($maxChilds)", 3) if $nrOfChilds >= $maxChilds;
	while ($nrOfChilds >= $maxChilds) {
		sleep 1;
	}

	$SIG{'INT'}   = $SIG{'HUP'} = $SIG{'TERM'}  = \&exitHandler;
	$SIG{'CHLD'}  = \&reaper;
	
	my ($sessID, $pid);
	if ($pid = &fork) {
		$nrOfChilds++;
		&log("New Child forked ($pid)! ($nrOfChilds/$maxChilds)", 3);
		if ($return) {
			return $pid;
		} else {
			print "Daemon started...\n";
			exit 0; 
		}
	}

	unless ($sessID = POSIX::setsid()) {
		&log("Cannot detach from controlling terminal", 1);
		die "Cannot detach from controlling terminal";
	}

	chdir "/";
	umask 0;
	foreach (0 .. POSIX::sysconf(&POSIX::_SC_OPEN_MAX)) {
		POSIX::close($_);
	}

	open(STDIN,  "+>/dev/null");
	open(STDOUT, "+>&STDIN");
#	open(STDERR, "+>&STDIN");
	$log	= new Log::LogLite($ENV{workdir} . '/logs/HaCid.log',3); $log->template('<date> <message>' . "\n");

	return 0;
}

sub fork {
	my $pid;
	FORK: {
		if (defined($pid = fork)) {
		} elsif ($! =~ /No more process/) {
			&log("Cannot fork: $!", 1);
			sleep 5;
			redo FORK;
		} else {
			die "Can't fork: $!";
		}
	}

	return $pid;
}

sub diel {
	my @msg	= @_;
	&log(join(', ', @msg), 1);
	die $msg[0];
}

sub log {
	my $msg		= shift;
	my $level	= shift || 1;
	my $local	= shift || 0;
	$msg			= (($level == 1) ? 'ERROR' : ($level == 2) ? 'INFO' : 'DEBUG') . ': ' . $msg;
	$msg			= $$ . ': ' . $msg;

	if (defined $log) {
		chomp($msg);
		$log->write($msg, $level) if $level < 3 || $debug;
		print STDERR "$msg\n" if $local;
	} else {
		print STDERR "$msg\n";
	}
}

sub reaper {
	my $child;
	while (($child  = waitpid(-1, WNOHANG)) > 0) {
		sleep 1;
		$pluginData->{running}->{$pluginData->{PID}->{$child}}	= 0 if exists $pluginData->{PID}->{$child};
		$nrOfChilds--;
		&log(" Child '$child' exited with: $? ($nrOfChilds/$maxChilds)", 3);
	}
	$SIG{CHLD}			= \&reaper;
}

sub exitHandler {
	my $sig = shift;

	&log("Catching SIG$sig", 2);

	if ($sig eq 'HUP') {
		&loadPlugins();
	}
	elsif ($sig eq 'INT' || $sig eq 'TERM') {
		$running  = 0;
	}
}

sub getPID {
	unless (open PID, $conf->{static}->{path}->{hacidpid}) {
		&log("Cannot open Pidfile '$conf->{static}->{path}->{hacidpid}' for reading: $!", 2);
		return;
	}
	my $pid	= <PID>;
	close PID;
	
	return $pid;
}

sub checkProcessList {
	my $pid				= &getPID();
	my $nrOfProz  = ($pid) ? qx(ps -p $pid --no-headers | wc -l) : 0;
	chomp($nrOfProz);
	return $nrOfProz;
}

sub cleanupPluginData {
	my $plugin	= {};

	foreach (keys %{$pluginData->{PID}}) {
		my $pid				= $_;
		my $pluginID	= $pluginData->{PID}->{$pid};
		my $nrOfProcs	= qx/ps -p $pid h | wc -l/;
		if (int($nrOfProcs)) {
			$plugin->{$pluginID}	= 1;
		} else {
			$plugin->{$pluginID}	= 0 unless exists $plugin->{$pluginID};
			delete $pluginData->{PID}->{$pid};
		}
	}

	foreach (keys %{$plugin}) {
		my $pluginID	= $_;

		unless ($plugin->{$pluginID}) {
			&log(" Clear Plugin '$pluginID' from pluginData, because no Child is working on this Plugin", 3);
			$pluginData->{running}->{$pluginID}	= 0;
		}
	}
}

sub main::HELP_MESSAGE {
	print "
NAME
	HaCid - HaCi Daemon - runns HaCi Plugins periodically

SYNOPSIS
	HaCid [-c] [-d]

OPTIONS
	-c	Cronjob mode. Don't run as daemon. Program exits after one full run
	-d	Debug mode. Logging all debug messages
";
}

sub wrPID {
	my $del	= shift || 0;
	
	if ($del) {
		unless (unlink $conf->{static}->{path}->{hacidpid}) {
			&log("Cannot delete Pidfile '$conf->{static}->{path}->{hacidpid}': $!", 2);
			return;
		}
	} else {
		unless (open PID, '>' . $conf->{static}->{path}->{hacidpid}) {
			&log("Cannot write Pidfile '$conf->{static}->{path}->{hacidpid}': $!", 2);
			return;
		}
		print PID $$;
		close PID;
	}
}

sub checkMemConsumption {
	my $size	= 0;
	
	open PIPE, "/bin/ps wwaxo 'pid,rss' |";
	while(<PIPE>) {
		next unless /^\s*$$\s/;
		s/^\s+//g;
		chomp;
		$size	= (split(' ', $_))[1];
	}
	close PIPE;

  &log("Checking size: ${size}kB (max ${maxMemCon}kB)", 3);
  if($size > $maxMemCon) {
  	&log("Max Memory Size reached: ${size}kB/${maxMemCon}kB, restarting ($progFile @ARGV)...", 2);
    exec $progFile, @argv or die "Restart failed: $!";
  }
}

# vim:ts=2:sts=2:sw=2
