#! /usr/bin/perl -w
# nagios: -epn
package Nagios::CheckLogfiles;

use strict;
use IO::File;
use File::Basename;
use Data::Dumper;
use Net::Domain qw(hostname hostdomain hostfqdn);
use Socket;
use POSIX qw(strftime);
use IPC::Open2;
use Encode qw(encode decode);


use constant GZIP => '/bin/gzip';
my $ERROR_OK = 0;
my $ERROR_WARNING = 1;
my $ERROR_CRITICAL = 2;
my $ERROR_UNKNOWN = 3;

our $ExitCode = $ERROR_OK;
our $ExitMsg = "OK";
my(%ERRORS, $TIMEOUT);
%ERRORS = ( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 );
$TIMEOUT = 60;

$| = 1;

sub new {
  my $class = shift;
  my $params = shift;
  my $self = bless {} , $class;
  return $self->init($params);
}

#
#  Read a hash with parameters
#
sub init {
  my $self = shift;
  my $params = shift;
  my($sec, $min, $hour, $mday, $mon, $year) = (localtime)[0, 1, 2, 3, 4, 5];
  $year += 1900; $mon += 1;
  $self->{tracefile} = $self->system_tempdir().'/check_logfiles.trace';
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  $self->{seekfilesdir} = $params->{seekfilesdir} || '/tmp';
  $self->{protocolsdir} = $params->{protocolsdir} || '/tmp';
  $self->{scriptpath} = $params->{scriptpath} || '/bin:/sbin:/usr/bin:/usr/sbin';
  $self->{protocolretention} = ($params->{protocolretention} || 7) * 24 * 3600;
  $self->{macros} = $params->{macros};
  $self->{timeout} = $params->{timeout};
  $self->{perfdata} = "";
  $self->{searches} = [];
  $self->{selectedsearches} = $params->{selectedsearches} || [];
  $self->{dynamictag} = $params->{dynamictag} || "";
  $self->{options} = { prescript => 1, smartprescript => 0,
      supersmartprescript => 0, postscript => 1, smartpostscript => 0,
      supersmartpostscript => 0 };
  if ($params->{cfgfile}) {
    if (ref($params->{cfgfile}) eq "ARRAY") {
      # multiple cfgfiles found in a config dir
      my @tmp_searches = ();
      $self->{cfgbase} = $params->{cfgbase} || "check_logfiles";
      foreach my $cfgfile (@{$params->{cfgfile}}) {
        $self->{cfgfile} = $cfgfile;
        if (! $self->init_from_file()) {
          return undef;
        }
        push(@tmp_searches, @{$self->{searches}});
        $self->{searches} = [];
      }
      my %seen = ();
      # newer searches replace searches with the same tag
      @tmp_searches = reverse map { 
        if (! exists $seen{$_->{tag}}) {
          $seen{$_->{tag}}++;
          $_;
        } else {
          ();
        }
      } reverse @tmp_searches;
      $self->{searches} = \@tmp_searches;
      my $uniqueseekfile = undef;
      my $uniqueprotocolfile = undef;
      foreach (@{$self->{searches}}) {
        $_->{cfgbase} = "check_logfiles";
        next if $_->{tag} eq "prescript";
        next if $_->{tag} eq "postscript";
        $_->construct_seekfile();
      }
      #$self->{cfgbase} = (split /\./, basename($params->{cfgfile}->[0]))[0];
      $self->{cfgbase} = "check_logfiles";
    } else {
      $self->{cfgfile} = $params->{cfgfile};
      $self->{cfgbase} = (split /\./, basename($self->{cfgfile}))[0];
      if (! $self->init_from_file()) {
        return undef;
      }
    } 
    # if there is a dynamictag parameter then replace template names with
    # template_dynamictagtag
    if (scalar(@{$self->{selectedsearches}})) {
      @{$self->{searches}} = map {
        my $srch = $_;
        if (grep {/^$srch->{tag}$/} @{$self->{selectedsearches}}) {
          $srch;
        } elsif (grep {/^$srch->{tag}$/} map { $_.'_'.$self->{dynamictag} } @{$self->{selectedsearches}}) {
          $srch;
        } elsif ($srch->{tag} eq "prescript") {
          $srch;
        } elsif ($srch->{tag} eq "postscript") {
          $srch;
        } else {
      	  $self->trace("skipping non-selected search %s", $srch->{tag});
          ();
        }
      } @{$self->{searches}};
    }
  } else {
    $self->{cfgbase} = $params->{cfgbase} || "check_logfiles";
    $self->init_macros;
    foreach (@{$params->{searches}}) {
      $_->{seekfilesdir} = $self->{seekfilesdir};
      $_->{scriptpath} = $self->{scriptpath};
      $_->{macros} = $self->{macros};
      $_->{tracefile} = $self->{tracefile};
      $_->{cfgbase} = $self->{cfgbase};
      if (my $search = Nagios::CheckLogfiles::Search->new($_)) {
        push(@{$self->{searches}}, $search);
      } else {
        $ExitCode = $ERROR_UNKNOWN;
        $ExitMsg = sprintf "cannot create %s search %s",
            $_->{type}, $_->{tag};
        return undef;
      }
    }  
  }
  $self->{protocolfile} = 
      sprintf "%s/%s.protocol-%04d-%02d-%02d-%02d-%02d-%02d",
      $self->{protocolsdir}, $self->{cfgbase}, 
      $year, $mon, $mday, $hour, $min, $sec;
  $self->{protocololdfiles} = sprintf "%s/%s.protocol-*-*-*-*-*-*",
      $self->{protocolsdir}, $self->{cfgbase};
  $self->{protocolfh} = new IO::File;
  $self->{protocolwritten} = 0;
  $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
  # if parameters update
  if (@{$self->{searches}}) {
    $self->{exitcode} = $ExitCode;
    $self->{exitmessage} = $ExitMsg;
    return $self;
  } else {
    $ExitCode = $ERROR_UNKNOWN;
    $ExitMsg = sprintf "UNKNOWN - configuration incomplete";
    return undef;
  }
}

sub init_from_file {
  my $self = shift;
  my $fullcfgfile;
  #
  #  variables from the config file.
  #
  our($seekfilesdir, $protocolsdir, $scriptpath, $protocolretention,
      $prescript, $prescriptparams ,$prescriptstdin, $prescriptdelay,
      $postscript, $postscriptparams, $postscriptstdin, $postscriptdelay,
      @searches, @logs, $tracefile, $options);
  our $MACROS = {};
  if (-f $self->{cfgfile}) {
    $fullcfgfile = $self->{cfgfile};
  } elsif (-f $self->{cfgfile}.'.cfg') {
    $fullcfgfile = $self->{cfgfile}.'.cfg';
  } elsif (-f $ENV{HOME}.'/'.$self->{cfgfile}) {
    $fullcfgfile = $ENV{HOME}.'/'.$self->{cfgfile};
  } elsif (-f $ENV{HOME}.'/'.$self->{cfgfile}.'.cfg') {
    $fullcfgfile = $ENV{HOME}.'/'.$self->{cfgfile}.'.cfg';
  } else {
    $ExitCode = $ERROR_UNKNOWN;
    $ExitMsg = sprintf "UNKNOWN - can not load configuration file %s", 
        $self->{cfgfile};
    return undef;
  }
  eval {
    require $fullcfgfile;
  };
  if ($@) {
    $ExitCode = $ERROR_UNKNOWN;
    $ExitMsg = sprintf "UNKNOWN - syntax error %s", (split(/\n/, $@))[0];
    return undef;
  }
  $self->{tracefile} = $tracefile if $tracefile;
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  $self->{cfgbase} = (split /\./, basename($self->{cfgfile}))[0];
  $self->{seekfilesdir} = $seekfilesdir if $seekfilesdir;
  $self->{protocolsdir} = $protocolsdir if $protocolsdir;
  $self->{scriptpath} = $scriptpath if $scriptpath;
  $self->{protocolretention} = $protocolretention if $protocolretention;
  $self->{prescript} = $prescript if $prescript;
  $self->{prescriptparams} = $prescriptparams if $prescriptparams;
  $self->{prescriptstdin} = $prescriptstdin if $prescriptstdin;
  $self->{prescriptdelay} = $prescriptdelay if $prescriptdelay;
  $self->{postscript} = $postscript if $postscript;
  $self->{postscriptparams} = $postscriptparams if $postscriptparams;
  $self->{postscriptstdin} = $postscriptstdin if $postscriptstdin;
  $self->{postscriptdelay} = $postscriptdelay if $postscriptdelay;
  $self->{macros} = $MACROS if $MACROS;
  $self->init_macros;
  $self->refresh_options($options);
  if (@logs) {
    #
    # Since version 1.4 the what/where-array is called @searches.
    # To stay compatible, @logs is still recognized.
    #
    @searches = @logs;
  }
  if ($self->{options}->{prescript}) {
    $_->{scriptpath} = $self->{scriptpath};
    $_->{macros} = $self->{macros};
    $_->{tracefile} = $self->{tracefile};
    $_->{cfgbase} = $self->{cfgbase};
    $_->{script} = $self->{prescript};
    $_->{scriptparams} = $self->{prescriptparams};
    $_->{scriptstdin} = $self->{prescriptstdin};
    $_->{scriptdelay} = $self->{prescriptdelay};   
    $_->{options} = sprintf "%s%sscript",
        $self->{options}->{supersmartprescript} ? "super" : "",
        $self->{options}->{smartprescript} ? "smart" : "";
    my $search = Nagios::CheckLogfiles::Search::Prescript->new($_);
    push(@{$self->{searches}}, $search); 
  }
  foreach (@searches) {
    $_->{seekfilesdir} = $self->{seekfilesdir};
    $_->{scriptpath} = $self->{scriptpath};
    $_->{macros} = $self->{macros};
    $_->{tracefile} = $self->{tracefile};
    $_->{cfgbase} = $self->{cfgbase};
    if ((exists $_->{template}) && ! $self->{dynamictag}) {
      # skip templates if they cannot be tagged
      next;
    }
    $_->{dynamictag} = $self->{dynamictag};
    if (my $search = Nagios::CheckLogfiles::Search->new($_)) {
      push(@{$self->{searches}}, $search);
    } else {
      $ExitCode = $ERROR_UNKNOWN;
      $ExitMsg = sprintf "cannot create %s search %s",
          $_->{type}, $_->{tag};
      return undef;
    }
  }
  if ($self->{options}->{postscript}) {
    $_->{scriptpath} = $self->{scriptpath};
    $_->{macros} = $self->{macros};
    $_->{tracefile} = $self->{tracefile};
    $_->{cfgbase} = $self->{cfgbase};
    $_->{script} = $self->{postscript};
    $_->{scriptparams} = $self->{postscriptparams};
    $_->{scriptstdin} = $self->{postscriptstdin};
    $_->{scriptdelay} = $self->{postscriptdelay};   
    $_->{options} = sprintf "%s%sscript",
        $self->{options}->{supersmartpostscript} ? "super" : "",
        $self->{options}->{smartpostscript} ? "smart" : "";
    my $search = Nagios::CheckLogfiles::Search::Postscript->new($_);
    push(@{$self->{searches}}, $search); 
  }
  return $self;
}

sub run {
  my $self = shift;
  foreach my $search (@{$self->{searches}}) {
    if (1) { # there will be a timesrunningout variable
      if ($search->{tag} eq "postscript") {
        $search->{macros}->{CL_SERVICESTATEID} = $self->{exitcode};
        $search->{macros}->{CL_SERVICEOUTPUT} = $self->{exitmessage};
        $search->{macros}->{CL_SERVICEPERFDATA} = $self->{perfdata};
        $search->{macros}->{CL_PROTOCOLFILE} = $self->{protocolfile};
        if ($search->{options}->{supersmartscript}) {
          # 
          #  Throw away evrything found so far. Supersmart postscripts
          #  have the last word.
          #
          $self->reset_result();        
        }     	
      }      
      $search->run();
      if (($search->{tag} eq "prescript") && 
          ($search->{options}->{supersmartscript}) &&
          ($search->{exitcode} > 0)) {
        #
        #  Prepare for a premature end. A failed supersmart prescript
        #  will abort the whole script.
        #
        $self->reset_result();
        $self->trace("failed supersmart prescript. aborting...");
      }
      if ($search->{options}->{protocol}) {
        if (scalar(@{$search->{matchlines}->{CRITICAL}}) ||
            scalar(@{$search->{matchlines}->{WARNING}}) ||
            scalar(@{$search->{matchlines}->{UNKNOWN}})) {
          if ($self->{protocolfh}->open($self->{protocolfile}, "a")) {
            foreach (qw(CRITICAL WARNING UNKNOWN)) {
              if (@{$search->{matchlines}->{$_}}) {
                $self->{protocolfh}->print(sprintf "%s Errors in %s (tag %s)\n",
                    $_, $search->{logbasename}, $search->{tag});
                foreach (@{$search->{matchlines}->{$_}}) {
                  $self->{protocolfh}->printf("%s\n", $_);
                }
              }
            }
            $self->{protocolfh}->close();
            $self->{protocolwritten} = 1;
          }
        }
      }
      if ($search->{options}->{count}) {
        foreach (qw(OK WARNING CRITICAL UNKNOWN)) {
          $self->{allerrors}->{$_} += scalar(@{$search->{matchlines}->{$_}});
          if ($search->{lastmsg}->{$_}) {
            $self->{lastmsg}->{$_} = $search->{lastmsg}->{$_};
          }
        }
      }
      $self->formulate_result();
      if (($search->{tag} eq "prescript") && 
          ($search->{options}->{supersmartscript}) &&
          ($search->{exitcode} > 0)) {
        #
        #  Failed supersmart prescript. I'm out...
        #
        last;
      } elsif (($search->{tag} eq "postscript") && 
          ($search->{options}->{supersmartscript})) {
        my $codestr = {reverse %ERRORS}->{$search->{exitcode}};
        ($self->{exitmessage}, $self->{perfdata}) = 
            split(/\|/, $search->{lastmsg}->{$codestr}, 2);
        $self->{exitcode} = $search->{exitcode};
      }
    }
  }
  $self->cleanup_protocols();
  return $self;
}

