#!/usr/bin/perl

###################################################
# Global Data
###################################################
# These are variables one might possibly want to modify by hand...

# These are the defaults for the various command line options
my $PluginDir = '/etc/systracker.d/';
my $ConfigFile = '/etc/systracker.d/systracker.conf';
my $Debug = 0;
my $Verbose = 0;
my $Restore = 0;
my $Print = 0;

# Other global variables
$ProgName = 'SysTracker';
my $ProgVersion = '1.0';
my $ProgDate = '2/07/00';
my $Copyright = 'Copyright 1999 by Kirk Bauer <kirk@kaybee.org>.  Released under the GPL.';

# Mail program
my $MailProg = '/bin/mail';
if (-x '/usr/bin/mailx') {
   $MailProg = '/usr/bin/mailx';
}

# Determine hostname
my $HostName = `hostname`;
chomp ($HostName);


###################################################
# Include needed modules...
###################################################
use Getopt::Long;
use Fcntl ':flock'; # import LOCK_* constants
use File::Copy;
use Symbol;


###################################################
# Define any global variables that plugins may use
###################################################

# If it is found that the last run of the program was not completed,
# then this variable is set and things will be set back to how they
$Recover = 0;

# This contains all of the program's options...
# Options are anything that appear in the config file that don't
# begin with +, - or *.
%Options = ();

# Function pointers may be added to this list.  They will be called
# immediately before the program exits.  They need to accept one parameter which
# is reserved for future use.
@ExecuteBeforeExit = ();

# This is the timestamp that will be placed on all log entries for this
# run of the program.  SetTimeStamp() calculates the value.
$TimeStamp = "";

# This will be set if a new repository is being initialized
$Init = 0;

# This will be set if config test mode is being used
$Test = 0;

# These are set when the program is to run in special modes
$SampleConfig = 0;
$ShowDocs = 0;

###################################################
# Define all other global variables
###################################################
# Variables used by the program for runtime status
# The name of the temp file used to keep the report that will be mailed
my $MailFile = "";

# This contains all the plugin information
# $Plugins{name}{'version'} = plugin version
# $Plugins{name}{'description'} = plugin description
# %{$Plugins{name}{'options'}} = plugin options (names & desc)
# %{$Plugins{name}{'filecommands'}} = file commands (names & desc)
# %{$Plugins{name}{'commands'}} = regular commands (names & desc)
# %{$Plugins{name}{'checktypes'}} = check types (names & desc)
my %Plugins = ();

# This contains all of the commansd that may appear in the file listing
# All file commands are in the config file and begin with + or -
my %FileCommands = ();

# These are other commands the plugin may provide.
my %Commands = ();

# This contains all of the check types.  Check types are the various
# types of checks that may be performed on files (i.e. MD5, Own, Size, etc)
my %CheckTypes = ();

# These are explicit commands that are executed in the config file
my @SpecialCommands = ();

# This is a list of open filehandles
my @OpenFiles = ();

# This is a list of data files that have been modified
my @DataFiles = ();

# This keeps track of all the file commands that were in the config file
my @CommandList = ();

# This is where the file database is loaded into
my %db = ();

# This is the filename for the main logfile (i.e. audit trail)
my $LogFile = "";


###################################################
# General subroutines (for use by everybody)
###################################################

# debug() will display a debugging message if debugging
# is turned on.  Pass it a string to display
sub debug ($$) {
   warn "DEBUG($_[0]): $_[1]\n" if ($Debug >= $_[0]);
}

# inform() information will only be displayed if the program
# is running in verbose mode.
sub inform ($) {
   report($_[0]) if ($Verbose or $Debug);
}

# report() should be called with any information that should
# be included in an optional mailed or printed report about
# changes to the system
sub report ($) {
   if ($Print) {
      print "$_[0]\n";
   }
   elsif ($Options{"$ProgName-MailTo"}) {
      StartMail() unless ($MailFile);
      print MAILFILE "$_[0]\n" unless ($MailFile eq 'NULL');
   }
}

# Adds an event to the system log (i.e. a file changed)
# Takes in a plugin name and then a string
# An optional 3rd argument is the filename this pertains to
# If a 3rd argument is given, there should be the characters
# "FILE" contained in the string passed in as the second argument.
# "FILE" will be replaced with the filename given in the 3rd argument
# This will allow for consistent representation of filenames in the
# log file.
sub LogEntry ($$$) {
   my ($Plugin, $String, $file) = @_;
   my $Version = $Plugins{$Plugin}{'version'};
   if ($file) {
      $String =~ s/FILE/[[$file]]/;
   }   
   unless ($Init) {
      print LOGFILE "[$TimeStamp] $Plugin($Version) $String\n";
      report $String;
   }
}

# This subroutine creates a file if it doesn't exist yet
# Most importantly, it sets the permissions and ownership
# so that it's only readable by root.  Finally, if the file
# is a symlink, it will be erased first.
sub TouchFile ($) {
   my $file = $_[0];
   unless (-f $file) {
      # Isn't a normal file...
      if (-l $file) {
         # It's a symlink
         unlink ($file);
      }
      # Create file...
      open (TOUCH, ">$file") or warn "*WARNING: Couldn't create file: $file\n";
      close (TOUCH);
   }
   chmod 0600, $file;
   chown 0, 0, $file;
}

###################################################
# Plugin Registration subroutines
###################################################
# These subroutines allow the plugins to register
# themselves and their capabilities

