#!/usr/bin/perl

use Config::General;
use Data::Dumper;
use Getopt::Std;
use IO::Handle;
use POSIX qw(setsid setuid setgid);
use POSIX ":sys_wait_h";


#################
## Global vars ## 
#################

my $confile;
my %conf;
my %running;
my $reload_conf=0;
my $debug=0;
#For each process, uid and gid to set after fork
my %gid;
my %uid;

#For sheduler
#Number of launch, for each daemon
my %launches;
my %ratio_launch_prio;
my %last_schedule;
my $priority_ref = "";

###############
## Functions ##
###############

sub sigHUP_handler{
   &debug_msg("got SIGHUP\n");
   $reload_conf=1;
}
   

#Read Configuration and init global vars
sub read_conf {
   my $file = shift;
   $config = new Config::General( -file =>"$file", 
	 			  -AllowMultiOptions =>"yes",
	 			  -LowerCaseNames =>"yes",);
   %conf = $config->getall;
   &debug_msg(Dumper(\%conf));
}


sub debug_msg(#){
   my $msg=shift;
   if ($debug == 1){
      print STDERR $msg;
   }
}
   

sub init {
   #getopts('hf:', \%opts) or die "Illegal program option. ($0 -h for list)\n";
   #&debug_msg(Dumper(\%opts));

   if ($confile eq ""){
      my $path = `pwd`;
      chomp($path);
      if ($path){
	 $confile="$path/muxdaemon.conf";
      }else{
	 print STDERR "Error : unable to get working directory, $!\n";
	 exit(1);
      }
   }

   if ( not getopts('dhf:', \%opts) or $opts{'h'}) {
      print STDERR "\nThis is mail processing Daemon :\n",
                   "-h          - this help message\n", 
		   "-f <file>   - use <file> as config file\n",
		   "-d          - debug mode\n";
      exit(1);
   }

   if ($opts{'f'}){
      if (-r "$opts{'f'}") {
	 print STDERR "Using $opts{'f'} as config file\n";
	 &read_conf("$opts{'f'}");
      } else {
	 print STDERR "$opts{'f'} : not a valid config file\n";
	 exit(1);
      }
   } else {
      print STDERR "Using $confile as config file\n";
      if (-r "$confile") {
	 &read_conf("$confile");
      } else {
	 print STDERR "No valid configuration file found, aborting\n";
	 exit(1);
      }
   }

   if ($opts{'d'}){
      $debug = 1;
   }else{
      $debug = 0;
   }

}

sub init_launches(){
   my $min_priority=100;
   foreach my $arg (keys %{$conf{'client'}}){
      $launches{"$arg"}=0;
      $ratio_launch_prio{"$arg"}=0;
   }
}


sub check_conf(){
   print STDERR "Checking configuration file for wrong user/groups, schedules, ....\n";
   foreach my $arg (keys %{$conf{'client'}}){
      #La conf du client courant
      my $hash=${$conf{'client'}}{"$arg"};
      if (${$hash}{'run_as_user'}){
	 my $id = getpwnam("${$hash}{'run_as_user'}");
	 if ($id){
	    $uid{"$arg"} = $id;
	 }else{
	    print STDERR "  Error, user ${$hash}{'run_as_user'} does not exist, please check...\n";
	    exit(1);
	 }
      }
      if (${$hash}{'run_as_group'}){
	 my $id = getgrnam("${$hash}{'run_as_group'}");
	 if ($id){
	    $gid{"$arg"} = $id;
	 }else{
	    print STDERR "  Error, group ${$hash}{'run_as_group'} does not exist, please check...\n";
	    exit(1);
	 }
      }
      if (${$hash}{'allow_many'} eq "yes" and ${$hash}{'min_schedule'}){
	 print STDERR "  Error, allow_many and min_schedule defined for $arg\n";
	 exit(1);
      }
   }
}

   
sub daemonize {
   print STDERR "Forking ... \n";
   chdir '/' or die "Can't chdir to /: $!";
   open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
   open STDOUT, '>>/dev/null' or die "Can't write to /dev/null: $!";
   if ($conf{'error_log'}){
      open STDERR, ">>$conf{'error_log'}" or die "Can't write to $conf{'error_log'}: $!";
   }else{
      open STDERR, '>>/dev/null' or die "Can't write to /dev/null: $!";
   }
   defined(my $pid = fork) or die "Can't fork: $!";
   exit if $pid;
   setsid or die "Can't start a new session: $!";
   umask 0;
   &debug_msg("Daemon started\n");
}

