#!/usr/local/bin/perl

# New_log.pl
#   Preserves current logs by moving them and appending a date stamp.
#
#   Expected input:
#     A text file where the first line indicates a directory
#     and subsequent lines list patterns for target log files.
#   Prompts for a this configuration file if not specified.
#
#   Expected output:
#     Diagnostic messages
#
#
#   12/05/1997 Version 1.2.1
#   This script is free, public domain, no strings attached
#   Igor S. Livshits <mailto:igorl@ayradyss.org>


# Define some global variables
#
$true= (1==1);
$false= (1==0);
$newLine= "\n";
$space= " ";
$suffixDelimiter= ".";
$configFilePrompt= "Please specify your configuration file:";
$oldSubdirectory= "old/";	# where to dump old logs
$directoryPath= "DIRECTORY+PATH"; # reserved token
$controlFileTag= "rc";		# denotes a special control file for a process
$syslogTag= "syslog";		# denotes  log maintained via syslog
$syslogDaemon= "syslogd";	# the name of the syslogd process
$startCommand= "start";		# commands for the control file
$stopCommand= "stop";
@controlFileDirectories= ("/etc/rc2.d/", "/etc/rc3.d/");
$psCommand= "/usr/bin/ps -eaf -o pid,user,time,stime,comm |";


# Grab our input
#
$configFile= shift;		# Only grab the  first command line argument
$configFile= &AskForFilePath($configFilePrompt) unless $configFile;


# Find and rename specified files
#
%matchedPatterns= &RenameMatchingFiles($configFile);


# Appropriately signal affected processes
#
&SignalAffectedProcesses($matchedPatterns);


# Terminate gracefully
#
exit(0);


#
# Subroutines
#


# AskForFilePath($promptString)
#   Asks for a path to a file, returning it
#   
#   $promptString is just a string displayed to the user
#
sub AskForFilePath
{
  print "$newLine";
  print (shift);
  print "$newLine>";
  
  while (<STDIN>)
  {				# read a line from standard input
    chop;			# kill the terminating newline
    last if -e $_;		# make sure the file exists

    print $newLine, $newLine;
    print "The file you just specified does not seem to exist.$newLine";
    print "Please re-enter the path:$newLine";
    print ">";
  }

  return $_;			# return the file path
}


# RenameMatchingFiles($configFile)
#   Renames files in a given directory that match provided patterns
#
#   $configFile specifies the path to our input file
#
sub RenameMatchingFiles
{
  local($configFile)= shift;	# path to the configuration file
  local(%matchPatterns);	# patterns which specify target files
  local($targetDirectory);	# where the targte logs reside
  local($dateStamp);		# a unique suffix for renamed logs
  local(%matchedPatterns);	# patterns that were successfully matched
  local($logDirectory, @logs);	# path to and contents of the log directory
  local($matchPattern, $candidate);
  
  %matchPatterns= &ReadInput($configFile);
  $logDirectory= $matchPatterns{$directoryPath};
  undef($matchPatterns{$directoryPath});
 
  $dateStamp= &CurrentDateStamp();

  # List  our log directory
  opendir(LOGS, $logDirectory)
    || die "Could not open log directory <$logDirectory>.$newLine";
  @logs= readdir(LOGS);
  closedir(LOGS);

  # Find matches and rename them
  foreach $candidate (@logs)
  {
    next unless -f $logDirectory.$candidate; # Only check files
    foreach $matchPattern (keys(%matchPatterns))
    {				# See if this file matches our pattern
      if ($candidate=~ /$matchPattern/)
      {
	print "[$matchPattern]: ";
	if (rename($logDirectory.$candidate, 
		   $logDirectory.$oldSubdirectory.$candidate.$dateStamp))
	{
	  print "Successfully renamed <$logDirectory$candidate> to ",
	  "<$logDirectory$oldSubdirectory$candidate$dateStamp>.$newLine";
	  
	  open(DUMMY, ">$logDirectory$candidate")
	    || warn "Could not create empty <$logDirectory$candidate>.",
	    $newLine;
	  close(DUMMY);

	  $matchedPatterns{$matchPattern}= $matchPatterns{$matchPattern};
	}
	else
	{
	  print "Could not move <$logDirectory$candidate>...$newLine";
	}
	last;			# Only process each candidate once
      }
    }
  }
  return %matchedPatterns;
}


# ReadInput($inputFile)
#   Read lines of the input file into an array
#
#   $inputFile specifies the path to our input file
#
sub ReadInput
{
  local($inputFile)= shift;	# path to the input file
  local(%patterns);		# match patterns and signal hints
  local(@patternSpec);		# words of the pattern specification line

  open(CONFIG, $inputFile)
    || die "Could not open configuration file <$inputFile>.$newLine";

  # Grab the directory path from the first line
  $patterns{$directoryPath}= <CONFIG>;
  chop($patterns{$directoryPath});

  while (<CONFIG>)
  {				# process each subsequent line
    @patternSpec= split;
    $patterns{@patternSpec[0]}= @patternSpec[1];
  }

  close(CONFIG);
  return %patterns;
}