# Every plugin must call this function
#   name:        The name for the plugin (i.e. RPM, Config)
#                The plugin name may not contain a hyphen
#   version:     The version string for the plugin
#   function_ptr: A pointer to the function to be called in restore mode
#                 that will process changes reported by this module
#      The function must accept ??
#   description: A verbose description of the plugin
# Returns 1 on success
sub PluginRegister ($$$$) {
   my ($name, $version, $function_ptr, $description) = @_;
   if ($name =~ /(=|-|\s|\#|\(|\))/) {
      # The following are bad characters:  = - ( ) # or whitespace
      warn "*WARNING: Plugin name $name contains invalid character(s)\n";
      return 0;
   }
   $Plugins{$name}{'version'} = $version;
   $Plugins{$name}{'restore'} = $function_ptr;
   $Plugins{$name}{'description'} = $description;
   debug 2, "Registered plugin: $name ($version)";
   return 1;
}

# If the plugin wants to have options in the config file, they
# must be registered through this function
#   option_name: The name of the option.  It *must* begin with
#                the name of the plugin followed by a hyphen
#   def_value:   Give the default value for the option
#   description: Describe the option and what it does
# Returns 1 on success
sub PluginRegisterOption ($$$) {
   my ($plugin_name);
   my ($option_name, $default_value, $description) = @_;
   # Determine name of plugin from option name which will
   # be something like RPM-OptionName
   $plugin_name = $option_name;
   unless ($plugin_name =~ s/^([^-]+)-.+$/$1/) {
      warn "*WARNING: Plugin registered invalid option: $option_name\n";
      return 0;
   }
   unless ($Plugins{$plugin_name}{'version'}) {
      warn "*WARNING: Plugin registered option for non-existent plugin: $option_name\n";
      return 0;
   }
   if ($option_name =~ /(=|\s|\#|\(|\))/) {
      # The following are bad characters:  = ( ) # or whitespace
      warn "*WARNING: Plugin option $option_name contains invalid character(s)\n";
      return 0;
   }
   # Store the description for the option
   $Plugins{$plugin_name}{'options'}{$option_name} = $description;
   # Store the default value for the option
   $Options{$option_name} = $default_value;
   debug 6, "Registered plugin option: $option_name = $default_value";
   return 1;
}

# This is used for plugins to register generic commands that can be called from
# the config file. 
#   plugin_name:  The name of the plugin that is registering this command
#   command_name: The name of the command that can be executed
#   function_ptr: A pointer to the function to be called.  
#      The function must accept one parameter which contains the arguments, if any
#   description:  A description of the function
# Returns 1 on success
sub PluginRegisterCommand ($$$$) {
   my ($plugin_name, $command_name, $function_ptr, $description) = @_;
   unless ($Plugins{$plugin_name}{'version'}) {
      warn "*WARNING: Non-existent plugin ($plugin_name) registered command: $command_name\n";
      return 0;
   }
   if ($Commands{$command_name}) {
      warn "*WARNING: Plugin $plugin_name tried to register a command that already exists: $command_name\n";
      return 0;
   }
   if ($command_name =~ /(=|\s|\#|\(|\))/) {
      # The following are bad characters:  = ( ) # or whitespace
      warn "*WARNING: Plugin command $command_name contains invalid character(s)\n";
      return 0;
   }
   # Store the description for the command
   $Plugins{$plugin_name}{'commands'}{$command_name} = $description;
   # Store the function pointer
   $Commands{$command_name} = $function_ptr;
   debug 6, "Registered command ($command_name) for plugin ($plugin_name)";
   return 1;
}

# If the plugin wants to add additional commands that can be
# used in the file listing, then they must be registered here
#   plugin_name:  The name of the plugin that is registering this command
#   command_name: The name of the command that can be placed in the file list
#   function_ptr: A pointer to the function to be called.  It must accept the following parameters:
#          1) The name of the file that needs to be either accepted or rejected
#          2) The arguments given in the config file, if any
#          The function must return true if the file is accepted, false if it is rejected
#   description:  A description of the function
# Returns 1 on success
sub PluginRegisterFileCommand ($$$$) {
   my ($plugin_name, $command_name, $function_ptr, $description) = @_;
   unless ($Plugins{$plugin_name}{'version'}) {
      warn "*WARNING: Non-existent plugin ($plugin_name) registered file command: $command_name\n";
      return 0;
   }
   if ($FileCommands{$command_name}) {
      warn "*WARNING: Plugin $plugin_name tried to register a file command that already exists: $command_name\n";
      return 0;
   }
   if ($command_name =~ /(=|\s|\#|\(|\))/) {
      # The following are bad characters:  = ( ) # or whitespace
      warn "*WARNING: Plugin file command $command_name contains invalid character(s)\n";
      return 0;
   }
   # Store the description for the command
   $Plugins{$plugin_name}{'filecommands'}{$command_name} = $description;
   # Store the function pointer
   $FileCommands{$command_name} = $function_ptr;
   debug 6, "Registered file command ($command_name) for plugin ($plugin_name)";
   return 1;
}

# If the plugin wants to be able to do additional checks on files, the
# check type must be registered here
#   plugin_name:  The name of the plugin that is registering this check type
#   check_name:   The name of the check type
#   function_ptr: A pointer to the function to be called.  It must accept two arguments:
#      1) The name of the file or directory to be checked
#      2) The test result from the last run, if any...
#      It must return the new test result.  Test results must be scalars and should be as concise as possible.
#      Optionally, a list of values may be returned, with the first value being the new test results, and the
#      subsequent values being other requested checks to be performed on the file.  The test results may also be
#      null, in which case no data is stored in the comparison database for that check.
#   description:  A description of the check type
sub PluginRegisterCheckType ($$$$) {
   my ($plugin_name, $check_name, $function_ptr, $description) = @_;
   unless ($Plugins{$plugin_name}{'version'}) {
      warn "*WARNING: Non-existent plugin ($plugin_name) registered check type: $check_name\n";
      return 0;
   }
   if ($CheckTypes{$check_name}) {
      warn "*WARNING: Plugin $plugin_name tried to register a check type that already exists: $check_name\n";
      return 0;
   }
   if ($check_name =~ /(=|\s|\#|\(|\))/) {
      # The following are bad characters:  = ( ) # or whitespace
      warn "*WARNING: Plugin check type $check_name contains invalid character(s)\n";
      return 0;
   }
   # Store the description for the check type
   $Plugins{$plugin_name}{'checktypes'}{$check_name} = $description;
   # Store the pointer to the function
   $CheckTypes{$check_name} = $function_ptr;
   debug 6, "Registered check type ($check_name) for plugin ($plugin_name)";
   return 1;
}


###########################################
# File functions to be used by plugins
###########################################
# Any files that will be modified during the execution of this program
# must be handled through these functions.  

# This file will first backup the file, and then open the file for writing.
# If you accept a single scalar as the return value, then the function will
# return an open filehandle.  The file is always replaced (i.e. not appended to)
sub OpenFile ($) {
   my $file = $_[0];
   my ($write, $read, $readfile);
   $write = gensym();
   if (($readfile = BackupFile ($file)) and (open ($write, ">$file")) ) { 
      push @OpenFiles, $write;
      if (wantarray) {
         # Return read and write handles if they want them
         $read = gensym();
         open ($read, $readfile);
         push @OpenFiles, $read;
         return ($write, $read);
      }
      else {
         # Otherwise, just return the write handle
         return $write;
      }
   }
   else {
      warn "*WARNING: File could not be opened for writing: $file\n";
      return undef;
   }
}

# This will simply backup the file and notify the program that it may be modified
# The file is not opened
sub UseFile ($) {
   my $file = $_[0];
   BackupFile ($file);
   return 1;
}

###########################################
# File functions for internal use
###########################################

# This function will do file recovery if $Recover is set
# otherwise, it backs up a file and adds it to the list of files to
# close later on...
sub BackupFile ($) {
   my $file = $_[0];
   my $ret = "$file.bak";
   if (($Recover) and (-f $ret) ) {
      # Backup file exists, so keep it...
      debug 1, "Backup file $file.bak kept after abnormal program termination.";
      copy ($ret, $file);
      push @DataFiles, $ret;
   }
   else {
      if (-f $file) {
         if (copy ($file, $ret)) {
            debug 6, "$file backed up.";
            push @DataFiles, $ret;
         }
         else {
            warn "*WARNING: Can't backup $file to $file.bak!\n";
            $ret = "";
         }
      }
      else {
         # Create file...
         TouchFile($file);
         TouchFile($ret);
         debug 6, "$file created and backed up.";
         push @DataFiles, $ret;
      }
   }
   return $ret;
}

# This function should be called before the lockfile is removed. 
# This will close all known open files.
sub CloseFiles () {
   my $file;
   foreach $file (@OpenFiles) {
      close ($file);
   }
}


# This function should be called after all files have been
# closed and the lockfile has been removed.  It will remove the
# backup files.
sub FinishFiles () {
   my $file;
   foreach $file (@DataFiles) {
      unlink ($file) or warn "*WARNING: Couldn't remove backup file: $ret\n";;
   }
}


###########################################
# Other internal-use functions
###########################################

sub StartMail () {
   $MailFile = $Options{"$ProgName-DataDir"} . '/mailfile.tmp';
   TouchFile ($MailFile);
   if (open (MAILFILE, ">$MailFile")) {
      # Make sure we end the mail before we exit...
      push @ExecuteBeforeExit, \&EndMail;
      debug 6, "Opened up temporary file for a mailed report: $MailFile";
      return 1;
   }
   else {
      warn "*WARNING: Can't open temporary mailed report file: $MailFile.\nReport will NOT be mailed!\n";
      $MailFile = 'NULL';
      return 0;
   }
}

# Parameter not used
sub EndMail ($) {
   close (MAILFILE);
   `$MailProg -s "$ProgName on $HostName [$TimeStamp]" $Options{"$ProgName-MailTo"} < $MailFile`;
   debug 6, 'Mailed report to ' . $Options{"$ProgName-MailTo"};
   unlink ($MailFile);
   return 1;
}


###########################################
# General functions for main program to call...
###########################################

# This function looks in the plugin directory and loads all the plugins that are there
sub LoadPlugins () {
   opendir (PLUGINS, $PluginDir) or die
       "ERROR: Can't open plugin directory $PluginDir: $!\nThe plugin directory must exist (and you'll probably want some plugins) for the program to run\n";
   my $ThisFile;
   while ($ThisFile = readdir(PLUGINS)) {
      if (($ThisFile =~ /\.plugin(|\.pl)$/) and (-x "$PluginDir/$ThisFile")) {
         # The file ends in .plugin or .plugin.pl, so it's a plugin
         inform "Loading plugin: $ThisFile";
         require "$PluginDir/$ThisFile";
      }
   }
   closedir (PLUGINS);
   return 1;
}

# See the config.plugin.pl file's Restore function for
# details about how this works
sub GlobalRestore ($$$$) {
   my ($file, $string, $action, $actual) = @_;
   if ($string =~ /^New file/) {
      if ($actual) {
	 if ($action == 0) {
	    # Undo this change
	    system("rm -f $file");
	    return (1);
	 }
	 else {
	    return (-1);   # We can't redo this change
	 }
      }
      else {
	 if ($action == 0) {
	    # This is trivial to undo this new file
	    return (1);
	 }
	 else {
	    return (-1);   # We can't redo this change
	 }
      }
   }
   elsif ($string =~ /FILE deleted/) {
      if ($action == 0) {
	 # Undo this change
	 return (-1);   # We can't undo this change
      }
      else {
	 # Redo this change
	 if ($actual) {
	    # Actually do it...
	    system("rm -f $file");
	    return (1);
	 }
	 else {
	    # This is trivial to do...
	    return (1);
	 }
      }
   }
}

# There is one special plugin, named after the program that is contained in this main program file
# and is always loaded.  These are the commands that define and load this "plugin"
sub SetupGlobalPlugin() {
   PluginRegister ($ProgName, $ProgVersion, \&GlobalRestore,
                   "This $ProgName plugin is really the main program.  It is always loaded and exists in the main program and not in a seperate plugin file.");
   PluginRegisterOption ("$ProgName-MailTo", 'root', 'If you want a report mailed to somebody, set this variable to their email address.  If the --print command line option is used, however, then the report will be displayed rather than mailed.  If this option is not specified in the config file and --print is not given on the command line, the report will be discarded.');
   PluginRegisterOption ("$ProgName-DataDir", '/var/systracker/data', 'This is the directory in which the state database is stored.  It is also used for temporary files.  This directory is not required to restore a machine, but it is required to determine what has changed since the last run.  For security reasons, this directory should only be writable by root (and being only readable by root isn\'t a bad idea).');
   PluginRegisterOption ("$ProgName-Repository", '/var/systracker/repository', 'This is the directory in which the repository is stored.  For security reasons, this directory should only be readable and writable by root.  This directory contains the log of the changes on the system as well as any data needed to reproduce these changes.');
   return 1;
}

# Display the usage text and exit
sub Usage() {
   my $Prog = $0;
   $Prog =~ s=^.*/([^/]+)$=$1=;
   print "Usage: $Prog [--print] [--debug X] [--verbose] [--help] [--version]\n";
   print "[--config <filename>] [--plugindir <directory>]\n";
   print "  --init         Initialize the database and repository\n";
   print "  --test         Displays what files would be processed and in what way\n";
   print "                 but doesn't do anything.  Use for testing config files\n";
   print "  --restore      Enter Restore mode\n";
   print "  --sampleconfig Prints a sample config file to stdout\n";
   print "  --showdocs     Prints full program documentation to stdout\n";
   print "  --print        Prints the report to the screen instead\n";
   print "                 of mailing or discarding it.\n";
   print "  --debug X      Sets debug mode.  A number from 1 to 9 is optional.\n";
   print "                 If no number is specified the debug level is set to 5.\n";
   print "  --verbose      Make the report that is either printed or mailed more\n";
   print "                 verbose and detailed.  You probably only want to use\n";
   print "                 this when priting the report with --print above\n";
   print "  --help         Displays this help message and exits\n";
   print "  --version      Displays the version and date of release and exits\n";
   print "  --config <filename> Specify a non-default location for the config file\n";
   print "  --plugindir <dir>   Specify a non-default location for the plugin dir\n";
   exit 99;
}

sub PrintPar ($$$) {
   my ($text,$indent,$prefix) = @_;
   my $width = 76;
   my ($i, $line, $t, $len, $first);
   $first = 1;
   do {
      # Clear line
      $line = '';
      $len = $width;
      # Add indentation
      $line = $prefix;
      $len -= length($prefix);
      if ($first) {
	 $first = 0;
      }
      else {
	 for ($i = 0; $i < $indent; $i++) {
	    $line .= ' ';
	    $len--;
	 }
      }
      # Add words
      until ($len <= 0) {
	 if ( ($text =~ s/^(\s+\S+\s+)//) or ($text =~ s/^(\S+\s+)//) or ($text =~ s/^(\S+)//) ) {
	    $t = $1;
	    $i = $t;
	    $i =~ s/\s+$//;
	    if (($len-length($i)) >= 0) {
	       $len -= length($t);
	       $line .= $t;
	    }
	    else {
	       $text = $t . $text;
	       $len = 0;
	    }
	 }
	 else {
	    $len = 0;
	 }
      }
      $line =~ s/\s+$//;
      print "$line\n";
      
   } while ($text);
}

sub PrintDesc($) {
   my $prefix = $_[0];
   print "\n";
   PrintPar "Options:  These are options that control how the plugin works.  Each option has a default value and that value will be used if the option is not specified in the config file.  Options are set in the config file by starting the line with the option name, followed by white space, and then followed by the value.", 0, $prefix;
   print "\n";
   PrintPar "File Commands:  File Commands are used in the config file to tell $ProgName what files to operate on.  Each file command will match a certain set of files.  File commands are preceeded by either the word 'include' or 'exclude' in the config file.  They must be followed by an open and close parenthesis with any required arguments placed inside those parenthesis.", 0, $prefix;
   print "\n";
   PrintPar "Checks:  Any number of checks may be performed on any given file.  These are the various check types available.  Examples would be permissions checks and file type checks.  Checks are listed on the same line as the file commands.  They follow the close parenthesis for the file command and they specify which checks will or will not be done on the files that match that file command.", 0, $prefix;
   print "\n";
   PrintPar "Extra Commands:  These are extra commands that can be run during the execution of $ProgName.  They are placed in the config file on a line by themselves and they are followed by an open and close parenthesis with any required arguments inside those parenthesis.", 0, $prefix;
   print "\n";
}

sub DoShowDocs() {
   my ($plugin, $option, $command, $check);
   print "############################################################################\n";
   print "$ProgName $ProgVersion ($ProgDate)\n";
   print "############################################################################\n\n";
   PrintPar "Each plugin is documented below.  Each plugin may provide capabilities in any of the following four categories below", 0, "";
   PrintDesc('');
   
   # Document each plugin and the capabilities that it provides
   foreach $plugin (keys %Plugins) {
      print "\n############################################################################\n";
      print "Plugin: $plugin (Version " . $Plugins{$plugin}{'version'} . ")\n";
      print "############################################################################\n";
      PrintPar "\nDescription: " . $Plugins{$plugin}{'description'}, 13, "";
      print "\nOptions:\n" if (keys %{$Plugins{$plugin}{'options'}});
      foreach $option (keys %{$Plugins{$plugin}{'options'}}) {
	 print "  Option Name: $option\n";
	 print "  Default Val: " . $Options{$option} . "\n";;
	 PrintPar "  Description: " . $Plugins{$plugin}{'options'}{$option}, 15, "";
	 print "\n";
      }
      print "\nFile Commands:\n" if (keys %{$Plugins{$plugin}{'filecommands'}});
      foreach $command (keys %{$Plugins{$plugin}{'filecommands'}}) {
	 print "  Command Name: $command\n";
	 PrintPar "  Description: " . $Plugins{$plugin}{'filecommands'}{$command}, 15, "";
	 print "\n";
      }
      print "\nChecks:\n" if (keys %{$Plugins{$plugin}{'checktypes'}});
      foreach $check (keys %{$Plugins{$plugin}{'checktypes'}}) {
	 print "  Check Name : $check\n";
	 PrintPar "  Description: " . $Plugins{$plugin}{'checktypes'}{$check}, 15, "";
	 print "\n";
      }
      print "\nExtra Commands:\n" if (keys %{$Plugins{$plugin}{'commands'}});
      foreach $command (keys %{$Plugins{$plugin}{'commands'}}) {
	 print "  Command Name: $command\n";
	 PrintPar "  Description: " . $Plugins{$plugin}{'commands'}{$command}, 15, "";
	 print "\n";
      }
   }
}

sub DoSampleConfig() {
   my ($option, $plugin);
   print "############################################################################\n";
   print "# $ProgName Sample Config File\n";
   print "############################################################################\n\n";
   PrintPar "There are various items that can be in the config file:", 0, '# ';
   PrintDesc('# ');
   PrintPar 'Variables may be created by making a line that is Name=Value.  Value may also be a command enclosed in back-ticks (``) in which case the command enclosed in the back-tics will be executed and the output to stdout is substituted as the value of the variable.  Variables may be substituted in in the following manner: ${VariableName}.', 0, '# ';
   print "\n############################################################################\n";
   print "# Set Options\n";
   print "############################################################################\n\n";
   foreach $plugin (keys %Plugins) {
      foreach $option (keys %{$Plugins{$plugin}{'options'}}) {
	 PrintPar $Plugins{$plugin}{'options'}{$option}, 0, "# ";
	 print "$option " . $Options{$option} . "\n\n";
      }
   }
   print "\n############################################################################\n";
   print "# Run Optional Commands\n";
   print "############################################################################\n\n";
   PrintPar "You may run any of the following commands by placing them on a line by themselves followed by open and close parenthesis with optional variables inside", 0, "# ";
   print "\n";
   foreach $plugin (keys %Plugins) {
      foreach $command (keys %{$Plugins{$plugin}{'commands'}}) {
	 PrintPar "$command: " . $Plugins{$plugin}{'commands'}{$command}, length($command) + 2, "# ";
	 print "\n";
      }
   }
   print "\n############################################################################\n";
   print "# Define File List\n";
   print "############################################################################\n\n";
   PrintPar 'Here you must build your file list.  You can test your configuration by running the program with the --test option.  You use the words include or exclude, followed by whitespace, followed by a file command with the argument(s) in parethesis.  This is then followed by more whitespace, with the list of checks to be included or excluded from the matched files, with multiple checks separated by commas.', 0, '# ';
   print "\n";
   PrintPar 'NOTE:  A directory will not be recursed unless at has at least one check done on it.  So, if the root directory (/) doesn\'t have at least one type of check (i.e. Perms or Type) done on it, then absolutely no other file or directory will be checked as the / directory will not be recursed.', 0, '# ';
   print "\n# Available File Commands:\n# -----------------------\n\n";
   foreach $plugin (keys %Plugins) {
      foreach $command (keys %{$Plugins{$plugin}{'filecommands'}}) {
	 PrintPar "$command: " . $Plugins{$plugin}{'filecommands'}{$command}, length($command) + 2, "# ";
	 print "\n";
      }
   }
   print "\n# Available Checks:\n# ----------------\n\n";
   foreach $plugin (keys %Plugins) {
      foreach $command (keys %{$Plugins{$plugin}{'checktypes'}}) {
	 PrintPar "$command: " . $Plugins{$plugin}{'checktypes'}{$command}, length($command) + 2, "# ";
	 print "\n";
      }
   }
}

# Processes the command line arguments
sub ProcessCommandLine() {
   my ($OldDebug, $Version, $Usage);
   $OldDebug = $Debug;
   $Debug = -1;
   $Version = 0;
   $Usage = 0;
   GetOptions ( 'c|config=s' => \$ConfigFile,
                'p|print' => \$Print,
                'i|init' => \$Init,
                't|test' => \$Test,
                'd|debug:i' => \$Debug,
                'r|restore' => \$Restore,
                'plugindir=s' => \$PluginDir,
                'v|verbose' => \$Verbose,
                'showdocs' => \$ShowDocs,
                'sampleconfig' => \$SampleConfig,
                'h|?|help|usage' => \$Usage,
                'version' => \$Version
                ) or Usage();
   Usage() if $Usage;
   if ($Version) {
      print "$ProgName Version $ProgVersion ($ProgDate)\n$Copyright\n";
      exit 1;
   }
   if ($Debug == 0) {
      # If Debug equals zero, it means that they specified the debug option,
      # but did not provide the optional numerical argument, so set it to
      # a middle value of 5
      $Debug = 5;
   }
   elsif ($Debug == -1) {
      # If $Debug still is -1, it means it wasn't specified, so reset it to
      # the default
      $Debug = $OldDebug;
   }
   debug 1, "Debug Level $Debug Enabled";
   return 1;
}

# This procedure reads in the config file
sub ReadConfig() {
   my ($ThisLine, $Option, $Value, $LineNum, %Variables, $Variable, $Command, $Type, $Args, $Checks, $Check);
   $LineNum = 0;
   open CONFIG, $ConfigFile or die "ERROR: Can't open config file $ConfigFile: $!\n";
   while ($ThisLine = <CONFIG>) {
      $LineNum++;
      $ThisLine =~ s/\#.*$//;  # Get rid of comments

      # Do variable substitution
      while (($Variable) = ($ThisLine =~ /\${([^\}]+)}/)) {
         if (exists($Variables{$Variable})) {
            $ThisLine =~ s/\${[^\}]+}/$Variables{$Variable}/;
            debug 9, "Variable $Variable substituted on line $LineNum";
         }
         else {
            warn "*WARNING: Variable $Variable not defined but used on line $LineNum\n";
            $ThisLine =~ s/\${[^\}]+}//;
         }
      }

      $ThisLine =~ s/^\s+//;   # Get rid of leading white space
      $ThisLine =~ s/\s+$//;   # Get rid of trailing white space

      if ($ThisLine) {         # only continue if there is any text left
         if (($Command, $Args) = ($ThisLine =~ /^(\S+)\s*\((.*)\)$/)) {
            # A command in the form CommandName(options)
            if (exists($Commands{$Command})) {
               my $Com;
               $Com->{'command'} = $Command;
               $Com->{'args'} = $Args;
               push @SpecialCommands, $Com;
            }
            else {
               warn "*WARNING: Command $Command doesn't exist on line $LineNum\n";
            }
         }
         elsif (($Variable, $Value) = ($ThisLine =~ /^(\S+)\s*=\s*(.*)$/)) {
            # Any line in the form Variable = Value sets a variable in the config file
            if ($Value =~ s/^\`(.+)\`$/$1/) {
               # Optionally, Value can begin and end with a backtick (`) specifying that
               # a command is to be executed and the output is to become the value
               if ($Variables{$Variable} = `$Value 2>/dev/null`) {
                  chomp ($Variables{$Variable});
                  debug 8, "Variable $Variable set to $Value";
               }
               else {
                  warn "*WARNING: Couldn't set variable $Variable to the output of \`$Value\` on line $LineNum\n";
               }
            }
            else {
               $Variables{$Variable} = $Value;
               debug 8, "Variable $Variable set to $Value";
            }
         }
         elsif ( ($Type, $Command, $Args, $Checks) = ($ThisLine =~ /^(\+|\-|include|exclude)\s+(\S+)\s*\(\s*([^\)\s]*)\s*\)\s+(.+)$/)) {
            # File listing command...
            my $entry;
            if (exists($FileCommands{$Command})) {
               $Type = '+' if $Type eq 'include';
               $Type = '-' if $Type eq 'exclude';
               $entry->{'type'} = $Type;
               $entry->{'command'} = $Command;
               $entry->{'args'} = $Args;
               foreach $Check (split /,/, $Checks) {
                  if (exists($CheckTypes{$Check})) {
                     push @{$entry->{'checks'}}, $Check;
                  }     
                  else {
                     warn "*WARNING: Check type $Check doesn't exist on line $LineNum\n";
                  }
               }
               push @CommandList, $entry;
            }
            else {
               warn "*WARNING: File command $Command doesn't exist on line $LineNum\n";
            }
         }
         elsif (($Option, $Value) = ($ThisLine =~ /^(\S+)\s+(.*)$/)) {
            # Well, we must now assume the line is a configuration variable
            # Get the variable name and the value.  The name of the variable
            # must not contain any white space, and the value must be separated
            # from the variable name by some white space.
            ($Option, $Value) = ($ThisLine =~ /^(\S+)\s+(.*)$/);
            if (exists($Options{$Option})) {
               $Options{$Option} = $Value;
               debug 4, "Option set from config file: $Option = $Value";
            }
            else {
               warn "*WARNING: Invalid option in config file (Line $LineNum): $Option\n";
            }
         }
         else {
            warn "*WARNING: Line $LineNum in config file is not recognized\n";
         }
      }
   }
   close (CONFIG);
   return 1;
}


sub CleanupForExit () {
   my ($This);
   foreach $This (@ExecuteBeforeExit) {
      $This->("");
   }
}

# Here is how the locking works:
#     1) Lock file is created
#     2) Lock file is opened and exclusive lock obtained using flock
#     3) Program runs.  Files being modified are first backed up as *.bak
#     4) After entire program is complete, the lock file is closed and deleted
#     5) Backup files are deleted
#
# So, if the lock file already exists, but a lock can be obtained, then this means
# that the last run of the program did not exit normally.  So, all of the *.bak files
# need to be copied back over to their original locations.  
#
# If the lock file exists but a lock can't be obtained, it means another instance of
# this program is still running.  
#
# Any files to be modified by any plugin should be opened by calling OpenFile or UseFile.
# This will make the above system work.  If a plugin doesn't call OpenFile or UseFile whenever
# opening a file for writing, consistency might not be maintained.
sub LockProgram () {
   my $LockFile = $Options{"$ProgName-DataDir"} . '/lockfile';
   if (-f $LockFile) {
      # Lock file exists already... try to flock it...
      open (LOCKFILE, ">$LockFile") or die "ERROR: Lockfile ($LockFile) exists and is not writable\n";
      if (flock(LOCKFILE, LOCK_EX | LOCK_NB)) {
         # Okay, we got a lock, so this means that the last run of the program didn't
         # complete, as the lockfile is still there but not flock()ed.
         report "*WARNING: $ProgName did not exit cleanly on the last run.";
         warn "*WARNING: $ProgName did not exit cleanly on the last run.\n";
         $Recover = 1;
      }
      else {
         # We did not get the lock, which means the other process is still running
         die "ERROR: Another instance of $ProgName is currently running.\nWait for the process to finish or, if you think you received this\nmessage in error, you can delete $LockFile\nand proceed at your own risk.\n";
      }
   }
   else {
      # No lockfile there, so create it and then lock it
      open (LOCKFILE, ">$LockFile") or die "ERROR: Can't create lockfile: $LockFile\n";
      flock(LOCKFILE, LOCK_EX | LOCK_NB) or die "ERROR: There has been an unexpected locking error.\nThere may be another instance of $ProgName running.\n";
   }
   debug 2, "Exclusive lock obtained successfully ($LockFile).";
   return 1;
}

sub UnlockProgram () {
   my $LockFile = $Options{"$ProgName-DataDir"} . '/lockfile';
   # Close all open files.
   CloseFiles();

   # Close the system log
   StopLog();

   # Unlock program...
   flock(LOCKFILE, LOCK_UN);
   close (LOCKFILE);
   unlink ($LockFile);
   debug 2, "Exclusive lock released ($LockFile).";

   # Remove backup files...
   FinishFiles();

   if ($LogFile) {
      unlink ("$LogFile.new");
   }
   
   return 1;
}


###########################################
# Main Processing Loop
###########################################
# This builds the list of files and analyzes them
sub Execute () {
   my ($dbread, $dbwrite, $ThisLine, $file, $checks, $Check, $name, $value, $wrote);
   StartLog();
   # First thing - load database to compare against
   # The format of the file is like this:
   # /full/path/to/file|||CheckType(value)||CheckType(value)
   ($dbwrite, $dbread) = OpenFile ($Options{"$ProgName-DataDir"} . '/database');
   inform "Loading file database...";
   while ($ThisLine = <$dbread>) {
      chomp ($ThisLine);
      ($file, $checks) = split /\|\|\|/, $ThisLine;
      $db{$file}{'examined'} = 0;
      if ($checks) {
         foreach $Check (split /\|\|/, $checks) {
            ($name, $value) = ($Check =~ /(\S+)\((.*)\)/);
            $db{$file}{$name} = $value;
         }
      }
   }

   # Start processing from the root directory
   inform "Processing files...";
   ProcessDir ('/');

   unless ($Test) {
      # Now, write back the database to the file...
      # Also, check for files that have been deleted from the filesystem
      inform "Saving file database...";
      foreach $file (keys %db) {
	 if ($db{$file}{'examined'}) {
	    # The file was found and processed
	    # So, write back a new entry
	    print $dbwrite $file . '|';
	    $wrote = 0;
	    foreach $Check (keys %{$db{$file}}) {
	       unless ($Check eq 'examined') {
		  if ($db{$file}{$Check}) {
		     print $dbwrite '||' . $Check . '(' . $db{$file}{$Check} . ')';
		     $wrote = 1;
		  }
	       }
	    }
	    if ($wrote) {
	       print $dbwrite "\n";
	    }
	    else {
	       print $dbwrite "||\n";
	    }
	 }
	 else {
	    # The file was not processed.  It was either deleted, or 
	    # the file selection criteria have changed so that the file
	    # will no longer be checked.
	    unless (-e $file) {
	       # Well, the file is gone, so it was deleted
	       # record that fact in the log
	       unless (-l $file) {
		  LogEntry $ProgName, "FILE deleted", $file;
	       }
	    }
	 }
      }
   }
}

# This processes all the files in a given directory
# Parameters are:
#   1) directory to process
sub ProcessDir ($) {
   my $Dir = $_[0];
   my ($This, @DirList);
   # First, process the directory itself...
   # If that returns true, then look at the files in the directory
   if (ProcessEntry($Dir)) {
      opendir (DIR, $Dir);
      while ($This = readdir(DIR)) {
         unless (($This eq '.') or ($This eq '..')) {
            $This = $Dir . $This;
            if ((-d $This) and (not -l $This)) {
               $This .= '/';
               push @DirList, $This;
            }
            else {
               ProcessEntry($This);
            }
         }
      }
      closedir(DIR);
      # Now recurse into any directories
      foreach $This (@DirList) {
         ProcessDir($This);
      }
   }
}

sub ProcessEntry ($) {
   my ($ret, $CommandLine, $Check, %checks, $add, $type, $command, $args, @also, @chklst);
   my $file = $_[0];
   foreach $CommandLine (@CommandList) {
      $type = $ {%{$CommandLine}}{'type'};
      $command = $ {%{$CommandLine}}{'command'};
      $args = $ {%{$CommandLine}}{'args'};
      $ret = $FileCommands{$command}->($file, $args);
      if ($ret) {
         foreach $Check (@{$ {%{$CommandLine}}{'checks'}}) {
            if ($type eq '+') {
               $checks{$Check} = 1;
            }
            else {
               $checks{$Check} = 0;
            }
         }
      }
   }
   $ret = 0;
   foreach $Check (keys %checks) {
      if ($checks{$Check}) {
         push @chklst, $Check;
         $ret = 1;
      }
   }
   if ($Test) {
      #Testing mode... just say what checks would be run...
      my $string = "$file:";
      if (@chklst) {
	 while ($Check = pop @chklst) {
	    $string .= " $Check";
	 }
      }
      else {
	 if (-d  $file) {
	    $string .= " No Checks (directory and everything under it will be ignored)";
	 }
      }
      print "$string\n";
   }
   else {
      # See if it is a new file
      if (@chklst) {
	 unless (exists($db{$file})) {
	    LogEntry $ProgName, "New file: FILE", $file;
	 }
      }   
      while ($Check = pop @chklst) {
	 # Record the fact that this file was processed
	 $db{$file}{'examined'} = 1;
	 # Now call the check routines
	 ($db{$file}{$Check}, @also) = $CheckTypes{$Check}->($file, $db{$file}{$Check});
	 # If any new checks are returned, check them and add them to the processing list
	 foreach $Check (@also) {
	    unless ($checks{$Check}) {
	       # Wasn't already going to run...
	       if (exists($CheckTypes{$Check})) {
		  push @chklst, $Check;
	       }
	    }
	 }
      }
   }
   return ($ret);
}

sub RunSpecialCommands () {
   my $This;
   foreach $This (@SpecialCommands) {
      # Execute command, passing in arguments...
      debug 5, "Calling $Command with parameters: $Option";
      $Commands{$This->{'command'}}->($This->{'args'});
   }
}

# This applies the additions to the system logfile (i.e. audit trail)
sub StopLog () {
   if ($LogFile) {
      close (LOGFILE);
      TouchFile($LogFile);
      # Append new entries onto logfile
      system ("cat $LogFile.new >> $LogFile 2>/dev/null");
   }
}

# Calculates the time stamp for this run of the program
sub SetTimeStamp() {
   my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time);
   $mon++;
   if ($mon =~ /^\d$/) {
      $mon = '0' . $mon;
   }
   if ($mday =~ /^\d$/) {
      $mday = '0' . $mday;
   }
   if ($min =~ /^\d$/) {
      $min = '0' . $min;
   }
   if ($hour =~ /^\d$/) {
      $hour = '0' . $hour;
   }
   $year += 1900;
   $TimeStamp = "$mon/$mday/$year $hour:$min";
}