sub get_load {
   my $load = `$conf{"ldcmd"}`;
   chop($load);
   #&debug_msg("$load\n");
   return $load;
}

#Return the list of client for which max_load is => ld
sub get_possible_client(#){
   my $ld=shift;
   my @list=();
   #&debug_msg("get_possible_client, load = $ld\n");
   foreach my $arg (keys %{$conf{'client'}}){
      #&debug_msg("get_possible_client : Trying client $arg :");
      my $hash = ${$conf{'client'}}{$arg};
      if (${$hash}{'max_load'} >= $ld){
	 #&debug_msg("max_load = ".${$hash}{'max_load'}." => in\n");
	 push(@list,$arg);
      }else{
	 #&debug_msg("max_load = ".${$hash}{'max_load'}." => out\n");
      }
   }
   return \@list;
}
   

#Return the client that should be executed according to priorities
#given in conf file
# Refaire avec calcul du min de launches/prio !
sub get_next(#){
   my $ref=shift;
   my @possible_client = @{$ref};
   #List des proc executes assez souvent
   my $client="";
   my $min_ratio=-1;
   
   foreach my $arg (@possible_client){
      if ($ratio_launch_prio{"$arg"} < $min_ratio or $min_ratio == -1){
	 $min_ratio=$ratio_launch_prio{"$arg"};
	 $client=$arg;
      }
   }

   if ($client){
      #Update ratio for him
      $launches{"$client"}++;
      $ratio_launch_prio{"$client"}=$launches{"$client"}/${${$conf{'client'}}{$client}}{'priority'};
      return $client;
   }else{
      &debug_msg("No client runnable in get_next\n");
      return;
   }
   return;
}

#Elimine les clients présent dans la liste qui sont en cours d'éxécution et
#qui n'ont pas le allow_many (pour pas les éxécuter deux fois)
#Elimine aussi les clients qui ont terminé depuis moins que min_schedule
sub trim_possible_client(#){
   my $ref=shift;
   my @out_list;


   foreach my $arg (@$ref){
      if (${${$conf{'client'}}{$arg}}{'allow_many'} eq "no"){
	 #Exec only one
	 if ((time - $last_schedule{$arg}) >= ${${$conf{'client'}}{$arg}}{'min_schedule'}){
	    push(@out_list,$arg) unless grep($_ eq $arg, values %running);
	 }
      }else{
	 push(@out_list,$arg);
      }
   }

   return \@out_list;
}


sub change_uid_gid(#){
   my $client=shift;
   if ($gid{"$client"}){
      &debug_msg("Changing gid to $gid{$client} for $client ...");
      if (POSIX::setgid($gid{"$client"})){
	 &debug_msg("done\n");
      }else{
	 &debug_msg("error, $!\n");
	 exit(1);
      }
   }
   if ($uid{"$client"}){
      &debug_msg("Changing uid to $uid{$client} for $client ...");
      if (POSIX::setuid($uid{"$client"})){
	 &debug_msg("done\n");
      }else{
	 &debug_msg("error, $!\n");
	 exit(1);
      }
   }
}