# CurrentDateStamp()
#   Returns a current date stamp as a string ".yymmdd"
#
sub CurrentDateStamp
{
  local($second, $minute, $hour, $day, $month, $year)= localtime(time());

  $month++;			# correct for 0 based count
                                # and pad single digits and incomplete years
  $month= "0".$month if ($month < 10);
  $day= "0".$day if ($day < 10);

  return $suffixDelimiter.$year.$month.$day;
}


# SignalAffectedProcesses($matchedPatterns)
#   Send a hangup signal (default) or kill and restart affected processes
#
#   %matchedPatterns lists process matching patterns and signal specification
#
sub SignalAffectedProcesses
{
  local($matchedPatterns)= shift; # patterns of the log files we moved
  local($pattern);		# a given match pattern
  local($currentFileHandle);	# just a place holder
  local($restartSyslogDaemon);	# if we troubled any logs maintained by syslogd

  $restartSyslogDaemon= $false;

  # Separate previous messages from upcoming
  print $newLine, "-=-=-=-", $newLine, $newLine;

  # Set up buffering in case we call system() below
  $currentFileHandle= select(STDOUT);
  $|= $true;
  select($currentFileHandle);

  # Dispatch restart duties
  foreach $pattern (keys(%matchedPatterns))
  {
    if ($matchedPatterns{$pattern} eq $controlFileTag)
    {
      &RestartViaControlFile($pattern);
    }
    elsif ($matchedPatterns{$pattern} eq $syslogTag)
    {
      $restartSyslogDaemon= $true;
    }
    else
    {
      &RestartViaHangupSignal($pattern);
    }
  }

  if ($restartSyslogDaemon)
  {				# Signal syslogd to notice new log files
    &RestartViaHangupSignal($syslogDaemon);
  }
}


# RestartViaControlFile($pattern)
#  Find an appropriate control file and restart the process
#
#  $pattern specifies how to match our process
#
sub RestartViaControlFile
{
  local($pattern)= shift;
  local($directory, @items, @candidates, $candidate);

  # First, find all possible control files
  foreach $directory (@controlFileDirectories)
  {				# search for a control file
    opendir(DIRECTORY, $directory) || next;
    @items= readdir(DIRECTORY);
    closedir(DIRECTORY);

    foreach $candidate (@items)
    {
      next unless -f $directory.$candidate; # only check files

      if ($candidate=~ /$pattern/)
      {				# found a matching control file
	push (@candidates, $directory.$candidate);
      }
    }
  }

  # Now, make sure it is unique and executable, and invoke it
  if (@candidates > 1)
  {				# ambiguous match pattern
    print "[$pattern]: More than one matching control files found. ",
    "Skipping...$newLine";
  }
  elsif (@candidates < 1)
  {				# make sure we have at least one match
    print "[$pattern]: No matching control files found. ",
    "Skipping...$newLine";
  }
  else
  {
    if (-x @candidates[0])
    {				# invoke it to stop then start our process
      print "[$pattern]: ";	# control file should write more :)
      system(@candidates[0], $stopCommand);
      system(@candidates[0], $startCommand);
    }
    else
    {				# cannot execute this file
      print "[$pattern]: Cannot execute @candidates[0]. ",
      "Skipping...$newLine";
    }
  }
}


# RestartViaHangupSignal($pattern)
#  Find an appropriate process and send it a hangup signal
#
#  $pattern specifies how to match our process
#
sub RestartViaHangupSignal
{
  local($pattern)= shift;	# will match our target process
  local($candidatePID, $result);
  local(@candidates);

  open(PROCESSES, $psCommand) || die "Cannot read current processes.";
  while (<PROCESSES>)
  {
    push(@candidates, $_) if (/$pattern/);
  }
  close(PROCESSES);

  if (@candidates)
  {
    if (@candidates > 1)
    {      
      print "[$pattern]: Multiple instances running! Skipping...$newLine";
    }
    else
    {
      $candidates[0]=~ s/^$space+//; # kill leading spaces
      ($candidatePID)= split($space, $candidates[0]);
      $result= kill 'HUP', $candidatePID;
      if ($result)
      {
	print "[$pattern]: Process #$candidatePID restarted.$newLine";
      }
      else
      {
	print "[$pattern]: Failed to restart process #$candidatePID.$newLine";
      }
    }
  }
  else
  {
    print "[$pattern]: No matching active processes.$newLine";
  }
}