# This stores changes to be added to the system logfile
sub StartLog () {
   unless ($LogFile) {
      $LogFile = $Options{"$ProgName-Repository"} . '/log';
      TouchFile("$LogFile.new");
      open (LOGFILE, ">$LogFile.new") or die "Can't open log output file: $LogFile.new\n";
   }
}

# This will initialize the database and repository.
# All it really has to do is confirm that the initialization
# should take place, offer to save the repository somewhere else,
# and then wipe out the repository and the data directories.
sub DoInit () {
   # Some of the system commands called here and the parsing of the output is very GNU specific
   my $rep = $Options{"$ProgName-Repository"};
   my $OK = 0;
   my $read;
   $rep =~ s/\/$//;
   if (-f "$rep/log") {
      print "\nA repository already exists in $rep\n";
      print "If you choose to initialize the database and repository,\n";
      print "the current repository will be completely erased.  If you choose to\n";
      print "proceed, you will be given the option to backup the current repository\n";
      print "\nWould you like to re-initialize the database and repository? (y/n) ";
      $read = <STDIN>;
      if ($read =~ /^[yY]/) {
	 print "\nWould you like to move the existing repository to a new location? (y/n) ";
	 $read = <STDIN>;
	 if ($read =~ /^[yY]/) {
	    my @temp = `du -sk $rep`;
	    my $size = $temp[0];
	    $size =~ s/^(\d+)\s+\Q$rep\E$/$1/;
	    $size = $size / 1000;
	    $size =~ s/(\.\d)\d+/$1/;
	    print "\nPlease enter the directory to which the repository should be moved.\n";
	    print "The directory will be created if necessary.\n";
	    print "There must be $size MB available on the destination drive.\n";
	    print "\nEnter destination directory -> ";
	    $read = <STDIN>;
	    chomp ($read);
	    if ($read) {
	       print "Moving current repository... ";
	       system ("mkdir -p $read") and die "ERROR: Couldn't create directory: $read\n";
	       @temp = `df -k $read`;
	       my $newsize = $temp[1];
	       $newsize =~ s/^\S+\s+\d+\s+\d+\s+(\d+)\s+.*$/$1/;
	       $newsize = $newsize / 1000;
	       if ($newsize < $size) {
		  die "\n\nERROR: There is not enough space ($size MB) in $read to move repository\n";
	       }
	       # Okay, the directory has been created and the partition has enough space
	       system ("cp -ar $rep $read") and die "\n\nERROR: The repository was not moved\n";
	       print "Done.\n";
	       $OK = 1;
	    }
	 }
	 else {
	    $OK = 1;
	 }
      }
   }
   else {
      $OK = 1;
   }
   if ($OK) {
      my $data = $Options{"$ProgName-DataDir"};
      # It's okay to erase the repository and data directories
      print "\nThe following directories are going to be wiped out in order to initialize the\n";
      print "repository and data directory:\n";
      print "   Data Directory: $data\n";
      print "   Repository: $rep\n";
      print "\nIs this okay? (y/n) ";
      $read = <STDIN>;
      if ($read =~ /^[yY]/) {
	 print "\nDeleting data and repository... ";
	 system ("rm -rf $rep/* $data/*") and die "\n\nERROR: The contents of the directories couldn't be erased\n";
	 print "Done.\n";
	 LockProgram();
      }
      else {
	 $OK = 0;
      }
   }
   unless ($OK) {
      print "\nERROR: Initialization was not completed... exiting program.\n";
      UnlockProgram();
      exit 1;
   }
}