sub formulate_result {
  my $self = shift;
  #
  #  create the summary from all information collected so far
  #
  $self->{hint} = sprintf "(%s", join(", ", grep { $_ }
    ($self->{allerrors}->{CRITICAL} ? 
        sprintf "%d errors", $self->{allerrors}->{CRITICAL} : undef,
    $self->{allerrors}->{WARNING} ? 
        sprintf "%d warnings", $self->{allerrors}->{WARNING} : undef,
    $self->{allerrors}->{UNKNOWN} ? 
        sprintf "%d unknown", $self->{allerrors}->{UNKNOWN} : undef));
  if ($self->{protocolwritten}) {
    $self->{hint} .= sprintf " in %s)", basename($self->{protocolfile});
  } else {
    $self->{hint} .= ")";
  }
  foreach my $level qw(CRITICAL WARNING UNKNOWN OK) {
    $self->{exitcode} = $ERRORS{$level};
    if (($level ne "OK") && ($self->{allerrors}->{$level})) {
      $self->{exitmessage} = sprintf "%s - %s - %s %s", $level, $self->{hint},
          $self->{lastmsg}->{$level}, 
          ($self->{allerrors}->{$level} == 1 ? "" : "...");
      last;
    } else {
#      if ($self->{lastmsg}->{$level}) {
#        $self->{exitmessage} = sprintf "OK - %s", $self->{lastmsg}->{$level};
#      } else {
        $self->{exitmessage} = sprintf "OK - no errors or warnings";
#      }
    }
  }
#  $self->{exitmessage} .= " |".join (" ", 
#      map { $_->formulate_perfdata(); if ($_->{perfdata}) {$_->{perfdata}} else {()} }
#      @{$self->{searches}});
  $self->{perfdata} = join (" ", 
      map { $_->formulate_perfdata(); if ($_->{perfdata}) {$_->{perfdata}} else {()} }
      @{$self->{searches}});
}

sub reset_result {
  my $self = shift;
  $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
}

sub cleanup_protocols {
  my $self = shift;
  #
  #  cleanup old protocol files
  #
  #
  foreach my $oldprotocolfile (glob $self->{protocololdfiles}) {
    if ((stat $oldprotocolfile)[9] < (time - $self->{protocolretention})) {
      $self->trace("deleting old protocol %s", $oldprotocolfile);
      unlink $oldprotocolfile;
    }
  }
}

sub init_macros {
  my $self = shift;
  my($sec, $min, $hour, $mday, $mon, $year) = (localtime)[0, 1, 2, 3, 4, 5];
  my $cw = $^O =~ /MSWin/ ? 0 : 
      strftime("%V", $sec, $min, $hour, $mday, $mon, $year, -1, -1, -1);
  $year += 1900; $mon += 1;
  #
  #  Set default values for the built-in macros.
  #
  my $DEFAULTMACROS = {
      CL_USERNAME => defined(&Win32::LoginName) ?
          &Win32::LoginName() : scalar getpwuid $>,
      CL_HOSTNAME => hostname(),
      CL_DOMAIN => hostdomain(),
      CL_FQDN => hostfqdn(),
      CL_IPADDRESS => inet_ntoa(scalar gethostbyname(hostname())),
      CL_SERVICEDESC => $self->{cfgbase},
      CL_DATE_YYYY => sprintf("%04d", $year),
      CL_DATE_YY => substr($year,2,2),
      CL_DATE_MM => sprintf("%02d", $mon),
      CL_DATE_DD => sprintf("%02d", $mday),
      CL_DATE_HH => sprintf("%02d", $hour),
      CL_DATE_MI => sprintf("%02d", $min),
      CL_DATE_SS => sprintf("%02d", $sec),
      CL_DATE_TIMESTAMP => sprintf("%10d", time),
      CL_DATE_CW => sprintf("%02d", $cw),
      CL_NSCA_SERVICEDESC => $self->{cfgbase},
      CL_NSCA_HOST_ADDRESS => "127.0.0.1",
      CL_NSCA_PORT => 5667,
      CL_NSCA_TO_SEC => 10,
      CL_NSCA_CONFIG_FILE => "send_nsca.cfg",
  };
  #
  #  Add self-defined macros to the defaultmacros structure or overwrite
  #  already defined macros.
  #
  if ($self->{macros}) {
    foreach (keys %{$self->{macros}}) {
      $DEFAULTMACROS->{$_} = $self->{macros}->{$_};
    }
  }
  #
  #  Escape the most commonly used special characters so they will no longer
  #  be treated like special characters in a pattern.
  #
  $self->{macros} = $DEFAULTMACROS;
  return $self;
}

#
#  Resolve macros in a string. 
#  If a second parameter is given, then this string is meant as a regular expression.
#  Escape special characters accordingly.
#
sub resolve_macros {
  my $self = shift;
  my $pstring = shift;
  while ($$pstring =~ /.*\$(\w+)\$.*/g) {
    my $maybemacro = $1;
    if (exists $self->{macros}->{$maybemacro}) {
      my $macro = $self->{macros}->{$maybemacro};
      $$pstring =~ s/\$$maybemacro\$/$macro/;
    }
  }
}

sub resolve_macros_in_pattern {
  my $self = shift;
  my $pstring = shift;
  while ($$pstring =~ /.*\$(\w+)\$.*/g) {
    my $maybemacro = $1;
    if (exists $self->{macros}->{$maybemacro}) {
      my $macro = $self->{macros}->{$maybemacro};
   	  #
      #  Escape the most commonly used special characters so they will no longer
      #  be treated like special characters in a pattern.
      #
      $macro =~ s|/|\\/|g;
      $macro =~ s|\-|\\-|g;
      $macro =~ s|\.|\\.|g;
      $$pstring =~ s/\$$maybemacro\$/$macro/;
    }
  }
}

sub refresh_options {
  my $self = shift;
  my $options = shift;
  if ($options) {
    foreach my $option (split /,/, $options) {
      my $optarg = undef;
      $option =~ s/^\s+//;
      $option =~ s/\s+$//;
      if ($option =~ /(.*)=(.*)/) {
      	$option = $1;
      	$optarg = $2;
        $optarg =~ s/^"//;
        $optarg =~ s/"$//;
        $optarg =~ s/^'//;
        $optarg =~ s/'$//;
      }
      foreach my $defoption (keys %{$self->{options}}) {
        if ($option eq $defoption) {
          if ($optarg) {
          	# example: sticky=3600,syslogclient="winhost1.dom"
          	$self->{options}->{$defoption} = $optarg;
          } else {
            $self->{options}->{$defoption} = 1;
          }
        } elsif ($option eq 'no'.$defoption) {
          $self->{options}->{$defoption} = 0;
        }
      }
    } 
  } 
  # reset [smart][pre|post]script options if no script should be called 
  foreach my $option (qw(script prescript postscript)) {
    if (exists $self->{options}->{'supersmart'.$option}) {
      $self->{options}->{'smart'.$option} = 1 
          if $self->{options}->{'supersmart'.$option};
    }
    if (exists $self->{options}->{'smart'.$option}) {
      $self->{options}->{$option} = 1
          if $self->{options}->{'smart'.$option};
    }
    if (exists $self->{options}->{$option}) {
      if (($self->{options}->{$option}) && ! exists $self->{$option}) {
        $self->{options}->{$option} = 0;
        $self->{options}->{'smart'.$option} = 0;
        $self->{options}->{'supersmart'.$option} = 0;
      }
    }
  }
  if ($self->{options}->{sticky}) {
    if ($self->{options}->{sticky} > 1) {
      $self->{maxstickytime} = $self->{options}->{sticky};
      $self->{options}->{sticky} = 1;
    } else {
      $self->{maxstickytime} = 3600 * 24 * 365 * 10;
    }
  }
  if ($self->{options}->{syslogclient}) {
    $self->{prefilter} = $self->{options}->{syslogclient};
  }
}

sub trace {
  my $self = shift;
  my $format = shift;
  if ($self->{verbose}) {
    printf("%s: ", scalar localtime);
    printf($format, @_);
  }
  if ($self->{trace}) {
    my $logfh = new IO::File;
    $logfh->autoflush(1);
    if ($logfh->open($self->{tracefile}, "a")) {
      $logfh->printf("%s: ", scalar localtime);
      $logfh->printf($format, @_);
      $logfh->printf("\n");
      $logfh->close();
    }
  }
}

sub action {
  my $self = shift;
  my $script = shift;
  my $scriptparams = shift;
  my $scriptstdin = shift;
  my $scriptdelay = shift;
  my $smart = shift;
  my $success = 0;
  my $rc = 0;
  my $exitvalue;
  my $signalnum;
  my $dumpedcore;
  my $output;
  my $pid = 0;
  my $wait = 0;
  my $strerror = (qw(OK WARNING CRITICAL UNKNOWN))
      [$self->{macros}->{CL_SERVICESTATEID}];
  my $cmd;
  my @stdinformat = ();
  foreach my $macro (keys %{$self->{macros}}) {
    my $envmacro = $macro;
    if ($envmacro =~ /^CL_/) {
      $envmacro =~ s/^CL_/CHECK_LOGFILES_/;
    } else {
      $envmacro = "CHECK_LOGFILES_".$macro;
    }
    $ENV{$envmacro} = $self->{macros}->{$macro};
  }
  $ENV{CHECK_LOGFILES_SERVICESTATE} = qw(OK WARNING CRITICAL UNKNOWN)
      [$ENV{CHECK_LOGFILES_SERVICESTATEID}];
  if (ref $script eq "CODE") {
    $self->trace("script is of type %s", ref $script);
    if (ref($scriptparams) eq "ARRAY") {
      foreach (@{$scriptparams}) {
        $self->resolve_macros(\$_) if $_;
      }
    }
    my $stdoutvar;
    *SAVEOUT = *STDOUT;
    eval {
      open OUT ,'>',\$stdoutvar;
      *STDOUT = *OUT;
      $exitvalue = &{$script}($scriptparams, $scriptstdin);
    };
    *STDOUT = *SAVEOUT;
    if ($@) {
      $output = $@;
      $success = 0;
      $rc = -1;
      $self->trace("script said: %s", $output);
    } else {
      $output = $stdoutvar || "";
      chomp $output;
      $self->trace("script said: %s", $output);
      if ($smart) {
        if (($exitvalue =~ /^\d/) && ($exitvalue >= 0 && $exitvalue <= 3)) {
          $success = 1;
          $rc = $exitvalue;
          $self->trace("script %s exits with code %d", $script, $rc);
        } else {
          $success = 1;
          $rc = -4;
          $self->trace("script %s failed for unknown reasons", $script);
        }
      } else {
        $success = 1;
        $rc = $exitvalue;
        $output = $self->{macros}->{CL_SERVICEOUTPUT};
      }
    }
  } else {
    foreach my $dir (split(/:/, $self->{scriptpath})) {
      if ( -x $dir.'/'.$script ) {
        $self->trace(sprintf "found script in %s/%s", $dir, $script);
        $cmd = sprintf "%s/%s", $dir, $script;
        if ($^O =~ /MSWin/) {
          $cmd =~ s/\//\\/g;
        }
        last;
      }
    }
    if ($cmd) {
      if (defined $scriptparams) {
        $self->resolve_macros(\$scriptparams);
        $cmd = sprintf "%s %s", $cmd, $scriptparams;
      }
      $self->trace(sprintf "execute %s", $cmd);
      if (defined $scriptstdin) {
        my $pid = 0;
        my $wait = 0;
        my $maxlines = 100;
        if (! ref($scriptstdin eq "ARRAY")) {
        	$scriptstdin = [$scriptstdin];
        }
        foreach (@{$scriptstdin}) {
          $self->resolve_macros(\$_);
        }
        @stdinformat = @{$scriptstdin};
        #  if the format string was defined using single quotes, the escape
        #  characters must be expanded.
        $stdinformat[0] =~ s/\\t/\t/g;
        $stdinformat[0] =~ s/\\n/\n/g;
        $SIG{'PIPE'} = sub {};
        $SIG{'CHLD'} = sub {};
        my($chld_out, $chld_in);
        $pid = open2($chld_out, $chld_in, $cmd);
        $self->trace("stdin is <<EOF");
        $self->trace(@stdinformat);
        $self->trace("EOF");
        $chld_in->printf(@stdinformat);
        $chld_in->close();
        $output = $chld_out->getline() || "";
        while ($maxlines-- > 0) {
          # sucking the remaining output to avoid sigpipe
          $chld_out->getline() || last;
        }
        chomp $output;
        $chld_out->flush();
        $chld_out->close();
        $wait = waitpid $pid, 0;
        $exitvalue  = $? >> 8;
        $signalnum  = $? & 127;
        $dumpedcore = $? & 128;
        if (($signalnum == 13) && ($maxlines < 0)) {
          $signalnum = 0;
          # the script printed more than the allowed 100 lines of output.
          # closing the descriptor $chld_out caused a SIGPIPE which will
          # be accepted here.
        }
      } else {
        $output = (`$cmd`)[0] || "";
        $exitvalue  = $? >> 8;
        $signalnum  = $? & 127;
        $dumpedcore = $? & 128;
        chomp $output;
      }
      $self->trace("script said: %s", $output);
      if ($wait != $pid) {
        $success = 0;
        $rc = -5;
        $self->trace("wait %d != %d", $wait, $pid);
      } elsif ($signalnum) {
        $success = 0;
        $rc = -2;
        $self->trace("script %s received signal %d", $script, $signalnum);
        $self->trace("script %s exits with code %d", $script, $rc);
      } elsif ($dumpedcore) {
        $success = 0;
        $rc = -3;
        $self->trace("script %s failed with core dump", $script);
      } elsif ($smart) {
        if ($exitvalue >= 0 && $exitvalue <= 3) {
          $success = 1;
          $rc = $exitvalue;
          $self->trace("script %s exits with code %d", $script, $rc);
        } else {
          $success = 0;
          $rc = -4;
          $self->trace("script %s failed for unknown reasons", $script);
        }
      } else {
        $success = 1;
        $rc = $exitvalue;
        $output = $self->{macros}->{CL_SERVICEOUTPUT};
      }
    } else {
      $self->trace(sprintf "could not find %s", $script);
      $success = 0;
      $rc = -1;
    }
  }
  if ($scriptdelay) {
    $self->trace(sprintf "sleeping for %d seconds", $scriptdelay);
    sleep $scriptdelay;
  }
  map { /^CHECK_LOGFILES/ && delete $ENV{$_}; } keys %{$ENV};
  if($output) {
    # remove ticks in case the script was badly programmed
    # this is ugly and should be left to the scripts author
    $output =~ s/^"//;
    $output =~ s/"$//g;
  }
  return ($success, $rc, $output)
}


