#!/usr/bin/perl

# Cronjob Execution Script.
# Version 1.1.0
# (C) 2016 Domero, Michel Kuipers
# chaosje@gmail.com
# All Rights Reserved

# Use at own risk
# no warrenty whatsoever

# User defined settings

$config = "/etc/cronjobs.conf";
$log = "/var/log/cronjobs.log";
$timers = "/var/lib/cronjobs/cronjobs.timers";

# End of user defined settings

# CODE

print mtime()." Cronjob execution script (C) 2016 Domero\n";

# Open log-file
if (!-e $log) {
  $out=mtime()." Created log file '$log'";
  `echo "$out\n">$log`;
}

# Read config file
@lines=();
sysopen(CONF,$config,O_RDONLY | O_BINARY) || error("Unable to open config file '$config'");
while (<CONF>) {
  my ($info,$comment) = split(/\#/,$_);
  $info =~ s/^[\s\t]+//;
  $info =~ s/[\s\t\n\r]+$//;
  if ($info) {
    push @lines,$info;
  }
}
close(CONF);

if ($#lines<0) {
  error("Please config '$config'")
}

# Parse jobs
@jobs=();
%jobnr=();
$nr=-1;
$mode=0;

foreach my $line (@lines) {
  if ($mode==0) {
    # expecting start
    if ($line =~ /^*?cronjob[\s\t]+(.*)?[\s\t]*\{/) {
      my $name=$1; $name =~ s/[\s]+$//;
      $nr++; 
      if (!$name) { error("Expected name in crobjob number ".($nr+1)) };
      push @jobs,{ name => $name };
      $jobnr{$name}=$nr;
      $mode=1
    } else {
      error("Expecting 'cronjob name {' but found '$line'")
    }
  } else {
    # Parse all keyvalues
    my ($key,$val)=split(/=/,$line);
    $key =~ s/[\s]+$//;
    $val =~ s/^[\s]+//;
    if ($key =~ /^cronjob/i) {
      error("Missing termination bracket in cronjob '$jobs[$nr]{name}'")
    }
    elsif (($key eq '}') && !$val) {
      # termination block
      $mode=0
    } 
    else {
      # check key
      if (validkey($key)) {
        $jobs[$nr]{$key}=$val
      } else {
        error("Illegal Key '$key' found in cronjob '$jobs[$nr]{name}'")
      }
      # check abstime
      if (lc($key) eq 'abstime') {
        if ($val =~ /[^0-9:]/) {
          error("Illegal value for key 'abstime' in cronjob '$jobs[$nr]{name}', use hh:mm in 24 hours format") 
        }
        my ($hr,$mn) = split(/\:/,$val);
        if (!hr || ($hr>23) || !$mn || ($mn>59)) {
          error("Illegal value for key 'abstime' in cronjob '$jobs[$nr]{name}', use hh:mm in 24 hours format") 
        }
        $jobs[$nr]{hour}=$hr;
        $jobs[$nr]{min}=$mn
      }
    }
  }
}

if ($mode) {
  error("Missing termination bracket in cronjob '$jobs[$nr]{name}'")
}

# Check for missing values
for (my $i=0; $i<=$nr; $i++) {
  if (!$jobs[$i]{process}) { error("Missing process-key in cronjob '$jobs[$i]{name}'") }
  if (!defined $jobs[$i]{time}) { $jobs[$i]{time}=1 }
  if (!defined $jobs[$i]{keepalive}) { $jobs[$i]{keepalive}=1 }
  if (!defined $jobs[$i]{block}) { $jobs[$i]{block}=0 }
  if (!defined $jobs[$i]{user}) { $jobs[$i]{user}='root' }
  if (!defined $jobs[$i]{verbose}) { $jobs[$i]{verbose}=1 }
  # pid = optional
  # abstime = optional
  # screen = optional
  # path = optional
}

# Check Timer-file
if (!-e $timers) {
  my $out;
  for (my $i=0; $i<=$nr; $i++) {
    $out.="$jobs[$i]{name} ".(time-60)."\n"
  }
  `echo "$out">$timers`;
  addlog("Created timer-file '$timers'");
  print "Created timer file '$timers'\n";
}

# Now GO!!

# Read timer file

sysopen(TMRS,$timers,O_RDONLY | O_BINARY) || error("Unable to open timers file '$timers'");
while (<TMRS>) {
  my $line=$_;
  my @sa=split(/\s/,$line);
  my $tm=pop @sa;
  my $proc=join(" ",@sa);
  if (defined $jobnr{$proc}) {
    $jobs[$jobnr{$proc}]{lasttime}=$tm
  }
}
close(TMRS);

# reading process list
my @pl=`ps axo comm,pid`;
shift @pl;
my %pids=();
my %procs=();
foreach (@pl) { 
  my ($comm,$pid) = split(/[\s]+/,$_);
  if ($pid && $comm) {
    $pids{$pid}=$comm;
    $procs{$comm}=$pid;
  }
}

for (my $i=0; $i<=$nr; $i++) {
#  addlog("$jobs[$i]{name}");
  if (!$jobs[$i]{block}) {
    if ($jobs[$i]{keepalive}) {
      # we must have this process running! overrule time-switch
      if ($jobs[$i]{pid}) {
        # pid, optional, file must exist, try to read it
        if (-e $jobs[$i]{pid}) {
          # pid exists, now read the pid
          open (my $pidfile,"<$jobs[$i]{pid}");
          my $pid=<$pidfile>;
          close($pidfile);
          $pid =~ s/[^0-9]//g;
          if ($pid) {
            # we gotta pid, now search for it
            if (!$pids{$pid}) {
              # process is death! kill the pid!
              addlog("Deleted stale PID $pid - $jobs[$i]{pid}");
              unlink $jobs[$i]{pid};
              # restart process
              startjob($jobs[$i],1);
            } else {
              # process is up!
            }
          } else {
            # bogus pidfile! check on process-name
            `rm $jobs[$i]{pid}`;
            my @fs=split(/\//,$jobs[$i]{process});
            my $fileparam=pop @fs; my $check=pop @fs;
            my ($file,@param) = split(/\s/,$fileparam);
            if ($file =~ /^[0-9]$/) { $file="$check/$file" }
            if ($procs{$file}) {
              # process is up!
            } else {
              # restart process
              startjob($jobs[$i],1);
            }
          }
        } else {
          # no pid, check on process-name
          checkbyname($i)
        }
      } else {
        # no pid, check on process-name
        checkbyname($i)
      }
    } else {
      # process must be run once, so check time
      # please make sure process takes less than 'time' or 1 day if using 'abstime'
      if ($jobs[$i]{abstime}) {
        my @t = localtime();
        if (($t[2]==$jobs[$i]{hour}) && ($t[1]==$jobs[$i]{min})) {
          startjob($jobs[$i])
        }
      } else {
        if ($jobs[$i]{time}>1) {
          # Read timer and check if process must be run
          my $diff=$jobs[$i]{time}*60-1;
          if (time-$jobs[$i]{lasttime}>=$diff) {
            startjob($jobs[$i])
          }
        } else {
          # start job
          startjob($jobs[$i])
        }
      }    
    }
  }
}

# Write timer file for next run
my $out;
for (my $i=0; $i<=$nr; $i++) {
  $out.="$jobs[$i]{name} $jobs[$i]{lasttime}\n"
}
`echo "$out">$timers`;

# WE'RE DONE!!
#addlog("Run OK");

# Subroutines

sub checkbyname {
  my ($i) = @_;
  my @ss=split(/ /,$jobs[$i]{process});
  my $pname=shift @ss;
  my @fs=split(/\//,$pname);
  my $file=pop @fs;
  if ($procs{$file}) {
    # process is up!
  } else {
    # restart process
    startjob($jobs[$i],1);
  }
}

sub startjob {
  my ($job,$restart)=@_; my $txt="Started";
  if ($restart) { $txt="Restarted" }
  my $path = ($job->{path} ? "cd $job->{path} ; ":"");
  if ($job->{screen}) {
    addlog("$txt cronjob '$job->{name}' - '$job->{process}' to screen '$job->{screen}'");
    if (!$job->{verbose}) {
      system($path."sudo -b -u $job->{user} screen -dmS $job->{screen} $job->{process} >> /dev/null 2>&1")
    } else {
      system($path."sudo -b -u $job->{user} screen -dmS $job->{screen} $job->{process} >> $log")
    }
  } else {
    addlog("$txt cronjob '$job->{name}' - $job->{process}");
    if (!$job->{verbose}) {
      system($path."sudo -b -u $job->{user} $job->{process} >> /dev/null 2>&1")
    } else {
      system($path."sudo -b -u $job->{user} $job->{process} >> $log")
    }
  }
  $job->{lasttime}=time
}
 
sub validkey {
  my $k=lc($_[0]);
  if ($k eq 'keepalive') { return 1 }
  if ($k eq 'block') { return 1 }
  if ($k eq 'user') { return 1 }
  if ($k eq 'pid') { return 1 }
  if ($k eq 'path') { return 1 }
  if ($k eq 'process') { return 1 }
  if ($k eq 'time') { return 1 }
  if ($k eq 'abstime') { return 1 }
  if ($k eq 'verbose') { return 1 }
  if ($k eq 'screen') { return 1 }
  return 0
}

sub error {
  $out="ERROR ".mtime()." ".join("\n",@_);
  `echo "$out">>$log`;
  print "\n*** Terminated due to errors!\n".join("\n",@_)."\n"; exit 1
}

sub addlog {
  $out=mtime()." ".join("\n",@_);
  `echo "$out">>$log`;
  print "\n".join("\n",@_)."\n";
}

sub mtime {
  @t=localtime();
  my $ts;
  if ($t[3]<10) { $ts="0" } $ts.=$t[3]."-";
  $t[4]++; if ($t[4]<10) { $ts.="0" } $ts.=$t[4]."-";
  $t[5]+=1900; $ts.=$t[5]." ";
  if ($t[2]<10) { $ts.="0" } $ts.=$t[2].":";
  if ($t[1]<10) { $ts.="0" } $ts.=$t[1].":";
  if ($t[0]<10) { $ts.="0" } $ts.=$t[0];
  return $ts
}

# END OF SCRIPT (C) 2012 Domero 