###########################################
# Restore Mode
###########################################

# This function has not been written yet.  It, in the future,
# will provide a way to retrieve the original version of a file.
# The first parameter is the full filename
# The second parameter is a 1 to actually get the file.  If the
#   second parameter is a zero, then this function needs to return
#   *if* the file could be retrieved.
# Return Value:
#   If the second parameter is a 0, returns 1 if the original
#      file can be retrieved, a 0 if it can't.
#   If the second parameter is a 1, the file should be retrieved
#      and a 1 should be returned on success, a 0 on failure.
sub GetOrigFile($$) {
   # Not implemented yet, always return 0.
   return 0;
}


# This function restores a file to a certain point.  The first
# parameter is the name of the file to restore.  The second 
# parameter is the time-stamp the file should be restored to
# (the file will be restored to its state at the end of that
# time stamp, not before) or an empty string to restore the
# file to its original state.  The third parameter is the list
# of log entries for the file...  it is a hash table with arrays
# at the hash entry for each timestamp.  Finally, the 4th parameter
# is the list of timestamps, in order.
sub RestoreFile($$$$) {
   my ($file, $stamp, $logentriesref, $stamplistref) = @_;
   my %logentries = %{$logentriesref};
   my @stamplist = @{$stamplistref};
   my ($thisstamp, $thislog, $thislog2, $thisplugin, $ret, $firstwithout);
   my ($backscore, $backpossible, $frontscore, $frontpossible, $foundstamp);
   if ($stamp eq "") {
      # Try restoring to original
      if (GetOrigFile($file, 1)) {
	 return 1;
      }
      else {
	 # Try backing up 
	 $backpossible = 1;
       BACKLOOP: foreach $thisstamp (reverse @stamplist) {
	  last BACKLOOP if ($thisstamp eq $stamp);
	  foreach $thislog (@{$logentries{$thisstamp}}) {
	     ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
	     $thislog2 =~ s/\Q[[$file]]\E/FILE/;
	     $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 0, 0);
	     debug 5, "Backup score for $thisplugin ($thislog2) is $ret";
	     if ($ret > 0) {
		$backscore += $ret;
	     }
	     elsif ($ret == 0) {
		$backpossible = 0;
		last BACKLOOP;
	     }
	  }
	  debug 5,"Backup score $backscore";
       }
	 if (($backpossible) and ($backscore)) {
	    debug 5, "Restoring by backing up";
	    foreach $thisstamp (reverse @stamplist) {
	       last if ($thisstamp eq $stamp);
	       foreach $thislog (@{$logentries{$thisstamp}}) {
		  ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
		  $thislog2 =~ s/\Q[[$file]]\E/FILE/;
		  $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 0, 1);
	       }
	    }
	 }
	 else {
	    return 0;
	 }
      }
   }
   else {
      # Try backing up first
      $backpossible = 1;
    BACKLOOP: foreach $thisstamp (reverse @stamplist) {
       last BACKLOOP if ($thisstamp eq $stamp);
       foreach $thislog (@{$logentries{$thisstamp}}) {
	  ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
	  $thislog2 =~ s/\Q[[$file]]\E/FILE/;
	  $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 0, 0);
	  debug 5, "Backup score for $thisplugin ($thislog2) is $ret";
	  if ($ret > 0) {
	     $backscore += $ret;
	  }
	  elsif ($ret == 0) {
	     $backpossible = 0;
	     last BACKLOOP;
	  }
       }
    }
      debug 5,"Backup score $backscore";
      # Find out score for going forwards
      $frontpossible = 1;
      $firstwithout = -1;
    FRONTLOOP: foreach $thisstamp (@stamplist) {
       last FRONTLOOP if (($foundstamp) and ($thisstamp ne $stamp));
       if ($thisstamp eq $stamp) {
	  $foundstamp = 1;
       }
       foreach $thislog (@{$logentries{$thisstamp}}) {
	  ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
	  $thislog2 =~ s/\Q[[$file]]\E/FILE/;
	  if ($firstwithout == -1) {
	     # First time through loop, so find out possibility with and without file existing
	     $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 1, 0);
	     $firstwithout = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 2, 0);
	  }
	  else {
	     $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 1, 0);
	  }
	  debug 5, "Forward score for $thisplugin ($thislog2) is $ret";
	  if ($ret > 0) {
	     $frontscore += $ret;
	  }
	  elsif ($ret == 0) {
	     $frontpossible = 0;
	     last FRONTLOOP;
	  }
       }
    }
      debug 5, "Forward score $frontscore";
      if (($frontpossible and $frontscore) and ($backpossible and $backscore) and ($firstwithout > 0)) {
	 # Going forwards and backwards is possible, without an existing file, so pick
	 # quickest method
	 if ($frontscore > $backscore) {
	    debug 5, "Restoring by backing up";
	    # Restore by backing up...
	    foreach $thisstamp (reverse @stamplist) {
	       last if ($thisstamp eq $stamp);
	       foreach $thislog (@{$logentries{$thisstamp}}) {
		  ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
		  $thislog2 =~ s/\Q[[$file]]\E/FILE/;
		  debug 5, "Calling restore function for $thisplugin ($thislog2)";
		  $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 0, 1);
	       }
	    }
	 }
	 else {
	    debug 5, "Restoring by going forward";
	    # Restore by going forward...
	    $foundstamp = 0;
	    foreach $thisstamp (@stamplist) {
	       last if (($foundstamp) and ($thisstamp ne $stamp));
	       if ($thisstamp eq $stamp) {
		  $foundstamp = 1;
	       }
	       foreach $thislog (@{$logentries{$thisstamp}}) {
		  ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
		  $thislog2 =~ s/\Q[[$file]]\E/FILE/;
		  $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 1, 1);
	       }
	    }
	 }
      }
      elsif ($backpossible and $backscore) {
	 # Backing up is possible, but going forward isn't possible without an existing file
	 # so, backing up is preferable
	 debug 5, "Restoring by backing up";
	 foreach $thisstamp (reverse @stamplist) {
	    last if ($thisstamp eq $stamp);
	    foreach $thislog (@{$logentries{$thisstamp}}) {
	       ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
	       $thislog2 =~ s/\Q[[$file]]\E/FILE/;
	       $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 0, 1);
	    }
	 }
      }
      elsif ($frontpossible and $frontscore) {
	 if ($firstwithout == 0) {
	    # we need to first get the file
	    unless (GetOrigFile($file, 1)) {
	       return 0;
	    }
	 }
	 $foundstamp = 0;
	 debug 5, "Restoring by going forward";
	 foreach $thisstamp (@stamplist) {
	    last if (($foundstamp) and ($thisstamp ne $stamp));
	    if ($thisstamp eq $stamp) {
	       $foundstamp = 1;
	    }
	    foreach $thislog (@{$logentries{$thisstamp}}) {
	       ($thisplugin, $thislog2) = ($thislog =~ /^([^\(]+)\([^ ]+\) (.+)$/);
	       $thislog2 =~ s/\Q[[$file]]\E/FILE/;
	       $ret = $Plugins{$thisplugin}{'restore'}->($file, $thislog2, 1, 1);
	    }
	 }
      }
      else {
	 # Apparently not possible to restore
	 return 0;
      }
   }
   return 1;
}