sub getfilefingerprint {
	use Encode qw(encode_utf8);
  my $self = shift;
  my $file = shift;
  if (-f $file) {
    if ($^O eq "MSWin32") {
      my $magic;
      if (ref $file) {
        my $pos = $file->tell();
        $file->seek(0, 0);
        $magic = $file->getline() || "this_was_an_empty_file";
        $file->seek(0, $pos);
      } else {
        my $fh = new IO::File;
        $fh->open($file, "r");
        $magic = $fh->getline() || "this_was_an_empty_file";
        $fh->close();
      }
      if ($self->{options}->{encoding}) {
        $magic =~ tr/\x80-\xFF//d;
        $magic =~ tr/\x00-\x1F//d;
      }
      $self->trace("magic: %s", $magic);
      #return(md5_base64($magic));
      return(unpack("H*", $magic));
      # use the creation time as unique identifier
      # haaaahaaaaaa win32 creation time is a good joke
      # google for "tunneling"
      return sprintf "0:%d", (stat $file)[10];
      #return "0:0";
    } else {
      return sprintf "%d:%d", (stat $file)[0], (stat $file)[1];
    }
  } else {
    return "0:0";
  }
}


sub getfilesize {
  my $self = shift;
  my $file = shift;
  return (-f $file) ? (stat $file)[7] : 0;
}

sub getfileisreadable {
  my $self = shift;
  my $file = shift;
  my $fh = new IO::File;
  if ($^O =~ /MSWin/) {
    if ($fh->open($file, "r")) {
      $fh->close();
      return 1;
    } else {
      return undef;
    }
  } elsif (($^O eq "linux") || ($^O eq "cygwin")) {
    if (! -r $file) {
      use filetest 'access';
      $self->trace("stat (%s) failed, try access instead", $file);
      return -r $file;
    }
    return -r $file;
  } else { 
    return -r $file;
  }
}

sub system_tempdir {
  my $self = shift;
  if ($^O =~ /MSWin/) {
    return $ENV{TEMP} if defined $ENV{TEMP};
    return $ENV{TMP} if defined $ENV{TMP};
    return File::Spec->catfile($ENV{windir}, 'Temp')
        if defined $ENV{windir};
    return 'C:\Temp';
  } else {
  	return "/tmp";
  }
}

sub decodestr {
  my $self = shift;
  my $encoding = shift;
  my $string = shift;
  if ($encoding eq "ucs-2") {
    return $self->unicode2ascii($self->ucs22unicode($string));
  }
}

sub ucs22unicode {
	my $self = shift;
	my $in = shift;
    #my @in = split /./, $string;
    my @out = ();

    my $i = 0;
    my $len = length ($in);
    printf "string is ((%s))\n", $in;
 printf "len is %s\n", $len;
    while ($i+1 < $len) {
	push (@out, ord (substr($in, $i, 1)) << 8 +
	      ord (substr($in, $i+1, 1)));

	$i ++;
    }
printf "out is ((%s))\n", join("",@out);
return join("",@out);
    return @out;
}

sub unicode2ascii {
  my $self = shift;
  my $text = shift;
  $text =~ s/0x00//gs;
  $text = unpack("C*", pack("U*", $text));
  return $text;
}


package Nagios::CheckLogfiles::Search;

use strict;
use Exporter;
use File::Basename;
use Digest::MD5 qw(md5_base64);
use POSIX qw(SSIZE_MAX);
use Unicode::Normalize;
use Encode;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles);

sub new {
  my $self = bless {}, shift;
  my $params = shift;
  $self->{tag} = $params->{tag} || 'default';
  $self->{template} = $params->{template} if $params->{template};
  $self->{dynamictag} = $params->{dynamictag} if $params->{dynamictag};
  if (exists $self->{template} && exists $self->{dynamictag}) {
    $self->{tag} = $self->{template}.'_'.$self->{dynamictag};
  } else {
    $self->{tag} = $params->{tag} || 'default';
  }
  $self->{type} = $params->{type};
  $self->{logfile} = $params->{logfile};
  $self->{rotation} = $params->{rotation};
  $self->{script} = $params->{script};
  $self->{scriptparams} = $params->{scriptparams};
  $self->{scriptstdin} = $params->{scriptstdin};
  $self->{scriptdelay} = $params->{scriptdelay};
  $self->{cfgbase} = $params->{cfgbase} || "check_logfiles";
  $self->{seekfilesdir} = $params->{seekfilesdir} || $self->system_tempdir();
  $self->{scriptpath} = $params->{scriptpath};
  $self->{macros} = $params->{macros};
  $self->{tracefile} = $params->{tracefile};
  $self->{prefilter} = $params->{prefilter};
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  if (! $self->{type}) {
    if ($self->{rotation}) {
      $self->{type} = "rotating";
    } else {
      $self->{type} = "simple";
    }
  }
  my $class = sprintf "Nagios::CheckLogfiles::Search::%s%s",
      uc substr($self->{type}, 0, 1), substr($self->{type}, 1);
  bless $self, $class;
  if (! $self->can("init")) {
  	#
  	#  Maybe $class was not defined in this file. Try to find 
  	#  the external module.
  	#
    my $module = $class.".pm";
    $module =~ s/::/\//g;
  	foreach (@INC) {
  	  if (-f $_."/$module") {
  	    require $module;
  	    bless $self, $class;
  	    last;
  	  }
  	}
  }
  if ($self->can("init")) {
    $self->init($params);
  } else {
    $self = undef;
  }
  return $self;
}

#
#  Read a hash with parameters
#
sub init {
  my $self = shift;
  my $params = shift;
  $self->{laststate} = {};
  $self->{options} = { script => 0, smartscript => 0, supersmartscript => 0,
      protocol => 1, count => 1, syslogserver => 0, logfilenocry => 1,
      perfdata => 1, case => 1, sticky => 0, syslogclient => 0,
      savethresholdcount => 1, encoding => 0 };
  $self->{relevantfiles} = [];
  $self->{preliminaryfilter} = { SKIP => [], NEED => [] };
  $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" };
  $self->{patterns} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{negpatterns} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{negpatterncnt} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{exceptions} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{threshold} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
  $self->{thresholdcnt} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
  $self->{hasinversepat} = 0;
  $self->{likeavirgin} = 0;
  $self->{linesread} = 0;
  $self->{perfdata} = "";
  $self->{max_readsize} = 1024 * 1024 * 128;
  # sysread can only read SSIZE_MAX bytes in one operation.
  # this is often (1024 * 1024 * 1024 * 2) - 1 = 2GB - 1
  # if we need to read from a non-seekable filehandle more than this
  # amount of data, then we have to perform multiple reads.
  # because the $bytes variable must hold the result of such a read and
  # its size is limited by available memory, it is divided by 16
  # so each read request does not overburden the sysread call and
  # does not inflate the process to more than 128MB
 
  # options
  $self->refresh_options($params->{options});
  #
  #  Dynamic logfile names may contain macros.
  #
  if (exists $self->{template} && exists $self->{dynamictag}) {
  	$self->{macros}->{CL_TAG} = $self->{dynamictag};
  	$self->{macros}->{CL_TEMPLATE} = $self->{template};
  } else {
    $self->resolve_macros(\$self->{tag});
    $self->{macros}->{CL_TAG} = $self->{tag};
  }
  $self->resolve_macros(\$self->{logfile});
  $self->{macros}->{CL_LOGFILE} = $self->{logfile};
  $self->{logbasename} = basename($self->{logfile});
  $self->{archivedir} = exists $params->{archivedir} ? $params->{archivedir} :
      dirname($self->{logfile});
  $self->resolve_macros(\$self->{archivedir});
  #
  #  Preliminary filter
  #
  if ($self->{prefilter}) {
    $self->resolve_macros_in_pattern(\$self->{prefilter});
    $self->addfilter(1, $self->{prefilter});
  }
  if ($self->{options}->{syslogserver}) {
  	my $syslogpattern = '($CL_HOSTNAME$|localhost)';
  	$self->resolve_macros_in_pattern(\$syslogpattern);
    $self->addfilter(1, $syslogpattern);
  }
  #
  #  Setup the structure describing what to search for.
  #
  foreach my $level qw(OK CRITICAL WARNING UNKNOWN) {
    #
    #  if a single pattern was given as a scalar, force it into an array
    #  and resolve macros.
    #
    if (exists $params->{(lc $level).'patterns'}) {
      if (ref($params->{(lc $level).'patterns'}) ne 'ARRAY') {
        $self->{patterns}->{$level} = [$params->{(lc $level).'patterns'}];
      } else {
        push(@{$self->{patterns}->{$level}}, @{$params->{(lc $level).'patterns'}});
      }
      foreach my $pattern (@{$self->{patterns}->{$level}}) {
        $self->resolve_macros_in_pattern(\$pattern);
      }

      if (exists $params->{(lc $level).'exceptions'}) {
        if (ref($params->{(lc $level).'exceptions'}) ne 'ARRAY') {
          $self->{exceptions}->{$level} = [$params->{(lc $level).'exceptions'}];
        } else {
          push(@{$self->{exceptions}->{$level}}, @{$params->{(lc $level).'exceptions'}});
        }
        foreach my $pattern (@{$self->{exceptions}->{$level}}) {
          $self->resolve_macros_in_pattern(\$pattern);
        }
      }
      #
      #  separate the pattern arrays. patterns beginning with a "!" will raise
      #  an error if they cannot be found.
      #  this type of pattern also needs a counter for the matches because after
      #  scanning the logfiles we must also check for a "not-found" condition.
      #
      @{$self->{negpatterns}->{$level}} = map {
        if (substr($_, 0, 1) eq "!") {
          push(@{$self->{negpatterncnt}->{$level}}, 0);
          substr($_, 1)
        } else { () }
      } @{$self->{patterns}->{$level}};
      if (scalar(@{$self->{negpatterns}->{$level}})) {
        $self->{hasinversepat} = 1;
        @{$self->{patterns}->{$level}} = map {
          if (substr($_, 0, 1) ne "!") { $_ } else { () }
        } @{$self->{patterns}->{$level}};
      }
      #
      #  prepend the patterns with (?i) if the case insensitivity option is set 
      #
      if (! $self->{options}->{case}) {
      	foreach my $pattern (@{$self->{patterns}->{$level}}) {
      	  $pattern = '(?i)'.$pattern;
      	}
      	foreach my $pattern (@{$self->{negpatterns}->{$level}}) {
      	  $pattern = '(?i)'.$pattern;
      	}
      	foreach my $pattern (@{$self->{exceptions}->{$level}}) {
      	  $pattern = '(?i)'.$pattern;
      	}
      }
      #
      #  ignore the match unless a minimum of threshold occurrances were found
      #
      if (exists $params->{(lc $level).'threshold'}) {
        $self->{threshold}->{$level} = $params->{(lc $level).'threshold'} - 1;
      } else {
        $self->{threshold}->{$level} = 0;
      }
    }
  }
  foreach my $level qw(CRITICAL WARNING UNKNOWN) {
    foreach my $pattern (@{$self->{negpatterns}->{$level}}) {
      push(@{$self->{negpatterncnt}->{$level}}, 0);
    }
  }
  $self->construct_seekfile();
  return $self;
}

