File System Update Daemon for synchronizing files between 2 RHAS 4 Apache Servers
  
 File locations:
/var/log/fsupd.webmaster
/etc/fsupd.conf
/etc/rc.d/init.d/fsupd
/usr/local/bin/fsupd.pl
 
/usr/local/bin/fsupd.pl contains:
#!/usr/bin/perl
 use strict;
use POSIX qw(setsid);
use Getopt::Std qw(getopts);
 ########## BOOTSTRAP ##########
#
# Read the config file
# Check logging directory
# Check user and demote?
# Start logging
# Daemonize
# Author: Thaddeus Hogan
 # Get options
my $opts = {};
getopts('d', $opts);
 my $debug = $opts->{'d'};
print "Debug output on.n" if $debug;
 # Find the config file using the following list in order
my @cfgPaths = (
        'fsupd.conf',           # Check working directory
        '/etc/fsupd.conf'       # Check /etc
);
 print "Scanning for config location.n" if $debug;
 my $cfgPath = undef;
 foreach my $c (@cfgPaths) {
        print "Checking for config at: $cn" if $debug;
         if (-r $c) {
                print "Found config at: $cn" if $debug;
                $cfgPath = $c;
                last;
        }
}
 if (! $cfgPath) {
        print "fsupd Unable to locate configuration file.  Terminating.n";
        exit 1;
}
 # Config file found, parse
my $scanPaths = {};
my $logdir = '/var/log/fsupd';
my $runas = undef;
my $logfile = 'fsupd.log';
my $pidfile = 'fsupd.pid';
 open CONF, $cfgPath;
my @cfg = <CONF>;
close CONF;
 my $cfgParseMode = 1;   # Parse mode 1 = pre-scandefs, 2 = scandefs
my $cscanPath = {};             # Current scan path
 foreach my $line (@cfg) {
        if ($cfgParseMode == 1) {
                $logdir = $1 if $line =~ /logdirs*=s*(.*)/;
                $runas = $1 if $line =~ /runass*=s*(.*)/;
                $logfile = $1 if $line =~ /logfiles*=s*(.*)/;
                $pidfile = $1 if $line =~ /pidfiles*=s*(.*)/;
                $cfgParseMode = 2 if $line =~ /^[/;
        }
         if ($cfgParseMode == 2) {
                if ($line =~ /[([^]]*)]/) {
                        $scanPaths->{$1} = {};
                        $cscanPath = $scanPaths->{$1};
                }
                 if ($line =~ /(w+)s*=s*(.+)/) {
                        $cscanPath->{$1} = $2;
                }
        }
}
 my $logpath = "$logdir/$logfile";
 # Print out configuration if debug is set
if ($debug) {
        print "Config:n";
        print "logdir = $logdirn";
        print "logfile = $logfilen";
        print "runas = $runasn";
         foreach my $scanPath (keys %{ $scanPaths }) {
                print "Scan Path: $scanPathn";
                foreach my $param (keys %{ $scanPaths->{$scanPath} }) {
                        print "t$param = $scanPaths->{$scanPath}->{$param}n";
                }
        }
         print "n";
}
 # Create logging directory if it does not exist
if (! -d $logdir) {
        print "Log directory $logdir does not exist, will attempt to create.n" if 
 $debug;
         if (! mkdir($logdir)) {
                print "Could not create logging directory: $logdirn";
                print "$!n";
                print "Terminating.n";
                exit 1;
        }
}
 # Demote daemon if requested
if ($runas) {
        print "Preparing to demote process to user: $runasn" if $debug;
         # Make sure user can demote
        if ($> != 0) {
                print "fsupd not run as root, cannot demote to $runas.n";
                print "Terminating.n";
                exit 1;
        }
         # Get UID for named user, make sure it exists
        my $runasUID = getpwnam($runas);
        print "runasUID = $runasUIDn" if $debug;
         if (! $runasUID) {
                print "fsupd requested to run as $runas but user does not 
 exist.n";
                exit 1;
        }
         # Make sure the log directory is owned by the requested user
        chown $runasUID, -1, $logdir;
        if (-e $logpath) {
                chown $runasUID, -1, $logpath;
        }
         # Demote the process
        $> = $runasUID;
        print "Process demoted to $runas.n" if $debug;
}
 # Start logging
my $logfh = undef;
open $logfh, ">>$logpath";
select((select($logfh), $| = 1)[0]);    # Make it hot
 # Daemonize
&log("fsupd startup successful, daemonizing");
 my $pid = fork;
if ($pid) {
        &log("daemon process is $pid");
        print "daemon process is $pidn" if $debug;
         open PIDF, ">$logdir/$pidfile";
        print PIDF $pid;
        close PIDF;
         exit;
}
 open STDOUT, "/dev/null";