sub SelectRestoreFile() {
   $LogFile = $Options{"$ProgName-Repository"} . '/log';
   my ($this, %entries, $stamp, $entry, $oldstamp, @stamps);
   print "\nWhat filename do you want to restore?\n> ";
   my $file = <STDIN>;
   chomp ($file);
   $file =~ s/^\s+//;
   $file =~ s/\s+$//;
   print "\nChanges for $file:\n";
   open (LOG, $LogFile);
   while ($this = <LOG>) {
      chomp $this;
      if ($this =~ /\Q[[$file]]\E/) {
	 ($stamp, $entry) = ($this =~ /^\[(..\/..\/.... [1234567890:]+)\] (.+)$/);
	 push @ {$entries{$stamp}}, $entry;
	 $entry =~ s/\Q\[\[$file\]\]\E/$file/;
	 if ($oldstamp eq $stamp) {
	    print "  $entry\n";
	 }
	 else {
	    push @stamps, $stamp;
	    $oldstamp = $stamp;
	    print "Timestamp: $stamp:\n  $entry\n";
	 }
      }
   }
   close (LOG);
   print "\nYou must now select the point you want to restore to.\n";
   print "Type in the Timestamp exactly as it is displayed above.\n";
   print "The file will be restored to its state at the *end* of\n";
   print "timestamp.  If you want to restore the file to its original\n";
   print "state, leave the timestamp blank.\n";
   print "(i.e. MM/DD/YYYY HH:MM)\n> ";
   my $stamp = <STDIN>;
   chomp $stamp;
   if ((not $stamp) or ($entries{$stamp})) {
      unless (RestoreFile($file, $stamp, \%entries, \@stamps)) {
	 print "\n$file couldn't be restored...\n\n";
      }
   }
   else {
      print "\nBad timestamp\n";
   }
}