sub construct_seekfile {
  my $self = shift;
  # since 2.0 the complete path to the logfile is mapped to the seekfilename
  $self->{seekfile} = $self->{logfile};
  $self->{seekfile} =~ s/\//_/g;
  $self->{seekfile} =~ s/\\/_/g;
  $self->{seekfile} =~ s/:/_/g;
  $self->{seekfile} =~ s/\s/_/g;
  $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{seekfile},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
  $self->{pre2seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{logbasename},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
}

sub force_cfgbase {
  # this is for the -F option. after initialization the seek/protocolfiles
  # must be reset to cfgbase of the base configfile is used
  my $self = shift;
  $self->{cfgbase} = shift;
  $self->construct_seekfile();
}


sub prepare {
  my $self = shift;
  return $self;
}


sub run {
  my $self = shift;
  $self->trace(sprintf "==================== %s ==================", $self->{logfile});
  $self->prepare();
  $self->loadstate();
  $self->analyze_situation();
  if ($self->{logrotated} || $self->{logmodified} || $self->{hasinversepat}) {
    # be lazy and examine files only if necessary
    $self->collectfiles();
  }
  if ($self->{hasinversepat} || scalar(@{$self->{relevantfiles}})) {
    $self->scan();
  } else {
    $self->trace("nothing to do");
    # $state keeps the old values
    foreach (keys %{$self->{laststate}}) {
      $self->{newstate}->{$_} = $self->{laststate}->{$_};
    }
    $self->trace("keeping %s", $self->{newstate}->{servicestateid});
  }
  $self->savestate();
  $self->formulate_perfdata();
}

=item loadstate()

    Load the last session's state. 
    The state is defined by
    - the position where the last search stopped
    - the time when the logfile was last touched then.
    - device and inode of the logfile (since version 1.4)
    If there is no state file, then this must be the first run of check_logfiles.
    In this case take the current file length as the stop position, so nothing will
    actually be done.
    
=cut
sub loadstate {
  my $self = shift;
  if (-f $self->{seekfile}) {
    $self->{likeavirgin} = 0;
    $self->trace(sprintf "found seekfile %s", $self->{seekfile});
    our $state = {};
    #eval {
      do $self->{seekfile};
    #};
    if ($@) {
      # found a seekfile with the old syntax
      $self->trace(sprintf "seekfile has old format %s", $@);
      my $seekfh = new IO::File;
      $seekfh->open($self->{seekfile}, "r");
      $self->{laststate} = {
          logoffset => $seekfh->getline() || 0,
          logtime => $seekfh->getline() || 0,
          devino => $seekfh->getline(),
          logfile => $self->{logfile},
      };
      chomp $self->{laststate}->{logoffset} if $self->{laststate}->{logoffset};
      chomp $self->{laststate}->{logtime} if $self->{laststate}->{logtime};
      chomp $self->{laststate}->{devino} if $self->{laststate}->{devino};
      $seekfh->close();
    } else {
      # found a new format seekfile
      $self->{laststate} = $state;
    }
    if (! $self->{laststate}->{logfile}) {
      $self->{laststate}->{logfile} = $self->{logfile};
    }
    if (! $self->{laststate}->{devino}) {
      # upgrade vom < 1.4 on the fly
      $self->{laststate}->{devino} = $self->getfilefingerprint($self->{logfile});
    }
    if (! $self->{laststate}->{servicestateid}) {
      $self->{laststate}->{servicestateid} = 0;
    }
    if (! $self->{laststate}->{serviceoutput}) {
      $self->{laststate}->{serviceoutput} = "OK";
    }
    foreach my $level qw(CRITICAL WARNING UNKNOWN) {
      if (exists $self->{laststate}->{thresholdcnt}->{$level}) {
        $self->{thresholdcnt}->{$level} =
            $self->{laststate}->{thresholdcnt}->{$level};
      } 
    }
    $self->trace("LS lastlogfile = %s", $self->{laststate}->{logfile});
    $self->trace("LS lastoffset = %u / lasttime = %d (%s) / inode = %s",
        $self->{laststate}->{logoffset}, $self->{laststate}->{logtime},
        scalar localtime($self->{laststate}->{logtime}),
        $self->{laststate}->{devino});
  } else {
    $self->trace("try seekfile %s instead", $self->{pre2seekfile});
    if (-f $self->{pre2seekfile}) {
      $self->trace("pre-2.0 seekfile %s found. rename it to %s", 
          $self->{pre2seekfile}, $self->{seekfile});
      rename $self->{pre2seekfile}, $self->{seekfile};
      $self->trace("and call load_state again");
      $self->loadstate();
      return $self;
    }
    $self->{likeavirgin} = 1;
    $self->trace("no seekfile %s found", $self->{seekfile});
    if (-e $self->{logfile}) {
    	$self->trace(sprintf "but logfile %s found", $self->{logfile});
      #  Fake a "the logfile was not touched" situation.
      $self->{laststate} = {
          logoffset => $self->getfilesize($self->{logfile}),
          logtime => (stat $self->{logfile})[10],
          devino => $self->getfilefingerprint($self->{logfile}),
          logfile => $self->{logfile},
          servicestateid => 0,
          serviceoutput => "OK",
      };
    } else {
    	$self->trace("and no logfile found");
      #  This is true virginity 
      $self->{laststate} = {
          logoffset => 0,
          logtime => 0,
          devino => "0:0",
          logfile => $self->{logfile},
          servicestateid => 0,
          serviceoutput => "OK",
      };
    }
    $self->trace("ILS lastlogfile = %s", $self->{laststate}->{logfile});
    $self->trace("ILS lastoffset = %u / lasttime = %d (%s) / inode = %s",
        $self->{laststate}->{logoffset}, $self->{laststate}->{logtime},
        scalar localtime($self->{laststate}->{logtime}), $self->{laststate}->{devino});
  }
  return $self;
}


=item savestate()

    Save a session's state. We need this for the next run of check_logfiles.
    Here we remember, how far we read the logfile, when it was last modified
    and what it's inode was.

=cut
sub savestate {
  my $self = shift;
  my $seekfh = new IO::File;
  $self->searchresult(); # calculate servicestateid and serviceoutput
  if ($self->{options}->{sticky}) {
    if ($self->{laststate}->{servicestateid}) {
      $self->trace("an error level of %s is sticking at me",
          $self->{laststate}->{servicestateid});
      $self->trace("and now i have %s",
          $self->{newstate}->{servicestateid});
      if ($self->{newstate}->{servicestateid}) {
        $self->{newstate}->{laststicked} = time;
        $self->trace("refresh laststicked");
        # dont forget to count the sticky error
        $self->addfirstevent($self->{laststate}->{servicestateid},
            $self->{laststate}->{serviceoutput});
        if (($self->{newstate}->{servicestateid} == 1) && 
            ($self->{laststate}->{servicestateid} == 2)) {
          # if this was a warning and we already have a sticky critical
          # save the critical as the sticky exitcode
          $self->{newstate}->{servicestateid} =
              $self->{laststate}->{servicestateid};
          # and keep the critical message as output
          $self->{newstate}->{serviceoutput} =
              $self->{laststate}->{serviceoutput};
        }
      } else {
        if ($self->{options}->{sticky} > 1) {
          # we had a stick error, then an ok pattern and no new error
          $self->trace("sticky error was resetted");
          $self->{newstate}->{laststicked} = 0;
          $self->{newstate}->{servicestateid} = 0;
          $self->{newstate}->{serviceoutput} = "";
        } else {
          # newstate is 0 because nothing happened in this scan
          # after maxstickytime do not carry on with this error.
          if ((time - $self->{laststate}->{laststicked}) >
              $self->{maxstickytime}) {
            $self->trace("maxstickytime %d expired", $self->{maxstickytime});
            $self->{newstate}->{laststicked} = 0;
            $self->{newstate}->{servicestateid} = 0;
            $self->{newstate}->{serviceoutput} = "";
          } else {
            $self->{newstate}->{laststicked} = 
                $self->{laststate}->{laststicked};
            $self->{newstate}->{servicestateid} = 
                $self->{laststate}->{servicestateid};
            $self->{newstate}->{serviceoutput} = 
                $self->{laststate}->{serviceoutput};
            $self->trace("stay sticky until %s", 
                scalar localtime ($self->{newstate}->{laststicked}
                + $self->{maxstickytime})); 
            $self->addevent($self->{newstate}->{servicestateid},
            $self->{newstate}->{serviceoutput});       	
          }  	  		
        }
      }
    } else {
      $self->trace("no sticky error from last run");
      if ($self->{newstate}->{servicestateid}) {
        $self->{newstate}->{laststicked} = time;
        $self->trace("stick until %s", 
            scalar localtime ($self->{newstate}->{laststicked} + 
            $self->{maxstickytime}));  	  
      }  		
    }
  }  
  # save threshold counts if a threshold exists for a level
  if ($self->{options}->{savethresholdcount}) {
    foreach my $level qw(CRITICAL WARNING UNKNOWN) {
      if ($self->{threshold}->{$level}) {
        $self->{newstate}->{thresholdcnt}->{$level} =
            $self->{thresholdcnt}->{$level};
      }
    } 
  }
  $self->{newstate}->{tag} = $self->{tag};
  if ($seekfh->open($self->{seekfile}, "w")) {
    my $dumpstate = Data::Dumper->new([$self->{newstate}], [qw(state)]);
    #printf("save %s\n", $dumpstate->Dump());
    $dumpstate = Data::Dumper->new([$self->{newstate}], [qw(state)]);
    $seekfh->printf("%s\n", $dumpstate->Dump());
    $seekfh->printf("\n1;\n");
    $seekfh->close();
    $self->trace("keeping position %u and time %d (%s) for inode %s in mind", 
        $self->{newstate}->{logoffset}, $self->{newstate}->{logtime},
        scalar localtime($self->{newstate}->{logtime}), 
        $self->{newstate}->{devino});
  } else {
    $self->{options}->{count} = 1;
    $self->addevent(WARNING, sprintf "cannot write status file %s",
    $self->{seekfile});
  }
  return $self;
}

sub formulate_perfdata {
  my $self = shift;
  if ($self->{options}->{perfdata}) { 
  	if (exists $self->{template} && $self->{dynamictag}) {
  	  $self->{perftag} = $self->{template};
  	} else {
  	  $self->{perftag} = $self->{tag};
  	}
    $self->{perfdata} = sprintf "%s_lines=%d %s_warnings=%d %s_criticals=%d %s_unknowns=%d",
        $self->{perftag}, $self->{linesread},
        $self->{perftag}, scalar(@{$self->{matchlines}->{WARNING}}),
        $self->{perftag}, scalar(@{$self->{matchlines}->{CRITICAL}}),
        $self->{perftag}, scalar(@{$self->{matchlines}->{UNKNOWN}});
  }
}

sub addevent {
  my $self = shift;
  my $level = shift;
  my $errormessage = shift;
  if ($level =~ /^\d/) {
    $level = (qw(OK WARNING CRITICAL UNKNOWN))[$level];
  }
  push(@{$self->{matchlines}->{$level}}, $errormessage);
  $self->{lastmsg}->{$level} =
      ${$self->{matchlines}->{$level}}[$#{$self->{matchlines}->{$level}}];
}

sub addfirstevent {
  my $self = shift;
  my $level = shift;
  my $errormessage = shift;
  if ($level =~ /^\d/) {
    $level = (qw(OK WARNING CRITICAL UNKNOWN))[$level];
  }
  unshift(@{$self->{matchlines}->{$level}}, $errormessage);
  $self->{lastmsg}->{$level} = 
      ${$self->{matchlines}->{$level}}[$#{$self->{matchlines}->{$level}}];
}

#
#  Read through all files found during analyze_situation and compare
#  the contents with patterns declared critical or warning or....
#
sub scan {
  my $self = shift;
  my $actionfailed = 0;
  my $resetted = 0;
  my $needfilter = scalar(@{$self->{preliminaryfilter}->{NEED}});
  my $skipfilter = scalar(@{$self->{preliminaryfilter}->{SKIP}});
  foreach my $logfile (@{$self->{relevantfiles}}) {
    $self->trace("moving to position %u in %s", $self->{laststate}->{logoffset},
        $logfile->{filename});
    if ($logfile->{seekable}) {
      $logfile->{fh}->seek($self->{laststate}->{logoffset}, 0);
    } else {
      my $buf;
      my $needtoread;
      $logfile->{offset} = 0;
      if ($self->{laststate}->{logoffset} > $self->{max_readsize}) {
        $needtoread = $self->{max_readsize};
        $self->trace("i cannot sysread %u bytes. begin with %u bytes",
            $self->{laststate}->{logoffset}, $needtoread);
      } else {
        $needtoread = $self->{laststate}->{logoffset};
      }
      while ($logfile->{offset} < $self->{laststate}->{logoffset}) {
        $self->trace("i start at offset %u", $logfile->{offset});
        my $bytes = $logfile->{fh}->sysread($buf, $needtoread);
        if (! defined $bytes) {
          $self->trace("read error at position %u", $logfile->{offset});
          last;
        } elsif ($bytes == 0) {
          # this should not happen, but at least it is an exit 
          # from an endless loop.
          $self->trace("i read %d bytes. looks like EOF at position %u",
              $bytes, $logfile->{offset});
          last;
        } else {
          $self->trace("i read %d bytes", $bytes);
          $logfile->{offset} += $bytes;
          if (($self->{laststate}->{logoffset} - $logfile->{offset}) >
              $self->{max_readsize}) {
            $needtoread = $self->{max_readsize};
            $self->trace("i cannot sysread %u bytes. continue with %u bytes",
                $self->{laststate}->{logoffset} - $logfile->{offset},
                $needtoread);
          } else {
            $needtoread = $self->{laststate}->{logoffset} - $logfile->{offset};
            $self->trace("i will sysread %u bytes.", $needtoread);
          }
        }
      }
      $self->trace("fake seek positioned at offset %u", $logfile->{offset});
    }
    while (my $line = $logfile->{fh}->getline()) {
      my $filteredout = 0;
      $self->{linesread}++;
      if (! $logfile->{seekable}) { $logfile->{offset} += length($line) }
      if ($self->{options}->{encoding}) {
      	# i am sure this is completely unreliable
      	$line = encode("ascii", decode($self->{options}->{encoding}, $line));
      }
      chomp($line);
      #
      #  If for example the prefilter option was set, check if the line 
      #  needs to be further examined. Only lines which match the needed filter
      #  can pass.
      #
      if ($needfilter) {
      	foreach my $filter (@{$self->{preliminaryfilter}->{NEED}}) {
      	  if ($line !~ /$filter/) {
      	  	$self->trace(sprintf "no need for %s", $line);
      	  	$filteredout = 1;
      	  	last;
      	  }
      	}
      }
      #
      #  Skip lines with blacklist patterns
      #
      if ($skipfilter) {
      	foreach my $filter (@{$self->{preliminaryfilter}->{SKIP}}) {
      	  if ($line =~ /$filter/) {
      	  	$self->trace(sprintf "skip unwanted %s", $line);
      	  	$self->trace(sprintf "because matching %s", $filter);
      	  	$filteredout = 1;
      	  	last;
      	  }
      	}      	
      }
      next if $filteredout;
      foreach my $level qw(CRITICAL WARNING UNKNOWN) {
        my $outplayed = 0;
        foreach my $exception (@{$self->{exceptions}->{$level}}) {
          if ($line =~ /$exception/) {
            $self->trace("exception %s found. aborting.", $exception);
            $outplayed = 1;
            last;
          }
        }
        next if $outplayed;
        my $patcnt = -1;
        foreach my $pattern (@{$self->{patterns}->{$level}}) {          
          $patcnt++;
          if ($line =~ /$pattern/) {
            $self->trace("MATCH %s %s with %s", $level, $pattern, $line);
            if ($self->{threshold}->{$level}) {
              if ($self->{thresholdcnt}->{$level} < 
                  $self->{threshold}->{$level}) {
                $self->trace("skip match and the next %d",
                    $self->{threshold}->{$level} - 
                    $self->{thresholdcnt}->{$level});
                $self->{thresholdcnt}->{$level}++;
                next;
              } else {
                $self->{thresholdcnt}->{$level} = 0;
              }
            }
            if ($self->{options}->{script}) {
              $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{$level};
              $self->{macros}->{CL_SERVICEOUTPUT} = $line;
              $self->{macros}->{CL_PATTERN_NUMBER} = $patcnt;
              my ($actionsuccess, $actionrc, $actionoutput) =
                  $self->action($self->{script}, $self->{scriptparams},
                  $self->{scriptstdin}, $self->{scriptdelay},
                  $self->{options}->{smartscript});
              if (! $actionsuccess) {
              	# note the script failure. multiple failures will generate
              	# one single event in the end.
                $actionfailed = 1;
                $self->addevent($level, $line);
              } elsif ($self->{options}->{supersmartscript}) {
              	# completely replace the matched line with the script output
                $self->addevent($actionrc, $actionoutput);                 
              } elsif ($self->{options}->{smartscript}) {
              	# both matched line and script output are events
                $self->addevent($level, $line);
                $self->addevent($actionrc, $actionoutput);
              } else {
              	# dumb scripts generate no events. only the matched line.
                $self->addevent($level, $line);
              }
            } else {
              $self->addevent($level, $line);
            }
          }
        }
        #  count patterns which raise an alert only if they were not found.
        $patcnt = -1;
        foreach my $pattern (@{$self->{negpatterns}->{$level}}) {
          $patcnt++;
          if ($line =~ /$pattern/) {
            $self->{negpatterncnt}->{$level}->[$patcnt]++;
            $self->trace("negative pattern %s found.", $pattern);
          }
        }
      }
      # maybe a okpattern wipes out the history
      foreach my $pattern (@{$self->{patterns}->{OK}}) {          
        if ($line =~ /$pattern/) {
          $self->trace("remedy pattern %s wipes out previous errors",
              $pattern);
          $self->{options}->{sticky}++ if $self->{options}->{sticky};
          # such a remedypattern neutralizes previous error
          $self->{matchlines}->{WARNING} = [];
          $self->{matchlines}->{CRITICAL} = [];
          $self->{matchlines}->{UNKNOWN} = [];
          last;
        }
      }   
    }
    #
    #  if there are more files to come, start searching at the beginning
    #  of each file.
    #  only the first (oldest) file will be positioned at an offset.
    #
    $self->{laststate}->{logoffset} = 0;
    $self->{newstate}->{logoffset} = $logfile->{seekable} ?
        $logfile->{fh}->tell() : $logfile->{offset};
    $self->{newstate}->{logtime} = (stat $logfile->{fh})[9];
    $self->{newstate}->{devino} = $self->getfilefingerprint($logfile->{fh});
    $self->trace("stopped reading at position %u",
        $self->{newstate}->{logoffset});
  }
  #
  #  if patterns beginning with ! were not found, treat this as an alert.
  #
  if ($self->{hasinversepat}) {
    foreach my $level qw(CRITICAL WARNING) {
      my $patcnt = -1;
      foreach my $pattern (@{$self->{negpatterns}->{$level}}) {
        $patcnt++;
        if ($self->{negpatterncnt}->{$level}->[$patcnt] == 0) {
          if ($self->{options}->{script}) {
            $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{$level};
            $self->{macros}->{CL_SERVICEOUTPUT} = sprintf("MISSING: %s", $pattern);
            $self->{macros}->{CL_PATTERN_NUMBER} = $patcnt;
            my ($actionsuccess, $actionrc, $actionoutput) =
                $self->action($self->{script}, $self->{scriptparams},
                $self->{scriptstdin}, $self->{scriptdelay},
                $self->{options}->{smartscript});
            if (! $actionsuccess) {
              $actionfailed = 1;
      	      $self->addevent($level, sprintf("MISSING: %s", $pattern));
            } elsif ($self->{options}->{supersmartscript}) {
              $self->addevent($actionrc, $actionoutput);
            } elsif ($self->{options}->{smartscript}) {
              $self->addevent($level, sprintf("MISSING: %s", $pattern));
              $self->addevent($actionrc, $actionoutput);
            } else {
              $self->addevent($level, sprintf("MISSING: %s", $pattern));
            }
          } else {
      	    $self->addevent($level, sprintf("MISSING: %s", $pattern));
          }
        }
      }
    }
    #
    #  no files were examined, so no positioning took place. 
    #  keep the old status.
    #
    if (scalar @{$self->{relevantfiles}} == 0) {
      $self->{newstate}->{logoffset} = $self->{laststate}->{logoffset};
      $self->{newstate}->{logtime} = $self->{laststate}->{logtime};
      $self->{newstate}->{devino} = $self->{laststate}->{devino};
    }
  }
  #
  #  now the heavy work is done. logfiles were searched and matching lines
  #  were found and noted.
  #  close the open file handles and store the current position in a seekfile.
  #
  foreach my $logfile (@{$self->{relevantfiles}}) {
    $logfile->{fh}->close();
  }
  if ((scalar @{$self->{relevantfiles}} > 0) && ($self->{logfile} ne
      @{$self->{relevantfiles}}[$#{$self->{relevantfiles}}]->{filename})) {
    #
    #  only rotated files were examined and a new logfile was not created yet.
    #  next time we hopefully will have a new logfile, so start at position 0.
    #  set the lastlogtime to now, and don't care no longer for the past.
    #
    $self->trace("rotated logfiles examined but no current logfile found");
    $self->{newstate}->{logoffset} = 0;
    $self->{newstate}->{logtime} = time;
  }
  if ($actionfailed) {
    $self->{options}->{count} = 1;
    push(@{$self->{matchlines}->{WARNING}},
        sprintf "could not execute %s", $self->{script});
  }
}

sub addfilter {
  my $self = shift;
  my $need = shift;
  my $pattern = shift;
  if ($need) {
    push(@{$self->{preliminaryfilter}->{NEED}}, $pattern);	
  } else {
    push(@{$self->{preliminaryfilter}->{SKIP}}, $pattern);   	
  }
}

sub searchresult {
  my $self = shift;
  if (scalar @{$self->{matchlines}->{CRITICAL}}) {
  	$self->{newstate}->{servicestateid} = 2;
  	$self->{newstate}->{serviceoutput} = 
  	    ${$self->{matchlines}->{CRITICAL}}[$#{$self->{matchlines}->{CRITICAL}}];
  } elsif (scalar @{$self->{matchlines}->{WARNING}}) {
  	$self->{newstate}->{servicestateid} = 1;
  	$self->{newstate}->{serviceoutput} = 
  	    ${$self->{matchlines}->{WARNING}}[$#{$self->{matchlines}->{WARNING}}];
  } elsif (scalar @{$self->{matchlines}->{UNKNOWN}}) {
    $self->{newstate}->{servicestateid} = 3;
  	$self->{newstate}->{serviceoutput} = 
  	    ${$self->{matchlines}->{UNKNOWN}}[$#{$self->{matchlines}->{UNKNOWN}}];
  } else {
  	$self->{newstate}->{servicestateid} = 0;
  	$self->{newstate}->{serviceoutput} = "";
  }
}



package Nagios::CheckLogfiles::Search::Simple;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub analyze_situation {
  my $self = shift;
  $self->{logrotated} = 0;
  $self->{logmodified} = 0;
  
  if (! -e $self->{logfile}) {
    #
    #  the logfile was deleted and no new events occurred since.
    #  todo: no collection, but reset counters, incl. timestamp
    #  with the modified flag we force a call to collectfiles where 
    #  [no]logfilenocry will be considered.
    $self->{logmodified} = 1;
    $self->trace(sprintf "there is no logfile %s at this moment",
        $self->{logfile});
    $self->{laststate}->{logoffset} = 0;
  } elsif (! $self->getfileisreadable($self->{logfile})) {
    $self->{logmodified} = 1;
    $self->trace(sprintf "first noticed that logfile %s is unreadable",
        $self->{logfile});
  } elsif ($self->{laststate}->{devino} ne 
        $self->getfilefingerprint($self->{logfile})) {
    # the inode changed (! the old inode could have been reused)
    # or maybe this is the first time this logfile was seen
    $self->trace(sprintf "this is not the same logfile %s %s != %s",
        $self->{logfile}, $self->{laststate}->{devino},
        $self->getfilefingerprint($self->{logfile}));
    $self->{logmodified} = 1;
    $self->{laststate}->{logoffset} = 0;
    $self->trace(sprintf "reset to offset 0");
  } elsif ($self->getfilesize($self->{logfile}) > 
        $self->{laststate}->{logoffset}) {
    #
    #  the logfile grew.
    #  this is the normal behaviour. in rare cases the logfile could have been
    #  rotated/recreated and grown very fast.
    $self->trace(sprintf "the logfile grew to %d",
        $self->getfilesize($self->{logfile}));
    if ($self->{likeavirgin}) {
      # if the logfile grew because we initialized the plugin with an offset of 0, position
      # at the end of the file and skip this search. otherwise lots of outdated messages could
      # match and raise alerts.
      $self->{laststate}->{logoffset} = $self->getfilesize($self->{logfile});
    } else {
      $self->{logmodified} = 1;
      # this is only relevant if
      # 1.a new logfile is created using the same inode as the deleted logfile
      # 2.the new logfile grew beyond the size of the last logfile before check_logfile ran
      #if ($self->{firstline} ne $self->{laststate}->{firstline}) {
      #	$self->trace(sprintf "a new logfile grew beyond the end of the last logfile");
      #	$self->{laststate}->{logoffset} = 0;
      #}
    }
  } elsif ($self->getfilesize($self->{logfile}) == 0) {
  	#
  	#  the logfile was either truncated or deleted and touched.
  	#  nothing to do except reset the position
    $self->{logmodified} = 0;
    $self->{laststate}->{logoffset} = 0;  
    $self->{laststate}->{logtime} = (stat $self->{logfile})[9];
    $self->trace("logfile has been truncated");
  } elsif ($self->getfilesize($self->{logfile}) < 
        $self->{laststate}->{logoffset}) {
    #
    #  logfile shrunk. either it was truncated or it was
    #  rotated and a new logfile was created.
    $self->trace(sprintf "the logfile shrunk from %d to %d",
        $self->{laststate}->{logoffset}, $self->getfilesize($self->{logfile}));
    $self->{logmodified} = 1;
    $self->{laststate}->{logoffset} = 0;
    $self->trace(sprintf "reset to offset 0");
  } elsif ($self->getfilesize($self->{logfile}) == 
        $self->{laststate}->{logoffset}) {
  	$self->trace(sprintf "the logfile did not change");
  } else {
    $self->trace("I HAVE NO IDEA WHAT HAPPENED");
  }
  return $self;
}

sub collectfiles {
  my $self = shift;
  my @rotatedfiles = ();
  if ($self->{logmodified}) {
  	my $fh = new IO::File;
    # cygwin lets you open files even after chmodding them to 0000, so double check with -r
    if ($self->getfileisreadable($self->{logfile})) {
      $fh->open($self->{logfile}, "r");
      $self->trace("opened logfile %s", $self->{logfile});
      push(@rotatedfiles, 
          { filename => $self->{logfile}, fh => $fh, seekable => 1 });
      $self->trace("logfile %s (modified %s / accessed %s / inode %d / inode changed %s)",
          $self->{logfile},
          scalar localtime((stat $self->{logfile})[9]),
          scalar localtime((stat $self->{logfile})[8]),
          (stat $self->{logfile})[1],
          scalar localtime((stat $self->{logfile})[10]));
    } else {
      if (-e $self->{logfile}) {
        #  permission problem
        $self->trace("could not open logfile %s", $self->{logfile});
        $self->addevent('CRITICAL', sprintf "could not open logfile %s",
            $self->{logfile});
      } else {
      	if ($self->{options}->{logfilenocry}) {
      	  # logfiles which are not rotated but deleted and re-created may be missing
          #  maybe a rotation situation, a typo in the configfile,...
          $self->trace("could not find logfile %s", $self->{logfile});
          $self->addevent('UNKNOWN', sprintf "could not find logfile %s",
              $self->{logfile});
        } else {
      	  # dont care.
      	  $self->trace("could not find logfile %s, but that's ok",
              $self->{logfile});  
        }
      }
    }
  }
  $self->trace(sprintf "relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles));
  $self->{relevantfiles} = \@rotatedfiles;
}


package Nagios::CheckLogfiles::Search::Rotating;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  $self->rotationpattern();
  return $self->init(shift);
}
 
sub analyze_situation {
  my $self = shift;
  $self->{logrotated} = 0;
  $self->{logmodified} = 0;
  
  ####
  # todo
  #
  # check for zero size logfile
  # read the first line and calculate the checksum
  #
  if (! -e $self->{logfile}) {
    #
    #  if no logfile exists, then probably it was rotated and no new logs
    #  were written since.
    #  find files which were modified after $lasttime. the most recent one
    #  is probably the former logfile. position at $lastoffset. 
    #  if this configurations does not care for rotations, there is nothing
    #  we can do here.
    #
    $self->{logrotated} = 1;
    $self->{logmodified} = 1;
    $self->trace(sprintf "there is no logfile %s at this moment",
        $self->{logfile});
  } elsif ($self->{laststate}->{devino} ne
        $self->getfilefingerprint($self->{logfile})) {
    # the inode changed (! the old inode could be reused)
    $self->trace(sprintf "this is not the same logfile %s != %s",
        $self->{laststate}->{devino},
        $self->getfilefingerprint($self->{logfile}));
    $self->{logrotated} = 1;
    $self->{logmodified} = 1;
  } elsif ($self->getfilesize($self->{logfile}) > 
        $self->{laststate}->{logoffset}) {
    #
    #  the logfile grew.
    #  this is the normal behaviour. in rare cases the logfile could have been
    #  rotated/recreated and grown very fast.
    $self->trace(sprintf "the logfile grew to %d",
        $self->getfilesize($self->{logfile}));
    if ($self->{likeavirgin}) {
      # if the logfile grew because we initialized the plugin with an offset of 0, position
      # at the end of the file and skip this search. otherwise lots of outdated messages could
      # match and raise alerts.
      $self->{laststate}->{logoffset} = $self->getfilesize($self->{logfile});
    } else {
      $self->{logmodified} = 1;
    }
  } elsif ($self->getfilesize($self->{logfile}) == 0) {
  	#
  	#  the logfile was either truncated or deleted and touched.
  	#  nothing to do except reset the position
    $self->{logrotated} = 1;  
    $self->{laststate}->{logtime} = (stat $self->{logfile})[9];
  } elsif ($self->getfilesize($self->{logfile}) < 
        $self->{laststate}->{logoffset}) {
    #
    #  logfile shrunk. either it was truncated or it was
    #  rotated and a new logfile was created.
    $self->trace(sprintf "the logfile shrunk from %d to %d",
        $self->{laststate}->{logoffset}, $self->getfilesize($self->{logfile}));
    $self->{logmodified} = 1;
    $self->{logrotated} = 1;
  } elsif ($self->getfilesize($self->{logfile}) == 
        $self->{laststate}->{logoffset}) {
    $self->trace(sprintf "the logfile did not change");
  } else {
    $self->trace("I HAVE NO IDEA WHAT HAPPENED");
  }
  return $self;
}
 
sub collectfiles {
  my $self = shift;
  my @rotatedfiles = ();
  if ($self->{logrotated} && $self->{rotation}) {
    $self->trace("looking for rotated files in %s with pattern %s",
        $self->{archivedir}, $self->{filenamepattern});
    opendir(DIR, $self->{archivedir});
    # read the filenames from DIR, match the filenamepattern, check the file age
    # open the file and return the handle
    # sort the handles by modification time
    #@rotatedfiles = sort { (stat $a->{fh})[9] <=> (stat $b->{fh})[9] } map {
    @rotatedfiles = sort { $a->{modtime} <=> $b->{modtime} } map {
      if (/$self->{filenamepattern}/) {
        my $archive = sprintf "%s/%s", $self->{archivedir}, $_;
        $self->trace("archive %s matches (modified %s / accessed %s / inode %d / inode changed %s)", $archive,
            scalar localtime((stat $archive)[9]),
            scalar localtime((stat $archive)[8]),
            (stat $archive)[1],
            scalar localtime((stat $archive)[10]));
        if ((stat $self->{archivedir}.'/'.$_)[9] >=
            $self->{laststate}->{logtime}) {
          $self->trace("archive %s was modified after %s", $archive,
              scalar localtime($self->{laststate}->{logtime}));
          my $fh = new IO::File;
          if (/.*\.gz\s*$/) {
            $self->trace("uncompressing %s with gzip -dc < %s|", $archive, 
                $archive);
            if ($fh->open('gzip -dc < '.$archive.'|')) {
              ({ filename => $archive,
                  fh => $fh, seekable => 0,
                  modtime => (stat $archive)[9],
                  fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) });
            } else {
              $self->trace("archive %s cannot be opened with gzip", $archive);
              ();
            }
          } elsif (/.*\.bz2\s*$/) {
            $self->trace("uncompressing %s with bzip2 -d < %s|", $archive, 
                $archive);
            if ($fh->open('bzip2 -d < '.$archive.'|')) {
              ({ filename => $archive,
                  fh => $fh, seekable => 0,
                  modtime => (stat $archive)[9],
                  fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) });
            } else {
              $self->trace("archive %s cannot be opened with bzip2", $archive);
              ();
            }
          } else {
            if ($fh->open($archive, "r")) {
              ({ filename => $archive,
                  fh => $fh, seekable => 1,
                  size => $self->getfilesize($fh),
                  modtime => (stat $archive)[9],
                  fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) });
            } else {
              $self->trace("archive %s cannot be opened", $_);
              ();
            }
          }
        } else {
          ();
        }
      } else {
        ();
      }
    } readdir(DIR);
    closedir(DIR);
    if (scalar(@rotatedfiles) == 0) {
      #
      #  although a logfile rotation was detected, no archived files were found.
      #  start seeking at position 0.
      #
      $self->{laststate}->{logoffset} = 0;
      $self->trace("although a logfile rotation was detected, no archived files were found");
    }
  }
  if ($self->{logmodified}) {
  	my $fh = new IO::File;
    # cygwin lets you open files even after chmodding them to 0000, so double check with -r
    if ($self->getfileisreadable($self->{logfile})) {
      $fh->open($self->{logfile}, "r");
      $self->trace("opened logfile %s", $self->{logfile});
      push(@rotatedfiles, 
          { filename => $self->{logfile}, fh => $fh, seekable => 1,
          size => $self->getfilesize($self->{logfile}),
          fingerprint => $self->getfilefingerprint($self->{logfile}).':'.$self->getfilesize($self->{logfile}) });
      $self->trace("logfile %s (modified %s / accessed %s / inode %d / inode changed %s)",
          $self->{logfile},
          scalar localtime((stat $self->{logfile})[9]),
          scalar localtime((stat $self->{logfile})[8]),
          (stat $self->{logfile})[1],
          scalar localtime((stat $self->{logfile})[10]));
    } else {
      if (-e $self->{logfile}) {
        #  permission problem
        $self->trace("could not open logfile %s", $self->{logfile});
        $self->addevent('CRITICAL', sprintf "could not open logfile %s",
            $self->{logfile});
      } else {
      	if ($self->{options}->{logfilenocry}) {
      	  # logfiles which are not rotated but deleted and re-created may be missing
          #  maybe a rotation situation, a typo in the configfile,...
          $self->trace("could not find logfile %s", $self->{logfile});
          $self->addevent('UNKNOWN', sprintf "could not find logfile %s",
              $self->{logfile});
       	} else {
      	  # dont care.
      	  $self->trace("could not find logfile %s, but that's ok", $self->{logfile});
        }
      }
    }
  }
  # now we have an array of structures each pointing to a file
  # which has been rotated since the last scan plus the current logfile.
  # the array members are sorted by modification time of the files.
  # now duplicate entries are removed. in one scenario the current logfile is
  # a symbolic link to a file which uses the same naming schema as the rotated
  # logfiles.
  $self->trace(sprintf "first relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles));
  my %seen = ();
  @rotatedfiles = reverse map {
    $self->trace("%s has fingerprint %s", $_->{filename}, $_->{fingerprint});
    # because of the windows dummy devino 0:0, we need to add the size
    if (exists $seen{$_->{fingerprint}}) {
      $self->trace("skipping %s (identical to %s)", 
          $_->{filename}, $seen{$_->{fingerprint}});
      ();
    } else {
      $seen{$_->{fingerprint}} = $_->{filename};
      $_;
    }
  } reverse @rotatedfiles;
  if (@rotatedfiles && (exists $rotatedfiles[0]->{size}) && 
      ($rotatedfiles[0]->{size} < $self->{laststate}->{logoffset})) {
    $self->trace(sprintf "file %s is too short (%d < %d). this should not happen. reset",
        $rotatedfiles[0]->{filename},
        $rotatedfiles[0]->{size}, $self->{laststate}->{logoffset});
    $self->{laststate}->{logoffset} = $rotatedfiles[0]->{size};
  }
  $self->trace(sprintf "relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles));
  $self->{relevantfiles} = \@rotatedfiles;
}

sub prepare {
  my $self = shift;
  if ("LOGLOGDATE8GZ" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf '^%s[\.\-]{0,1}[0-9]{8}\.gz$',
        $self->{logbasename};
  } elsif ("LOGLOGDATE8BZ2" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf '^%s[\.\-]{0,1}[0-9]{8}\.bz2$',
        $self->{logbasename};
  } elsif ("LOGLOG0LOG1GZ" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+\.gz))$',
        $self->{logbasename};
  } elsif ("LOGLOG0GZLOG1GZ" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+[0-9]*))\.gz$',
        $self->{logbasename};
  } elsif ("LOGLOG0LOG1" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+[0-9]*))$',
        $self->{logbasename};
  } elsif ("SUSE" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf "%s.*[0-9]*.gz", $self->{logbasename};
  } elsif ("DEBIAN" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf "%s.0|%s.*[0-9]*.gz",
        $self->{logbasename}, $self->{logbasename};
  } elsif ("QMAIL" eq uc($self->{rotation})) {
    $self->{filenamepattern} = "\@.*";
  } elsif ("LOGROTATE" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf "%s.*[0-9]*.gz", $self->{logbasename};
  } elsif ("SOLARIS" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf "%s.*\\.[0-9]+", $self->{logbasename};
  } elsif ("HPUX" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf "OLD%s", $self->{logbasename};
  } elsif ("BMWHPUX" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf 'OLD%s|%s\\.[A-Z][0-9]+_[0-9]+\\.gz$',
        $self->{logbasename}, $self->{logbasename};
  } elsif ("MOD_LOG_ROTATE" eq uc($self->{rotation})) {
    $self->{filenamepattern} = sprintf 'access\.log\.\d{10}';
    bless $self, "Nagios::CheckLogfiles::Search::Rotating::Uniform";
    $self->prepare();
  } else {
    $self->{filenamepattern} = $self->{rotation};
    $self->resolve_macros_in_pattern(\$self->{filenamepattern});
  }
  return $self;
}




package Nagios::CheckLogfiles::Search::Rotating::Uniform;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search::Rotating);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub prepare {
  my $self = shift;
  my $params = shift;
  my @matchingfiles = ();
  # find newest rotatingpattern = logfile
  opendir(DIR, $self->{archivedir});
  @matchingfiles = sort { $a->{modtime} <=> $b->{modtime} } map {
      if (/$self->{filenamepattern}/) {
        my $archive = sprintf "%s/%s", $self->{archivedir}, $_;
       ({ filename => $archive, modtime => (stat $archive)[9]});
      } else {
        ();
      }
  } readdir(DIR);
  closedir(DIR);
  if (@matchingfiles) {
    $self->{logfile} = $matchingfiles[-1]->{filename};
    $self->trace("the newest uniform logfile i found is %s", $self->{logfile});
  } else {
    $self->{logfile} = $self->{archivedir}.'/logfilenotfound';
    $self->trace("i found no uniform logfiles in %s", $self->{archivedir});
  }
  $self->construct_seekfile();
}