open STDERR, "/dev/null";
setsid();
 ######### BUILD MONITOR CHAIN ##########
 # Run through the scan paths and construct the monitor chain
# This will be used in a state pump (shift, process, push)
my @monitors = ();
 foreach my $scanPath (keys %{ $scanPaths }) {
        # Sanity check
        if (! -d $scanPath) {
                &log("scan path not directory, excluding: $scanPath");
                next;
        }
         if (! -r $scanPath) {
                my $uname = getpwnam($>);
                &log("scan path not readable by $uname($>), excluding: $scanPath");
                next;
        }
         # Create monitor
        my $monitor = {
                path => $scanPath,
                nextscan => 0,
                on_update => $scanPaths->{$scanPath}->{'on_update'},
                scanfactor => $scanPaths->{$scanPath}->{'scanfactor'},
                scanmin => $scanPaths->{$scanPath}->{'scanmin'},
                lastscantime => 0,
                evtUpdatePending => 0,
                items => {},
                excludes => []
        };
         &log("added scan path: $scanPath");
         my @excludes = split /|/, $scanPaths->{$scanPath}->{'excludes'};
        foreach my $e (@excludes) {
                push @{ $monitor->{'excludes'} }, $e;
                &log("excluding on pattern: $e");
        }
         push @monitors, $monitor;
}
 # Always have string 'sleep' in monitor array, the pump below will snooze