sub RestoreMode () {
   my $Done = 0;
   until ($Done) {
      print "\n\nRestore Mode\n------------\n\n";
      print "  1) Restore A File\n";
      print "  Q) Quit\n";
      print "\n> ";
      my $read = <STDIN>;
      chomp ($read);
      if (($read eq 'q') or ($read eq 'Q')) {
	 $Done = 1;
      }
      elsif ($read eq '1') {
	 SelectRestoreFile();
      }
      else {
	 print "\nInvalid Choice!\n";
      }
   }
}

###########################################
# Main Program Begins
###########################################

# Generate the time stamp to be used for this session
SetTimeStamp();

# First, process the command line options
ProcessCommandLine();

# We must register our own options.  They are registered
# under the "plugin" named the same as this program.  
SetupGlobalPlugin();

# Next thing to do is to load all the plugins
# The plugins will register themselves upon loading
LoadPlugins();

# Run special modes if necessary
if ($ShowDocs) {
   DoShowDocs();
   exit 0;
}
if ($SampleConfig) {
   DoSampleConfig();
   exit 0;
}

# Now, read the config file
ReadConfig();

# Lock the program to make sure only one copy is run at once
LockProgram();

# If they are initializing things, this DoInit() must be called
if ($Init) {
   DoInit();
}
elsif (not -f $Options{"$ProgName-DataDir"} . '/database') {
   # The database doesn't exist, so assume initialization 
   print "\nNo database exists... initializing...\n\n";
   $Init = 1;
}

if ($Restore) {
   # Enter restore mode
   RestoreMode();
}
else {
   unless ($Test) {
      # Run any special commands called from inside the config file
      RunSpecialCommands();
   }
   # Now it's time to build the file list to be analyzed
   # and do the analyzing
   Execute();
}

# Now we must clean things up before exit...
CleanupForExit();

# Unlock the program
UnlockProgram();

exit 0;

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