sub construct_seekfile {
  my $self = shift;
  # modify seekfilename so it can be found even if the logfile has changed
  $self->{logbasename} = basename($self->{logfile});
  $self->{seekfile} = dirname($self->{logfile}).'/uniformlogfile';
  $self->{seekfile} =~ s/\//_/g;
  $self->{seekfile} =~ s/\\/_/g;
  $self->{seekfile} =~ s/:/_/g;
  $self->{seekfile} =~ s/\s/_/g;
  $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{seekfile},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
  $self->{pre2seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{logbasename},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
  $self->trace("rewrote uniform seekfile to %s", $self->{seekfile});
  return $self;
}
 

package Nagios::CheckLogfiles::Search::Virtual;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}
    
sub loadstate {
  my $self = shift;
  $self->{laststate}->{logoffset} = 0;
}

sub savestate {
}

sub analyze_situation {
  my $self = shift;
  $self->{logmodified} = 1; 
}

sub collectfiles {
  my $self = shift;
  my @rotatedfiles = ();
  my $fh = new IO::File;
  if ($self->getfileisreadable($self->{logfile})) {
    $fh->open($self->{logfile}, "r");
    $self->trace("opened logfile %s", $self->{logfile});
    push(@rotatedfiles,
        { filename => $self->{logfile}, fh => $fh, seekable => 1 });
  } else {
    if (-e $self->{logfile}) {
      #  permission problem
      $self->trace("could not open logfile %s", $self->{logfile});
      $self->addevent('CRITICAL', sprintf "could not open logfile %s",
          $self->{logfile});
    } else {
      if ($self->{options}->{logfilenocry}) {
        $self->trace("could not find logfile %s", $self->{logfile});
        $self->addevent('UNKNOWN', sprintf "could not find logfile %s",
            $self->{logfile});
      } else {
        # dont care.
        $self->trace("could not find logfile %s, but that's ok",
            $self->{logfile});
      }
    }
  }
  $self->{relevantfiles} = \@rotatedfiles;
}