#Sends mail on the input of the command (pipe)
sub send_data(#){
   my $client=shift;
   if (${${$conf{'client'}}{$client}}{'mbox'} =~ /\/$/){
      &debug_msg("Sending datas for ${${$conf{'client'}}{$client}}{'mbox'}\n");
      my @list=`ls -1 ${${$conf{'client'}}{$client}}{'mbox'}/new | head -n ${${$conf{'client'}}{$client}}{'nb_mails'}`;
      if ($#list >= 0){
	 #Send files, one by one
	 foreach my $fich (@list){
	    &debug_msg("File : $fich\n");
	    if (my $pid = open(CHILD, "|-")) {
	       CHILD->autoflush(1);
	       &debug_msg("Parent Pid $$\n");
	       if (open(DATA,"<${${$conf{'client'}}{$client}}{'mbox'}/new/$fich")){
		  while (my $line=<DATA>){
		     print CHILD "$line";
		     #&debug_msg("Sending : $line");
		  }
		  close(DATA);
	       }else{
		  &debug_msg("Error opening ${${$conf{'client'}}{$client}}{'mbox'}/new/$fich : $!\n");
		  exit(1);
	       }
	       close(CHILD);
	       #unlink("${${$conf{'client'}}{$client}}{'mbox'}/new/$fich");
	    } else {
	       die "cannot fork: $!" unless defined $pid;
	       &change_uid_gid("$client");
	       &debug_msg("Executing ${${$conf{'client'}}{$client}}{'command'} .... \n");
	       exec("${${$conf{'client'}}{$client}}{'command'}");
	    }
	 }
      }else{
	 &debug_msg("No mail available\n");
      }
   }else{
      &debug_msg("Mbox format not yet supported.\n");
      exit(1);
   }
}
      
   

sub launch(#){
   my $client=shift;
   #Launch the command of a simple client
   &debug_msg("Son $client, launching ${${$conf{'client'}}{$client}}{'command'}\n");
   #Change uid and gid if needed
   &change_uid_gid("$client");
   exec(${${$conf{'client'}}{$client}}{'command'});
}


sub main_loop {
   
   my $ld;
   my $nchild;

   while (1) {
      
      if ($reload_conf == 1){
	 #We have to load conf again
	 $reload_conf=0;
	 &debug_msg("Reloading configuration .... \n");
	 &debug_msg("  Waiting all child to terminate ...");
	 while ((my $kid=wait) != -1){
	    $nchild--;
	    delete($running{$kid});
	 }
	 &debug_msg("done\n");
	 &debug_msg("  Restarting                     ...");
	 &init;
	 &init_launches;
	 &debug_msg("done\n");
      }
	 
	 
      if ($nchild >= $conf{'max_client'}){
	 #We have to wait for one child (Blocking)"
	 &debug_msg("Waiting for childs to terminate\n");
	 my $kid=wait;
	 if ($kid>0){
	    &debug_msg("Kid=$kid est mort, c'était un $running{$kid}\n");
	    $nchild--;
	    delete($running{$kid});
	 }
      }else{
	 #Rip child terminated
	 &debug_msg("Checking dead childs....\n");
	 while ( (my $kid = waitpid(-1, WNOHANG)) > 0 ){
	    &debug_msg("Kid=$kid est mort, c'était un $running{$kid}\n");
	    $nchild--;
	    delete($running{$kid});
	 }
	 
	 #Look for a client to execute
	 my $ld = &get_load; 
	 &debug_msg("Load = $ld, processing\n");
	 my $possible_client=&get_possible_client($ld);
	 my @list=@{$possible_client};
	 &debug_msg("Avant Trim :  \n");
	 &debug_msg(Dumper($possible_client));
	 $possible_client=&trim_possible_client($possible_client);
	 &debug_msg("Après Trim : \n");
	 &debug_msg(Dumper($possible_client));
	 my $client=&get_next($possible_client);
	 if ($client){
	    #sleep(1);
	    &debug_msg("$client was chosen, launching\n");
	    $last_schedule{$client} = time ;
	    if (my $pid = fork){
	       #Pere, on marque qu'on l'a lancé
	       $running{$pid}="$client";
	       $nchild++;
	       &debug_msg("$client launched as PID $pid\n");
	       &debug_msg(Dumper(\%running));
	       &debug_msg(Dumper(\%launches));
	    }else{
	       #Fils
	       if (${${$conf{'client'}}{$client}}{'mbox'}){
		  #We have to send data ourselves
		  &send_data("$client");
	       }else{
		  #We only have to execute a command
		  &launch("$client");
	       }
	       exit(0);
	    }
	 }else{
	    &debug_msg("No client runable\n");
	    sleep($conf{'sleep_poll'});
	 }
	 sleep($conf{'active_poll'});
      }
   }
}



##########
## Main ##
##########

$SIG{HUP}=\&sigHUP_handler;
&init();
&init_launches();
&check_conf();
&daemonize();
&main_loop();


