#! /usr/bin/perl -w
#  cycled v1.0.3         (c) Copyright 1998 by Milan Sorm <sorm@fi.muni.cz>
#
## Usage: cycled [OPTION]...
## Touch-based starting daemon system
##
##   -h            this short help
##   -V            print version info
##   -c <file>     use another configuration file (default /etc/cycled.conf)
##   -l <file>     use another log file (default /var/log/cycled)
##   -q            quick start, don't do processes in first turn
##   -L <num>      set level of logging
##                   0  none
##                   1  errors
##                   2  forking processes (default)
##                   3  maximum level

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

my $defaultconf = "/etc/cycled.conf";     ## default configuration file
my $defaultlogfile = "/var/log/cycled";   ## default log file
my $defaultloglevel = 2;                  ## default log level
my $delaytime = 0.2;                      ## delay time in each turn

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

use English;
use strict;
use vars qw($opt_h $opt_V $opt_c $opt_l $opt_L $opt_q);
use POSIX;
use Getopt::Std;

my $conf;
my $logfile;
my $loglevel;
my $quickstart = 0;
my %SCRIPTS = ();
my %TIMES   = ();
my %LAST    = ();
my $testfile;

## Logging operations
sub do_syslog {
  my ($kind,$message) = @_;
  open LOG,">>$logfile" or (print STDERR "$logfile: $!\n" and return);
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime time;
  my $date = POSIX::strftime("%Y-%m-%d.%H-%M-%S",$sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst);
  print LOG "$date:cycled:$kind:$message\n";
  close LOG;
}

## Print help on this program (first ## lines)
sub help {
  open THISCODE,$PROGRAM_NAME or die "Can't open perl-code.";
  flock THISCODE,2;
  while (<THISCODE>) {
    chomp;
    next if /^#!/;
    next if /^# /;
    next if /^#$/;
    if (/^## (.*)$/) { print "$1\n"; } 
    elsif (/^##$/) { print "\n"; } 
    else { last; }
  }
  close THISCODE;
  exit;
}

## Print version of this program (first not #! line)
sub version {
  open THISCODE,$PROGRAM_NAME or die "Can't open perl-code.";
  flock THISCODE,2;
  while (<THISCODE>) {
    chomp;
    next if /^#!/;
    if (/^#  (.*)$/) { print "$1\n"; last; }
  }
  close THISCODE;
  exit;
}

## Die but use syslog instead standard error output
sub diewithsyslog {
  my $message = shift;
  do_syslog('err',$message) if $loglevel;
  print STDERR "$message\n";
  exit;
}

## start scripts passwed by arguments, but daemon go next
sub start_script {
  my $script = shift;        ## which
  my $pid;

FORK: {
  if ($pid = fork) {         ## parent process --- cycled daemon
    do_syslog 'info',"Fork process $script" if $loglevel >= 2;
  } elsif (defined $pid) {   ## child process --- cycled must do exec to start script 
    exec $script or diewithsyslog "I can't start $script";
    exit;
  } elsif ($! =~ /No more process/) {
    # EAGAIN, I must wait and then redo FORK
    sleep 5;
    redo FORK;
  } else {
    # I can't do fork
    diewithsyslog "I can't fork child process --- $!";
  }
}
}

## read configuration file
sub readconf {
  open CONF,$conf or diewithsyslog "$conf: $!";
  flock CONF,2 or diewithsyslog "$conf: $!";
  while (<CONF>) {
    chomp;
    $_ =~ s/#.*$//;      ## strip comments
    $_ =~ s/ $//g;       ## strip ending spaces
    $_ =~ s/^ //g;       ## strip leading spaces
    next unless $_;      ## skip empty lines
    if (/^(.*):([-]?\d*):(.*)$/) {     ## query-file:times:script
      $SCRIPTS{$1} = $3;
      my $orig_2 = $2?$2:0;
      $TIMES{$1} = abs $orig_2;
      
      if ($orig_2 < 0 or $quickstart) {
        $LAST{$1} = (stat $1)[9]; ## if times < 0 then don't do script in first way 
      } else {
        $LAST{$1} = -1-$TIMES{$1};     ## firstly always do all of the scripts 
      }
      unless ($1 and $3) {
        delete $SCRIPTS{$1};  delete $TIMES{$1};  delete $LAST{$1};
        do_syslog 'warn',"$conf: Invalid line $_\n" if $loglevel;
      } 
    } else {
      do_syslog 'warn',"$conf: Invalid line $_\n" if $loglevel;
    }
  }
  close CONF;
}

## main daemon with reading configuration file

getopt('clL');
if (defined $opt_L and ($opt_L eq '0')) { 
  $loglevel = 0; 
} else { 
  $loglevel = $opt_L || $defaultloglevel; 
}

$conf = $opt_c || $defaultconf;
$logfile = $opt_l || $defaultlogfile;
$quickstart = $opt_q || 0;

version if $opt_V;
help if $opt_h;

do_syslog 'info',"Start cycled" if $loglevel;
do_syslog 'info',"Process configuration file $conf." if $loglevel >= 2;
readconf;

$SIG{CHLD} = sub { wait };   # if my child stopped or terminated, I kill his zombie

while (1) {
  foreach $testfile (keys %SCRIPTS) {
    if (-f $testfile) {
      my $mtime = (stat $testfile)[9];
      if ($mtime > $LAST{$testfile}+$TIMES{$testfile}) {
        $LAST{$testfile} = $mtime;  start_script $SCRIPTS{$testfile};
      }
    } else {
      if ($LAST{$testfile} < 0) {
        start_script $SCRIPTS{$testfile};        ## do all scripts when cycled start
        $LAST{$testfile} = 0;
      }
    }
  }
  select(undef,undef,undef,$delaytime);        ## wait for $delaytime second, we can go slowly
}