package Nagios::CheckLogfiles::Search::Prescript;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{tag} = "prescript";
  $self->{scriptpath} = $params->{scriptpath};
  $self->{macros} = $params->{macros};
  $self->{tracefile} = $params->{tracefile};
  $self->{cfgbase} = $params->{cfgbase};
  $self->{logbasename} = "prescript";
  $self->{script} = $params->{script};
  $self->{scriptparams} = $params->{scriptparams};
  $self->{scriptstdin} = $params->{scriptstdin};
  $self->{scriptdelay} = $params->{scriptdelay};   
  $self->{options} = { script => 0, protocol => 0, count => 1,
      smartscript => 0, supersmartscript => 0 };
  $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" };
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  $self->refresh_options($params->{options});
  $self->{exitcode} = 0;
  $self->{macros}->{CL_LOGFILE} = $params->{cfgbase};
  $self->{macros}->{CL_TAG} = $self->{tag};
  $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{OK};
  $self->{macros}->{CL_SERVICEOUTPUT} = "OK - starting up";
  $self->{macros}->{CL_PATTERN_NUMBER} = 0;
  return $self;
}

sub run {
  my $self = shift;
  $self->trace("call (%s) prescript %s",
      $self->{options}->{smartscript} ? "smart" : "dumb", $self->{script});
  my ($actionsuccess, $actionrc, $actionoutput) =
      $self->action($self->{script}, $self->{scriptparams},
      $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript});
  if (! $actionsuccess) {
    $self->{options}->{count} = 1;
    $self->{options}->{protocol} = 1;
    $self->addevent('WARNING',
        sprintf "cannot execute %s", $self->{script});
  } elsif ($self->{options}->{smartscript}) {
    if ($actionrc) {
      $actionoutput = "prescript" if ! $actionoutput;
      $self->addevent($actionrc, $actionoutput);
    }
  }	
  $self->{exitcode} = $actionrc;
}