# when it encounters this
push @monitors, 'sleep';
 ########## MAIN MONITOR LOOP ##########
 while (1) {
        my $monitor = shift @monitors;
         # Check for sleep monitor
        if ($monitor eq 'sleep') {
                sleep 1;
                push @monitors, 'sleep';
                next;
        }
         # See if it is time to process this monitor
        if (time >= $monitor->{'nextscan'}) {
                # Scan the path, start time measure
                &log("scanning $monitor->{'path'}") if $debug;
                my $st = time;
                 my $lastScan = $monitor->{'items'};
                $monitor->{'items'} = &scanpath($monitor->{'path'}, 
 $monitor->{'excludes'});
                 # Variables for each event
                my $evt_adorm = 0; # added or removed
                my $evt_chmod = 0;
                my $evt_chown = 0;
                my $evt_chgrp = 0;
                my $evt_chsiz = 0;
                my $evt_chatm = 0;
                my $evt_chmtm = 0;
                my $evt_chctm = 0;
                 # Only do the compare if this isn't the first scan
                if (scalar(keys(%{ $lastScan }))) {
                        # Check for updates against the last scan
                        my @oldFiles = keys %{ $lastScan };
                        my @newFiles = keys %{ $monitor->{'items'} };
                         # See if any files were added or removed
                        if ( (scalar(@oldFiles) != scalar(@newFiles)) || (! 
 @oldFiles and @newFiles) ) {
                                &log("$monitor->{'path'} detected files added or 
 removed");
                                $evt_adorm = 1;
                        }
                         # Test for attribute changes
                        foreach my $oldFileName (@oldFiles) {
                                my $oldFile = $lastScan->{$oldFileName};
                                my $newFile = $monitor->{'items'}->{$oldFileName};
                                 if (! $newFile) {
                                        &log("File $oldFileName not in new set") if 
 $debug;
                                        next;
                                }
                                 if ($oldFile->{'mode'} != $newFile->{'mode'}) {
                                        &log("$oldFileName detected mode change") 
 if $debug;
                                        $evt_chmod = 1;
                                }
                                 if ($oldFile->{'uid'} != $newFile->{'uid'}) {
                                        &log("$oldFileName detected uid change") if 
 $debug;
                                        $evt_chown = 1;
                                }
                                 if ($oldFile->{'gid'} != $newFile->{'gid'}) {
                                        &log("$oldFileName detected gid change") if 
 $debug;
                                        $evt_chgrp = 1;
                                }
                                 if ($oldFile->{'size'} != $newFile->{'size'}) {
                                        &log("$oldFileName detected size change") 
 if $debug;
                                        $evt_chsiz = 1;
                                }
                                 if ($oldFile->{'atime'} != $newFile->{'atime'}) {
                                        &log("$oldFileName detected access time 
 change") if $debug;
                                        $evt_chatm = 1;
                                }
                                 if ($oldFile->{'mtime'} != $newFile->{'mtime'}) {
                                        &log("$oldFileName detected modification 
 time change") if $debug;
                                        $evt_chmtm = 1;
                                }
                                 if ($oldFile->{'ctime'} != $newFile->{'ctime'}) {
                                        &log("$oldFileName detected i-node change") 
 if $debug;
                                        $evt_chctm = 1;
                                }
                        }
                }
                 # Composite events
                my $evt_update = $evt_chmod || $evt_chown || $evt_chgrp || 
 $evt_chsiz || $evt_chmtm || $evt_adorm;
                 # Capture the elapsed time as soon as we finish
                my $et = time - $st;
                 # Calculate the next scan time
                #       elapsed time * scan factor OR scanmin, whichever is smaller
                my $nextscan = $et * $monitor->{'scanfactor'};
                $nextscan = $monitor->{'scanmin'} if $nextscan < 
 $monitor->{'scanmin'};
                $monitor->{'nextscan'} = time + $nextscan;
                $monitor->{'lastscantime'} = $et;
                 &log("scan time for $monitor->{'path'} was $et sec") if $debug;
                &log("next scan in T+$nextscan") if $debug;
                 # Finally, execute requested operations for any events that occured
                if ($evt_update) {
                        $monitor->{'evtUpdatePending'} = 1;
                        &log("$monitor->{'path'} update detected, pending 
 stablization for event") if $debug;
                }
                 if ($monitor->{'evtUpdatePending'} > 0 and 
 $monitor->{'evtUpdatePending'} < 3) {
                        $monitor->{'evtUpdatePending'}++;
                } elsif ($monitor->{'evtUpdatePending'} >= 3) {
                        &log("$monitor->{'path'} updates appear stable");
                        &log("exec $monitor->{'on_update'}");
                        system($monitor->{'on_update'});
                        $monitor->{'evtUpdatePending'} = 0;
                }
        }
         # Return the monitor to the end of the queue
        push @monitors, $monitor;
}
 ########## FILESYSTEM SCAN FUNCTIONS ##########
 # scanpath(pathToScan)
#       scans specified path and returns hash where keys are path items
#       and contents are file stats
sub scanpath {
        my ($path, $excludes) = @_;
         opendir DIR, $path;
        my @files = grep(!/^..?$/, readdir(DIR));
        closedir DIR;
         my $stats = {};
         foreach my $f (@files) {
                my $fp = "$path/$f";
                 if ($excludes) {
                        my $excluded = 0;
                        foreach my $ex (@{ $excludes }) {
                                if ($fp =~ /$ex/) {
                                        &log("excluded $fp on /$ex/") if $debug;
                                        $excluded++;
                                        last;
                                }
                        }
                         next if $excluded;
                }
                 my @stat = stat($fp);
                if (! @stat) {
                        &log("stat failed, invalidating scan: $path");
                        return 0;
                }
                 $stats->{$fp} = {};
                $stats->{$fp}->{'mode'} = $stat[2];
                $stats->{$fp}->{'uid'} = $stat[4];
                $stats->{$fp}->{'gid'} = $stat[5];
                $stats->{$fp}->{'size'} = $stat[7];
                $stats->{$fp}->{'atime'} = $stat[8];
                $stats->{$fp}->{'mtime'} = $stat[9];
                $stats->{$fp}->{'ctime'} = $stat[10];
                 if (-d $fp) {
                        $stats = {%{ $stats }, %{ &scanpath($fp) }};
                }
        }
         return $stats;
}
 ########## UTILITY FUNCTIONS ##########
 # Log sub logs a message and makes sure that the filehandle is still valid
sub log {
        my ($msg) = @_;
         my ($sec, $min, $hour, $day, $mon, $year) = localtime(time);
        $year += 1900;
        $mon += 1;
        $mon = "0$mon" if $mon < 10;
        $day = "0$day" if $day < 10;
        $hour = "0$hour" if $hour < 10;
        $min = "0$min" if $min < 10;
        $sec = "0$sec" if $sec < 10;
         if (! -e $logpath) {
                print "Log file disappeared! Attempting to re-open.n" if $debug;
                 close $logfh;
                open $logfh, ">>$logpath" or print "Log re-open failed: $!";
        }
         seek $logfh, 0, 1;
        print $logfh "[$year-$mon-$day $hour:$min:$sec] $msgn";
}
  
  
 /etc/fsupd.conf contains:
logdir = /var/log/fsupd.webmaster
[/var/www/html]
on_update = rsync -a --delete --exclude=/logs --exclude=.* /var/www/html/ 
 mnsvlwwwt001:/apps/apache2/htdocs/
scanfactor = 3
scanmin = 4
excludes = /var/www/html/logs|/.
 
/etc/rc.d/init.d/fsupd contains:
#!/bin/bash
#
# fsupd         System init script for the fsupd daemon
#
# chkconfig: - 99 10
# description: File System Update Daemon
 FSUPD=/usr/local/bin/fsupd.pl
PIDFILE=/var/log/fsupd.webmaster/fsupd.pid
 start() {
        su - webmaster -c "$FSUPD -d"
}
 stop() {
        kill `cat $PIDFILE`
        rm $PIDFILE
}
 case "$1" in
        start)
                start
        ;;
        stop)
                stop
        ;;
        restart)
                stop
                sleep 1
                start
        ;;
        *)
                echo "Usage: fsupd {start | stop | restart}"
                exit 1
esac