package Nagios::CheckLogfiles::Search::Postscript;

use strict;
use Exporter;
use File::Basename;
use vars qw(@ISA);

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{tag} = "postscript";
  $self->{scriptpath} = $params->{scriptpath};
  $self->{macros} = $params->{macros};
  $self->{tracefile} = $params->{tracefile};
  $self->{cfgbase} = $params->{cfgbase};
  $self->{logbasename} = "postscript";
  $self->{script} = $params->{script};
  $self->{scriptparams} = $params->{scriptparams};
  $self->{scriptstdin} = $params->{scriptstdin};
  $self->{scriptdelay} = $params->{scriptdelay};   
  $self->{options} = { script => 0, protocol => 0, count => 1,
      smartscript => 0, supersmartscript => 0 };
  $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" };
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  $self->refresh_options($params->{options});
  $self->{exitcode} = 0;
  $self->{macros}->{CL_LOGFILE} = $params->{cfgbase};
  $self->{macros}->{CL_TAG} = $self->{tag};
  $self->{macros}->{CL_SERVICESTATEID} = 0; # will be set in SUPER::run()
  $self->{macros}->{CL_SERVICEOUTPUT} = ""; # will be set in SUPER::run()
  $self->{macros}->{CL_PATTERN_NUMBER} = 0;
  return $self;
}

sub run {
  my $self = shift;
  $self->trace("call postscript %s", $self->{script});
  my ($actionsuccess, $actionrc, $actionoutput) =
      $self->action($self->{script}, $self->{scriptparams},
      $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript});
  if (! $actionsuccess) {
    $self->{options}->{count} = 1;
    $self->{options}->{protocol} = 1;
    $self->addevent('WARNING',
        sprintf "cannot execute %s", $self->{script});
    $actionrc = 2;
  } elsif ($self->{options}->{smartscript}) {
    if ($actionrc || $self->{options}->{supersmartscript}) {
      $actionoutput = "postscript" if ! $actionoutput;
      $self->addevent($actionrc, $actionoutput);
    }
  }
  $self->{exitcode} = $actionrc;
}



package Nagios::CheckLogfiles::Search::Errpt;

use strict;
use Exporter;
use File::Basename;
use Time::Local;
use IO::File;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{logfile} = sprintf "%s/errpt.%s", $self->{seekfilesdir},
      $self->{tag};
  $self->SUPER::init($params);
  $self->{clo} = {
  	path => $params->{errpt}->{path} ? 
  	    $params->{errpt}->{path} : "errpt",
    errortype => $params->{errpt}->{errortype},
    errorclass => $params->{errpt}->{errorclass},
    errorlabel => $params->{errpt}->{errorlabel}
  };
}
  
sub prepare {
  my $self = shift;
  $self->{options}->{nologfilenocry} = 1;
  # the last minute is the end time. in-progess minutes are not 
  # interesting yet.
  my($sec, $min, $hour, $mday, $mon, $year) = 
      #(localtime $self->{macros}->{CL_DATE_TIMESTAMP})[0, 1, 2, 3, 4, 5];
      # macro is not suitable for testing because it is not updated
      (localtime time)[0, 1, 2, 3, 4, 5];
  $self->{errpt}->{endtime} = 
      timelocal(0, $min, $hour, $mday, $mon, $year) - 60;
}

sub loadstate {
  my $self = shift;
  $self->SUPER::loadstate();
  # always scan the whole output. thst's what starttime is for.
  $self->{laststate}->{logoffset} = 0;
  # if this is the very first run, look back 5 mintes in the past.
  $self->{errpt}->{starttime} = $self->{laststate}->{logtime} ?
      $self->{laststate}->{logtime} + 60 : $self->{errpt}->{endtime} - 300;
}

sub savestate {
  my $self = shift;
  $self->{newstate} = $self->{laststate};
  # remember the last minute scanned.
  $self->{newstate}->{logtime} = $self->{errpt}->{endtime};
  $self->SUPER::savestate();
}

sub analyze_situation {
  my $self = shift;
  if ($self->{errpt}->{starttime} <= $self->{errpt}->{endtime}) {
    $self->{logmodified} = 1; 
  } else {
    # this happens if you call the plugin in too short intervals.
    $self->trace("%s not before %s", 
        scalar localtime $self->{errpt}->{starttime},
        scalar localtime $self->{errpt}->{endtime});
  }
}

sub collectfiles {
  my $self = shift;
  my $fh = new IO::File;
  if ($self->{logmodified}) {
    my($sec, $min, $hour, $mday, $mon, $year) = 
        (localtime $self->{errpt}->{starttime})[0, 1, 2, 3, 4, 5];
    $self->{errpt}->{ibmstarttime} = sprintf "%02d%02d%02d%02d%02d",
        $mon + 1, $mday, $hour, $min, substr($year + 1900, 2, 2);
    ($sec, $min, $hour, $mday, $mon, $year) = 
        (localtime $self->{errpt}->{endtime})[0, 1, 2, 3, 4, 5];
    $self->{errpt}->{ibmendtime} = sprintf "%02d%02d%02d%02d%02d",
        $mon + 1, $mday, $hour, $min, substr($year + 1900, 2, 2);
    my $errpt = sprintf "%s -s %s -e %s %s %s %s |", $self->{clo}->{path},
        $self->{errpt}->{ibmstarttime}, $self->{errpt}->{ibmendtime},
        $self->{clo}->{errortype} ? '-T '.$self->{clo}->{errortype} : "",
        $self->{clo}->{errorclass} ? '-d '.$self->{clo}->{errorclass} : "",
        $self->{clo}->{errorlabel} ? '-J '.$self->{clo}->{errorlabel} : "";
    $self->trace("calling %s", $errpt); 
    $self->trace("calling errpt -s (%s) -e (%s)", 
        scalar localtime $self->{errpt}->{starttime},
        scalar localtime $self->{errpt}->{endtime});
    if ($fh->open($errpt)) {
      push(@{$self->{relevantfiles}},
        { filename => "errpt|",
          fh => $fh, seekable => 0,
          modtime => $self->{errpt}->{endtime},
          fingerprint => "0:0" });
    } else {
      $self->trace("cannot execute errpt");
      $self->addevent('UNKNOWN', "cannot execute errpt");
    }
  }
}

package Nagios::CheckLogfiles::Search::Psloglist;

use strict;
use Exporter;
use File::Basename;
use Time::Local;
use IO::File;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{logfile} = '/psloglist/is/cool';
  $self->SUPER::init($params);
  $self->{clo} = {
  	path => $params->{psloglist}->{path} ? $params->{psloglist}->{path} :
  	    ($^O =~ "MSWin") ? "C:/Programme/PsTools/psloglist" :
  	    "/cygdrive/c/Programme/PsTools/psloglist",
    eventlog => $params->{psloglist}->{eventlog} || "system",
    computer => $params->{psloglist}->{computer},
    username => $params->{psloglist}->{username},
    password => $params->{psloglist}->{password},
    source => $params->{psloglist}->{source}
  };
}
    
sub prepare {
  my $self = shift;
  $self->{options}->{nologfilenocry} = 1;
  # the last minute is the end time. in-progess minutes are not 
  # interesting yet.
  my($sec, $min, $hour, $mday, $mon, $year) = 
      #(localtime $self->{macros}->{CL_DATE_TIMESTAMP})[0, 1, 2, 3, 4, 5];
      # macro is not suitable for testing because it is not updated after each search
      (localtime time)[0, 1, 2, 3, 4, 5];
  $self->{eventlog}->{thisminute} = 
      timelocal(0, $min, $hour, $mday, $mon, $year);
  $self->{eventlog}->{nowminutefilter} = sprintf "%02d.%02d.%4d %02d:%02d:.*",
      $mday, $mon + 1, $year + 1900, $hour, $min;
  $self->trace(sprintf "i will discard messages with %s", 
      $self->{eventlog}->{nowminutefilter});
  $self->addfilter(0, $self->{eventlog}->{nowminutefilter});
}

sub loadstate {
  my $self = shift;
  $self->SUPER::loadstate();
  # always scan the whole output. thst's what starttime is for.
  $self->{laststate}->{logoffset} = 0;
  # if this is the very first run, look back 5 mintes in the past.
  $self->{laststate}->{logtime} = $self->{laststate}->{logtime} ?
      $self->{laststate}->{logtime} : $self->{eventlog}->{thisminute} - 600;
}

sub savestate {
  my $self = shift;
  $self->{newstate} = $self->{laststate};
  # remember the last minute scanned.
  $self->{newstate}->{logtime} = $self->{eventlog}->{thisminute};
  $self->SUPER::savestate();
}

sub analyze_situation {
  my $self = shift;
  $self->trace("last scanned until %s", 
      scalar localtime $self->{laststate}->{logtime});
  $self->{eventlog}->{distance} = 
      1 + int ((time - $self->{laststate}->{logtime}) / 60);
  $self->trace("analyze events from the last %d minutes", 
      $self->{eventlog}->{distance});
  $self->trace(sprintf "from %s to %s",
      scalar localtime (time - 60 * $self->{eventlog}->{distance}),
      scalar localtime time);
  if ((time - $self->{laststate}->{logtime}) > 60) {
    $self->{logmodified} = 1; 
    my($sec, $min, $hour, $mday, $mon, $year) = 
        (localtime ($self->{laststate}->{logtime} - 60))[0, 1, 2, 3, 4, 5];
    $self->{eventlog}->{thenminutefilter} = 
       sprintf "%02d.%02d.%4d %02d:%02d:.*",
        $mday, $mon + 1, $year + 1900, $hour, $min;
    $self->addfilter(0, $self->{eventlog}->{thenminutefilter});
    $self->trace(sprintf "filter %s\,", $self->{eventlog}->{nowminutefilter});
    $self->trace(sprintf "filter %s\,", $self->{eventlog}->{thenminutefilter});
  } else {
    # this happens if you call the plugin in too short intervals.
    $self->trace("please wait for a minute"); 
  }
}

sub collectfiles {
  my $self = shift;
  my $fh = new IO::File;
  if ($self->{logmodified}) {
    my $eventlog = sprintf "%s %s %s %s -s -m %d -r %s %s %s", $self->{clo}->{path},
        $self->{clo}->{computer} ? '\\\\'.$self->{clo}->{computer} : "",
        $self->{clo}->{username} ? '-u '.$self->{clo}->{username} : "",
        $self->{clo}->{password} ? '-p '.$self->{clo}->{password} : "",
        $self->{eventlog}->{distance},
        $self->{clo}->{source} ? '-o '.$self->{clo}->{source} : "",
        $self->{clo}->{eventlog},
        ($^O eq "cygwin") ? '2>/dev/null |' : '2>NUL |';
    $self->trace("calling %s", $eventlog);
    if ($fh->open($eventlog)) {
      while (my $line = $fh->getline()) {
    	$self->trace(sprintf "skipping header %s", $line);
        last if $line =~ /^\w+ log on/;
      }
      push(@{$self->{relevantfiles}},
        { filename => "eventlog|",
          fh => $fh, seekable => 0,
          modtime => $self->{eventlog}->{nowminute},
          fingerprint => "0:0" });
    } else {
      $self->trace("cannot execute psloglist");
      $self->addevent('UNKNOWN', "cannot execute psloglist");
    }
  }
}

package Nagios::CheckLogfiles::Search::Ipmitool;

use strict;
use Exporter;
use File::Basename;
use Time::Local;
use IO::File;
use Digest::MD5 qw(md5_base64);
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search::Simple);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{logfile} = sprintf "%s/ipmitool.%s", $self->{seekfilesdir},
      $self->{tag};
  $self->SUPER::init($params);
  $self->{clo} = {
      path => $params->{ipmitool}->{path} ?
      $params->{ipmitool}->{path} : "ipmitool",
      cache => exists $params->{ipmitool}->{elist} ? 1 : 0,
      listcmd => exists $params->{ipmitool}->{elist} ? "elist" : "list"
  };
}

sub prepare {
  my $self = shift;
  $self->{options}->{nologfilenocry} = 1;
  $self->{logfile} = sprintf "%s/ipmitool.sel.dump.%s",
      $self->system_tempdir(), $self->{tag};
  $self->{sdrcache} = sprintf "%s/ipmitool.sdr.cache",
      $self->system_tempdir();
  if ($self->{clo}->{cache} && (! -f $self->{sdrcache} || 
      ((time - ($self->{sdrcache})[9]) > 86400))) {
    $self->trace("creating/refreshing sdr cache %s", $self->{sdrcache});
    system($self->{clo}->{path}.' sdr dump '.$self->{sdrcache}.' >/dev/null 2>&1');
  }
  unlink $self->{logfile};
  my $ipmitool_sel_list = sprintf "%s %s sel %s 2>&1 |",
      $self->{clo}->{path}, 
      $self->{clo}->{cache} ? "-S $self->{sdrcache}" : "",
      $self->{clo}->{listcmd};
  my $ipmitool_fh = new IO::File;
  my $spool_fh = new IO::File;
  $self->trace("executing %s", $ipmitool_sel_list);
  if ($ipmitool_fh->open($ipmitool_sel_list)) {
    if ($spool_fh->open('>'.$self->{logfile})) {
      while (my $event = $ipmitool_fh->getline()) {
        chomp $event;
        next if $event =~ /SEL has no entries/;
        $event =~ s/\|/;/g;
        $spool_fh->printf("%s\n", $event);
      }
      $spool_fh->close();
    }
    $ipmitool_fh->close();
  }
  $self->trace("wrote spoolfile %s", $self->{logfile});
}

sub getfilefingerprint {
  my $self = shift;
  my $file = shift;
  if (-f $file) {
    my $magic;
    if (ref $file) {
      my $pos = $file->tell();
      $file->seek(0, 0);
      $magic = $file->getline() || "this_was_an_empty_file";
      $file->seek(0, $pos);
    } else {
      my $fh = new IO::File;
      $fh->open($file, "r");
      $magic = $fh->getline() || "this_was_an_empty_file";
      $fh->close();
    }
    $self->trace("magic: %s", $magic);
    return(md5_base64($magic));
  } else {
    return "0:0";
  }
}
	package Nagios::CheckLogfiles::Search::Oraclealertlog;

use strict;
use Exporter;
use File::Basename;
use Time::Local;
use IO::File;
use vars qw(@ISA);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

@ISA = qw(Nagios::CheckLogfiles::Search);

sub new {
  my $self = bless {}, shift;
  return $self->init(shift);
}

sub init {
  my $self = shift;
  my $params = shift;
  $self->{logfile} = sprintf "%s/alertlog.%s", $self->{seekfilesdir},
      $self->{tag};
  $self->SUPER::init($params);
  $self->{oraalert}->{tns} = {
    connect => $params->{oraclealertlog}->{connect},
    username => $params->{oraclealertlog}->{username},
    password => $params->{oraclealertlog}->{password},
  };
}
    
sub prepare {
  my $self = shift;
  $self->{options}->{nologfilenocry} = 1;
  # the last second is the end time. in-progess seconds are not 
  # interesting yet.
  $self->{oraalert}->{untilsecond} = time - 1;
}

sub loadstate {
  my $self = shift;
  $self->SUPER::loadstate();
  # always scan the whole output. thst's what starttime is for.
  $self->{laststate}->{logoffset} = 0;
  # if this is the very first run, look back 5 mintes in the past.
  $self->{laststate}->{logtime} = $self->{laststate}->{logtime} ?
      $self->{laststate}->{logtime} : $self->{oraalert}->{untilsecond} - 60000;
}

sub savestate {
  my $self = shift;
  $self->{newstate} = $self->{laststate};
  # remember the last second scanned.
  $self->{newstate}->{logtime} = $self->{oraalert}->{untilsecond};
  $self->SUPER::savestate();
}

sub analyze_situation {
  my $self = shift;
  $self->trace("last scanned until %s", 
      scalar localtime $self->{laststate}->{logtime});
  if ($self->{laststate}->{logtime} < $self->{oraalert}->{untilsecond}) {
    $self->{logmodified} = 1;
  } else {
    $self->trace("please come back later");
  }
}

sub collectfiles {
  my $self = shift;
  my $fh = new IO::File;
  if ($self->{logmodified}) {
    # open database connection and select rows created since $self->{laststate}->{logtime}
    # and $self->{oraalert}->{untilsecond}
    my $linesread = 0;
    eval {
      require DBI;
      if (my $dbh = DBI->connect(
          sprintf("DBI:Oracle:%s", $self->{oraalert}->{tns}->{connect}),
          $self->{oraalert}->{tns}->{username},
          $self->{oraalert}->{tns}->{password}, 
          { RaiseError => 1, PrintError => 0 })) {
        my $sql = q{
            SELECT alert_timestamp, alert_text FROM alert_log 
            WHERE alert_timestamp > ? AND alert_timestamp <= ?
        };
        if (my $sth = $dbh->prepare($sql)) {
          $self->trace(sprintf "select events between %d and %d (%s and %s)",
              $self->{laststate}->{logtime}, $self->{oraalert}->{untilsecond},
              scalar localtime $self->{laststate}->{logtime},
              scalar localtime $self->{oraalert}->{untilsecond});
          $sth->execute($self->{laststate}->{logtime}, $self->{oraalert}->{untilsecond});
          if (my $fh = new IO::File($self->{logfile}, "w")) {
            while(my($alert_timestamp, $alert_text) = $sth->fetchrow_array()) {
              $fh->printf("%s %s\n", scalar localtime $alert_timestamp, $alert_text);
              $linesread++;
            }
            $fh->close();
          }
          $sth->finish();
        }
        $dbh->disconnect();
      }
    };
    if ($@) {
      $self->trace(sprintf "database operation failed: %s", $@);
      $self->addevent('UNKNOWN', sprintf "database operation failed: %s", $@);
    }
    $self->trace(sprintf "read %d lines from database", $linesread);
    if ($linesread) {
      if (my $fh = new IO::File($self->{logfile}, "r")) {
        $self->trace(sprintf "reopen logfile");
        push(@{$self->{relevantfiles}},
          { filename => "eventlog|",
            fh => $fh, seekable => 0,
            modtime => $self->{eventlog}->{nowminute},
            fingerprint => "0:0" });
      }
    }
  }
}

package main;

use strict;
use utf8;
use File::Basename;
use File::Find;
use Getopt::Long qw(:config no_ignore_case);

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;

my($opt_f, $opt_F, $opt_V, $opt_h, $opt_t, $opt_v, $opt_d);
my($opt_tag, $opt_logfile, $opt_rotation, $opt_okpattern,
    $opt_criticalpattern,$opt_criticalexception,
    $opt_warningpattern, $opt_warningexception, 
    $opt_noprotocol, $opt_syslogserver, $opt_syslogclient,
    $opt_noperfdata, $opt_sticky,
    $opt_selectedsearches, $opt_template);
my @cfgfiles = ();

my $plugin_revision = '$Revision: 1.0 $ ';
my $progname = basename($0);

sub print_version {
  printf "%s v2.4.1.3\n", basename($0);
}

sub print_help {
  print <<EOTXT;
This Nagios Plugin comes with absolutely NO WARRANTY. You may use
it on your own risk!
Copyright by ConSol Software GmbH, Gerhard Lausser.

This plugin looks for patterns in logfiles, even in those who were rotated
since the last run of this plugin.

Usage: check_logfiles [-t timeout] -f <configfile>

The configfile looks like this:

\$seekfilesdir = '/opt/nagios/var/tmp';
# where the state information will be saved.

\$protocolsdir = '/opt/nagios/var/tmp';
# where protocols with found patterns will be stored.

\$scriptpath = '/opt/nagios/var/tmp';
# where scripts will be searched for.

\$MACROS = \{ CL_DISK01 => "/dev/dsk/c0d1", CL_DISK02 => "/dev/dsk/c0d2" \};

\@searches = (
  {
    tag => 'temperature',
    logfile => '/var/adm/syslog/syslog.log',
    rotation => 'bmwhpux',
    criticalpatterns => ['OVERTEMP_EMERG', 'Power supply failed'],
    warningpatterns => ['OVERTEMP_CRIT', 'Corrected ECC Error'],
    options => 'script,protocol,nocount',
    script => 'sendnsca_cmd'
  },
  {
    tag => 'scsi',
    logfile => '/var/adm/messages',
    rotation => 'solaris',
    criticalpatterns => 'Sense Key: Not Ready',
    criticalexceptions => 'Sense Key: Not Ready /dev/testdisk',
    options => 'noprotocol'
  },
  {
    tag => 'logins',
    logfile => '/var/adm/messages',
    rotation => 'solaris',
    criticalpatterns => ['illegal key', 'read error.*\$CL_DISK01\$'],
    criticalthreshold => 4
    warningpatterns => ['read error.*\$CL_DISK02\$'],
  }
);

EOTXT
}

sub print_usage {
  print <<EOTXT;
Usage: check_logfiles [-t timeout] -f <configfile> [--searches=tag1,tag2,...]
       check_logfiles [-t timeout] --logfile=<logfile> --tag=<tag> --rotation=<rotation>
                      --criticalpattern=<regexp> --warningpattern=<regexp>
EOTXT
}


if (! GetOptions(
    "f|config=s" => \$opt_f,
    "F|configdir=s" => \$opt_F,
    "t|timeout=i" => \$opt_t,
    "V|version" => \$opt_V,
    "h|help" => \$opt_h,
    "d|debug" => \$opt_d,
    "v|verbose" => \$opt_v,
    "tag=s" => \$opt_tag,
    "logfile=s" => \$opt_logfile,
    "rotation=s" => \$opt_rotation,
    "criticalpattern=s" => \$opt_criticalpattern,
    "criticalexception=s" => \$opt_criticalexception,
    "warningpattern=s" => \$opt_warningpattern,
    "warningexception=s" => \$opt_warningexception,
    "okpattern=s" => \$opt_okpattern,
    "noprotocol" => \$opt_noprotocol,
    "syslogserver" => \$opt_syslogserver,
    "syslogclient=s" => \$opt_syslogclient,
    "sticky:s" => \$opt_sticky,
    "noperfdata" => \$opt_noperfdata,
    "searches=s" => \$opt_selectedsearches,
    "selectedsearches=s" => \$opt_selectedsearches,
    "template=s" => \$opt_template
 )) {
  print_help();
  exit UNKNOWN;
}

if ($opt_V) {
  print_version();
  exit UNKNOWN;
}
if ($opt_h) {
  print_help();
  exit UNKNOWN;
}

if (! ($opt_f || $opt_F) && ! $opt_logfile) {
  print_usage();
  exit UNKNOWN;
}
if ($opt_F) {
  sub eachFile {
    my $filename = $_;
    my $fullpath = $File::Find::name;
    #remember that File::Find changes your CWD, 
    #so you can call open with just $_
    if ((-f $filename) && ($filename =~ /\.(cfg|conf)$/)) { 
      push(@cfgfiles, $fullpath);
    }
  }
  find (\&eachFile, $opt_F);
  @cfgfiles = sort { $a cmp $b } @cfgfiles;
}
if ($opt_f) {
  # -f is always first
  unshift(@cfgfiles, $opt_f);
}
if (scalar(@cfgfiles) == 1) {
  $opt_f = $cfgfiles[0];
} elsif (scalar(@cfgfiles) > 1) {
  $opt_f = \@cfgfiles;
}
if (! $opt_selectedsearches) {
  $opt_selectedsearches = "";
}
if (my $cl = Nagios::CheckLogfiles->new({
      #cfgfile => $opt_f ? $opt_f : undef,
      cfgfile => $opt_f ? $opt_f : undef,
      searches => [ map { foreach my $key (keys %{$_}) { 
      	  delete $_->{$key} unless $_->{$key}}; $_; } ({
          tag => $opt_tag ? $opt_tag : undef,
          logfile => $opt_logfile ? $opt_logfile : undef,
          rotation => $opt_rotation ? $opt_rotation : undef,
          criticalpatterns => $opt_criticalpattern ? $opt_criticalpattern : undef,
          criticalexceptions => $opt_criticalexception ? $opt_criticalexception : undef,
          warningpatterns => $opt_warningpattern ? $opt_warningpattern : undef,
          warningexceptions => $opt_warningexception ? $opt_warningexception : undef,
          okpatterns => $opt_okpattern ? $opt_okpattern : undef,
          options => join(',', grep { $_ }
              $opt_noprotocol ? "noprotocol" : undef,
              $opt_noperfdata ? "noperfdata" : undef,
              $opt_syslogserver ? "syslogserver" : undef,
              $opt_syslogclient ? "syslogclient=".$opt_syslogclient : undef,
              defined $opt_sticky ? "sticky".($opt_sticky ? "=".$opt_sticky : "") : undef )
      })],
      selectedsearches => [split(/,/, $opt_selectedsearches)],
      dynamictag => $opt_tag ? $opt_tag : undef
  })) {
  $cl->{verbose} = $opt_v ? 1 : 0;
  $cl->{timeout} = $opt_t ? $opt_t : 60;
  $cl->run();
  printf "%s%s\n", $cl->{exitmessage}, $cl->{perfdata} ? "|".$cl->{perfdata} : "";
  exit $cl->{exitcode};
} else {
  printf "%s\n", $Nagios::CheckLogfiles::ExitMsg;
  exit $Nagios::CheckLogfiles::ExitCode;
}

