#! /usr/bin/perl -w
# nagios: -epn
#
# Logfile::Config::Tivoli.pm - Tivoli Config Module
#
# Purpose: Provide a convenient way for loading
#          tivoli config files and
#          return it as hash structure
#
package Nagios::Tivoli::Config::Logfile;

use strict;

sub new {
  my($this, $param ) = @_;
  my $class = ref($this) || $this;

  my $self = {
      formatfile   => '',  # format file with tivoli format definitions,
                           # can be an array of files
      formatstring => '',  # format file content as string
      severity_mappings => {},
      max_continuation_lines => 0, # in case there are %n in among the patterns
      line_buffer => [],   # for continuation lines
      line_buffer_size => 0,
  };
  bless $self, $class;

  $self->set_severity_mapping('fatal', 2);
  $self->set_severity_mapping('critical', 2);
  $self->set_severity_mapping('severe', 2);
  $self->set_severity_mapping('warning', 1);
  $self->set_severity_mapping('minor', 1);
  $self->set_severity_mapping('harmless', 0);
  $self->set_severity_mapping('unknown', 0);

  # parse parameter
  if (ref($param) eq "HASH") {
    for my $key (keys %{$param}) {
      if (!defined $self->{lc $key}) {
        printf STDERR "unrecognized parameter: %s\n", $key;
        return undef;
      } else {
        if (ref($param->{$key}) eq 'HASH') {
          $self->merge_hash($self->{$key}, $param->{$key});
        } else {
          $self->{lc $key} = $param->{$key};
        }
      }
    }
  } elsif (ref($param) eq "") {
    $self->{formatfile} = $param;
  } else {
    printf STDERR "formatfile is a required parameter\n";
  }
  if ((!defined $self->{formatfile} || $self->{formatfile} eq '') &&
      (!defined $self->{formatstring} || $self->{formatstring} eq '')) {
        printf STDERR "please either specify formatfile or formatstring\n";
    return undef;
  }
  if (defined $self->{formatstring} and $self->{formatstring} ne '') {
    $self->{_formatstring} = $self->{formatstring};
  } else {
    $self->{_formatstring} = $self->_read($self->{formatfile});
  }
  if (! $self->{_formatstring}) {
    return undef;
  }
  foreach (keys %{$self->{tivolimapping}}) {
    $self->set_severity_mapping($_, $self->{tivolimapping}->{$_});
  }
  if ($self->_parse) {
    #Data::Dumper::Dumper($self->{formats});
    return $self;
  } else {
    printf STDERR ("parsing failed, see previous messages...");
    return undef;
  }
}

sub _read {
  my $self     = shift;
  my $filename = shift;
  my $content;
  if (ref($filename) eq 'ARRAY') {
    for my $file (@{$filename}) {
      $content .= $self->_read($file);
    }
  } else {
    if (open FMT, $filename) {
      while(<FMT>) {
        $content .= $_;
      }
      close FMT;
    } else {
      printf STDERR "unable to read file %s: %s\n", $filename, $!;
      return undef;
    }
  }
  return($content);
}

sub _parse {
  my $self = shift;
  my $format;
  my $lineno = 0;
  for my $line (split /\n/, $self->{_formatstring}) {
    $lineno++;
    chomp $line;
    $line = $1 if $line =~ /^\s*(.*?)\s*$/;

    next if $line =~ m/^\/\//;
    next if $line eq "";

    if ($line =~ m/^FORMAT/) {
      my($name, $follows, $followname) = 
          $line =~ m/^FORMAT\s+(.*?)\s*(|FOLLOWS\s+(.*?))$/;
      $format= Nagios::Tivoli::Config::Logfile::Format->new({
          name => $name,
          lineno => $lineno,
          severity_mappings => $self->{severity_mappings},
      });
      if (defined $followname) {
        my @follows = split /\s*,\s*/, $followname;
        for my $follow (@follows) {
          if (my $follow_format = $self->get_format_by_name($follow)) {
            $format->inherit($follow_format);
          }
        }
        $format->{follows} = \@follows;
      }
    } elsif ($line =~ m/^END/) {
      if (!defined $format) {
        printf STDERR "found format end without beginning\n";
        return 0;
      }
      if (!defined $format->{pattern}) {
        if (!exists $format->{follows}) {
          printf STDERR "found format without pattern\n";
          return 0;
        }
      }
      $self->add_format($format);
    } elsif (defined $format) {
      if (!defined $format->{pattern}) {
        # %s Specifies a variable string.
        # %t Specifies a variable date of the form 'MMM DD hh:mm:ss'
        # %s+ Specifies one or more variable strings that
        #     are separated by spaces.
        # %s* Specifies zero or more strings separated by white space.
        # %n Specifies a new line (CR).
        #    This applies only to the following adapters:
        #    tecad_logfile_aix4-r1, tecad_logfile_hpux10,
        #    tecad_logfile_linux_ix86, tecad_logfile_linux-ppc,
        #    tecad_logfile_linux-s390, tecad_logfile_solaris2,
        #    and tecad_win.
        $format->{tiv_pattern} = $line;
        $format->{patternlines} = 0;
        if ($line =~ /%n/) {
          $format->{patternlines}++ while $line =~ /%n/g;
          $format->{pattern} = [map { $self->translate_pattern($_) } split /%n/, $line];
          $self->{max_continuation_lines} = $format->{patternlines} unless
              $format->{patternlines} <= $self->{max_continuation_lines};
        } else {
          $format->{pattern} = $self->translate_pattern($line);
        }
      } elsif ($line =~ m/^-(.*?)\s+(.*)$/i) {
        $format->add_variable($1, $2);
      } elsif ($line =~ m/^(.*?)\s+"*(.*?)"*\s*$/) {
        $format->add_slot($1, $2);
      }
    } else {
      printf STDERR "%s is outside of a format definition\n", $line;
      return 0;
    }
  }
  return 1;
}

sub translate_pattern {
  my $self = shift;
  my $tiv_pattern = shift;
  $tiv_pattern =~ s/\\/\\\\/g;          # quote \
  $tiv_pattern =~ s/\(/\\(/g;           # quote (
  $tiv_pattern =~ s/\)/\\)/g;           # quote )
  $tiv_pattern =~ s/%\[\d+\]s/%s/g;     # replace %[2]s with just %s
  $tiv_pattern =~ s/\[/\\[/g;           # quote [
  $tiv_pattern =~ s/\]/\\]/g;           # quote ]
  $tiv_pattern =~ s/\?/\\?/g;           # quote ?
  $tiv_pattern =~ s/\|/\\|/g;           # quote |
  $tiv_pattern =~ s/\-/\\-/g;           # quote -
  #$tiv_pattern =~ s/%s\+/\(.+?\)/g;     # %s+  becomes .+?
  #$tiv_pattern =~ s/%s\*/\(.*?\)/g;     # %s*  becomes .*?
  #$tiv_pattern =~ s/%s/\(\[^\\s\]+?\)/g;  # %s   becomes [^\s]+?
  $tiv_pattern =~ s/%s\+/\([^\\s]*?.+[^\\s]*?\)/g; # %s+ becomes [^\s]*?.+[^\s]*?
  $tiv_pattern =~ s/%s\*\s*$/\(.*\)/g;     # last %s*  becomes .* eats the rest
  $tiv_pattern =~ s/%s\*/\(.*?\)/g;     # %s*  becomes .*? eats as much as necessary
  $tiv_pattern =~ s/%s/\(\[^\\s\]+\)/g;  # %s   becomes [^\s]+?
  #$tiv_pattern =~ s/%n/\\n/g;           # %n   becomes \n
  $tiv_pattern =~ s/[ ]+/\\s\+/g;           # blanks become \s+
  $tiv_pattern =~ s/%n//g;           # %n   becomes \n
  $tiv_pattern =~ s/%t/\(\\w\{3\}\\s+\\d\{1,2\}\\s+\\d\{1,2\}\:\\d\{1,2\}\:\\d\{1,2\}\)/g;
  return $tiv_pattern;
}

sub match {
  my $self = shift;
  my $line = shift;
  if ($self->{line_buffer_size} < $self->{max_continuation_lines} + 1) {
    push(@{$self->{line_buffer}}, $line);
    $self->{line_buffer_size}++;
  } else {
    shift @{$self->{line_buffer}};
    push(@{$self->{line_buffer}}, $line);
  }
#printf STDERR "try: %s\n", $line;
  foreach my $format (reverse @{$self->{'formats'}}) {
    if (($format->{name} ne '*DISCARD*') &&
        (! $format->has_slots() || ! $format->get_slot('severity'))) {
      next; # ungueltiges format
    }
    my @matches = ();
#printf STDERR "format %s\n", $format->{name};
#printf STDERR "match /%s/\n", $format->{pattern};
    if (my @matches = $self->match_pattern($line, $format->{pattern})) {
      my $hit = Nagios::Tivoli::Config::Logfile::Hit->new({
          format => $format,
          logline => $line,
          matches => \@matches,
          format_mappings => $self->{format_mappings},
          severity_mappings => $self->{severity_mappings},
      });
#printf STDERR "hit: %s\n", $line;
      if ($format->{name} eq '*DISCARD*') {
#printf STDERR "discard: %s %s\n", $line, Data::Dumper::Dumper($hit);
        last;
      } else {
#printf STDERR "hit2: %s // %s\n", $hit->{subject}, $format->{name};
        return({
          exit_code   => $hit->get_nagios_severity(),
          severity    => $hit->{severity},
          format_name => $hit->{format_name},
          subject     => $hit->{subject},
          logline     => $line,
          slots       => $hit->{slots},
        });
      }
    }
  }
#printf STDERR "mis: %s\n", $line;
  return({
    exit_code   => $self->get_severity_mapping('HARMLESS'),
    severity    => 'HARMLESS',
    format_name => 'NO MATCHING RULE',
    subject     => 'NO MATCHING RULE',
    logline     => $line,
    slots       => { },
  });
}

sub match_pattern {
  my $self = shift;
  my $line = shift;
  my $pattern = shift;
  if (ref($pattern) eq 'ARRAY') {
    my @all_matches = ();
    # 
    my $patterns = scalar(@{$pattern});
    if ($patterns > $self->{line_buffer_size}) {
      # zu wenig zeilen vorhanden
      return ();
    } else {
      my $startidx = $self->{line_buffer_size} - $patterns;
      my $idx = 0;
      while ($idx < $patterns) {
        # pattern[$idx] matched ${$self->{line_buffer}}[$startidx + $idx] ?
        if (my @matches = 
            ${$self->{line_buffer}}[$startidx + $idx] =~ /$pattern->[$idx]/) {
          $idx++;
          push(@all_matches, @matches);
        } else {
          last;
        }
      }
      if ($idx == $patterns) {
        return @all_matches;
      } else {
        return ();
      }
    }
  } else {
    my @matches = $line =~ /$pattern/;
    return @matches;
  }
}

# inherit
#
# copy variable and slot definitions of a followed format to the current format
#
sub inherit {
  my $self = shift;
  my $ancestor = shift;
  $self->merge_hash($self->{variables}, $ancestor->{variables});
  $self->merge_hash($self->{slots}, $ancestor->{slots});
}

# get_severity_mapping
#
# get the numerical nagios level for a tivoli level
#
sub get_severity_mapping {
  my $self = shift;
  my $tivoli_severity = lc shift;
  return $self->{severity_mappings}->{$tivoli_severity};
}

# set_severity_mapping
#
# set the numerical nagios level for a tivoli level
#
sub set_severity_mapping {
  my $self = shift;
  my $tivoli_severity = lc shift;
  my $nagios_severity = shift;
  $self->{severity_mappings}->{$tivoli_severity} = $nagios_severity;
}

# set_format_mappings
#
# set runtime values for LABEL, DEFAULT,...
#
sub set_format_mappings {
    my $self = shift;
    my %mappings = @_;
    foreach (keys %mappings) {
      $self->{format_mappings}->{$_} = $mappings{$_};
    }
}

sub add_format {
  my $self = shift;
  my $format = shift;
  push(@{$self->{formats}}, $format);
}

sub get_format_by_name {
  my $self = shift;
  my $name = shift;
  foreach (@{$self->{formats}}) {
    return $_ if $_->{name} eq $name;
  }
  return undef;
}

sub merge_hash {
    my $self  = shift;
    my $hash1 = shift;
    my $hash2 = shift;

    for my $key (keys %{$hash2}) {
        $hash1->{$key} = $hash2->{$key};
    }
    return($hash1);
}


package Nagios::Tivoli::Config::Logfile::Format;

use strict;
use warnings;
use Carp;
use vars qw(@ISA);

@ISA = qw(Nagios::Tivoli::Config::Logfile);

sub new {
  my($this, $param ) = @_;
  my $class = ref($this) || $this;

  my $self = {
      name => '',
      lineno => 0,
      slots => {},
      variables => {},
      severity_mappings => {},
  };
  bless $self, $class;

  if (ref($param) eq "HASH") {
    for my $key (keys %{$param}) {
      if (!defined $self->{lc $key}) {
        carp("unrecognized parameter: $key");
      } else {
        if (ref($param->{$key}) eq 'HASH') {
          $self->merge_hash($self->{$key}, $param->{$key});
        } else {
          $self->{lc $key} = $param->{$key};
        }
      }
    }
  }
  if (!defined $self->{name}) {
    die "please either specify formatfile or formatstring";
  }
  return $self;
}

sub add_slot {
  my $self = shift;
  my $slot = shift;
  my $value = shift;
  $self->{slots}->{$slot} = $value;
}

sub get_slot {
  my $self = shift;
  my $slot = shift;
  return $self->{slots}->{$slot};
}

sub has_slots {
  my $self = shift;
  return scalar (keys %{$self->{slots}});
}

sub add_variable {
  my $self = shift;
  my $variable = shift;
  my $value = shift;
  $self->{variables}->{$variable} = $value;
}

sub get_variable {
  my $self = shift;
  my $variable = shift;
  return $self->{variables}->{$variable};
}

sub has_variables {
  my $self = shift;
  return scalar (keys %{$self->{variables}});
}


package Nagios::Tivoli::Config::Logfile::Hit;

use strict;
use warnings;
use Carp;
use vars qw(@ISA);

@ISA = qw(Nagios::Tivoli::Config::Logfile::Format);

sub new {
  my($this, $param ) = @_;
  my $class = ref($this) || $this;

  my $self = {
      format => $param->{format},
      logline => $param->{logline},
      format_mappings => $param->{format_mappings},
      severity_mappings => $param->{severity_mappings},
      matches => {},
      variables => {},
      slots => {},
  };
  bless $self, $class;
  my $matchcnt = 1;
  map { $self->{matches}->{$matchcnt++} = $_; } @{$param->{matches}};
  $self->init();
  return $self;
}

sub init {
  my $self = shift;
  $self->{severity} = $self->{format}->{slots}->{severity};
  $self->{format_name} = $self->{format}->{name};
  $self->merge_hash($self->{variables}, $self->{format}->{variables});
  $self->merge_hash($self->{slots}, $self->{format}->{slots});
  # resolve pattern groups in internal variables
  foreach my $var (keys %{$self->{variables}}) {
    if ($self->{variables}->{$var} =~ /^\$(\d+)/) {
      if (defined $self->{matches}->{$1}) {
        $self->{variables}->{$var} = $self->{matches}->{$1};
      } else {
        printf STDERR "cannot replace \$%d in var %s\n", $1, $var;
      }
    }
  }
  # resolve pattern groups and format reserved words in slots
  foreach my $slot (keys %{$self->{slots}}) {
    if ($self->{slots}->{$slot} =~ /^\$(\d+)/) {
      if (defined $self->{matches}->{$1}) {
        $self->{slots}->{$slot} = $self->{matches}->{$1};
      } else {
        printf STDERR "cannot replace \$%d in slot %s\n", $1, $slot;
      }
    } elsif ($self->{slots}->{$slot} eq 'DEFAULT') {
      if ($slot eq 'hostname') {
        $self->{slots}->{$slot} = $self->{format_mappings}->{hostname};
      } elsif ($slot eq 'fqhostname') {
        $self->{slots}->{$slot} = $self->{format_mappings}->{fqhostname};
      } elsif ($slot eq 'origin') {
        $self->{slots}->{$slot} = $self->{format_mappings}->{origin};
      } else {
        $self->{slots}->{$slot} = 'check_logfiles';
      }
    } elsif ($self->{slots}->{$slot} eq 'LABEL') {
      $self->{slots}->{$slot} = $self->{format_mappings}->{LABEL};
    } elsif ($self->{slots}->{$slot} eq 'FILENAME') {
      $self->{slots}->{$slot} = $self->{format_mappings}->{FILENAME};
    } else {
    }
  }
  foreach my $slot (keys %{$self->{slots}}) {
    if ($self->{slots}->{$slot} =~ /PRINTF/i) {
      $self->{slots}->{$slot} = $self->printf($self->{slots}->{$slot});
    }
  }
  $self->{subject} = $self->{slots}->{msg} || $self->{logline};
  #delete $self->{slots}->{msg};
}

sub printf {
  my $self = shift;
  my $text = shift;
  my @printf = $text =~ m/printf\("(.*?)"\s*,\s*(.*)\)/i;
  my $result = $text;
  my @replacements;
  for my $key (split /\s*,\s*/, $printf[1]) {
    if (defined $self->{variables}->{$key}) {
      push @replacements, $self->{variables}->{$key};
    } elsif (defined $self->{slots}->{$key}) {
      push @replacements, $self->{slots}->{$key};
    } else {
      print STDERR "$key not found\n";
      push @replacements,  '';
    }
  }
  eval {
      $result = sprintf($printf[0], @replacements);
  };
  return($result);
}

sub get_nagios_severity {
  my $self = shift;
  return $self->get_severity_mapping($self->{slots}->{severity});
}


package Nagios::CheckLogfiles;

use strict;
use IO::File;
use File::Basename;
use File::Spec;
use Cwd;
use Data::Dumper;
#use Net::Domain qw(hostname hostdomain hostfqdn);
use Socket;
use POSIX qw(strftime);
use IPC::Open2;


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;

eval "require Win32;";
#eval "require Net::Domain qw(hostname hostdomain hostfqdn);";
eval "require Net::Domain;";

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} || '/var/tmp/check_logfiles';
  $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->{pidfile} = $params->{pidfile} || '/var/run/check_logfiles.pid';
  $self->{perfdata} = "";
  $self->{searches} = [];
  $self->{selectedsearches} = $params->{selectedsearches} || [];
  $self->{dynamictag} = $params->{dynamictag} || "";
  $self->{report} = $params->{report} || "short";
  $self->{cmdlinemacros} = $params->{cmdlinemacros} || {};
  $self->{reset} = $params->{reset} || 0;
  $self->default_options({ prescript => 1, smartprescript => 0,
      supersmartprescript => 0, postscript => 1, smartpostscript => 0,
      supersmartpostscript => 0 });
  if ($self->{report} !~ /^(long|short|html)$/) {
    $ExitCode = $ERROR_UNKNOWN;
    $ExitMsg = sprintf "UNKNOWN - output must be short, long or html";
    return undef;
  }
  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";
    } elsif ($params->{cfgfile} =~ /%0A/) {
      # this must be an encoded flat file
      $self->{cfgfile} = $params->{cfgfile};
      $self->{cfgbase} = "flatfile";
      if (! $self->init_from_file()) {
        return undef;
      }
    } 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};
      $_->{report} = $self->{report};
      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 (defined(&Win32::GetShortPathName) && ($^O =~ /Win/)) {
    # if this is true windows (not cygwin) and if the path exists
    # then transform it to a short form. undef if path does not exist.
    if (my $tmpshortpath = &Win32::GetShortPathName($self->{protocolsdir})) {
      $self->{protocolsdir} = $tmpshortpath;
    }
  }
  $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, $report, $timeout, $pidfile);
  our $MACROS = {};
  if ($^O =~ /MSWin/) {
    $ENV{HOME} = $ENV{HOMEPATH};
  }
  if ($self->{cfgbase} eq "flatfile") {
    $self->{cfgfile} =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
    eval $self->{cfgfile};
    if ($@) {
      $ExitCode = $ERROR_UNKNOWN;
      $ExitMsg = sprintf "UNKNOWN - syntax error %s", (split(/\n/, $@))[0];
      return undef;
    }
  } else {
    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;
    }
    $fullcfgfile = File::Spec->rel2abs($fullcfgfile) 
        unless File::Spec->file_name_is_absolute($fullcfgfile);
    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;
  # already done one level above $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 * 24 * 3600) 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->{report} = $report if $report;
  $self->{timeout} = $timeout if $timeout;
  $self->{pidfile} = $pidfile if $pidfile;
  $self->{privatestate} = {};
  $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" : "";
    $_->{privatestate} = $self->{privatestate};
    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};
    $_->{report} = $self->{report};
    if (my $search = Nagios::CheckLogfiles::Search->new($_)) {
      push(@{$self->{searches}}, $search);
      $_->{privatestate}->{$search->{tag}} = $search->{privatestate};
    } 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" : "";
    $_->{privatestate} = $self->{privatestate};
    my $search = Nagios::CheckLogfiles::Search::Postscript->new($_);
    push(@{$self->{searches}}, $search); 
  }
  return $self;
}

sub run {
  my $self = shift;
  if ($self->{reset}) {
    foreach my $search (@{$self->{searches}}) {
      if ($search->{tag} ne "prescript" && $search->{tag} ne "postscript") {
        $search->loadstate();
        foreach (keys %{$search->{laststate}}) {
          $search->{newstate}->{$_} = $search->{laststate}->{$_};
        }
        $search->addevent(0, "reset");
        $search->{newstate}->{logoffset} = 0;
        $search->savestate();
      }
    }
    return $self;
  }
  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_LONGSERVICEOUTPUT} = $self->{long_exitmessage} || $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...");
      }
      $_->{privatestate}->{$search->{tag}} = $search->{privatestate};
      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 {
      $self->{exitmessage} = sprintf "OK - no errors or warnings";
    }
  }
  $self->{perfdata} = join (" ", 
      map { $_->formulate_perfdata(); if ($_->{perfdata}) {$_->{perfdata}} else {()} }
      @{$self->{searches}});
  if ($self->{report} ne "short") {
    $self->formulate_long_result();
  }
}

sub formulate_long_result {
  my $self = shift;
  my $maxlength = 4 * 1024;
  $self->{long_exitmessage} = "";
  my $prefix = ($self->{report} eq "html") ?
      "<table style=\"border-collapse: collapse;\">" : "";
  my $suffix = ($self->{report} eq "html") ?
      "</table>" : "";
  my $messagelen = length($prefix) + length($suffix) +
      length($self->{exitmessage});
  my $line = "";
   
  foreach my $search (@{$self->{searches}}) {
    if (scalar(@{$search->{matchlines}->{CRITICAL}}) ||
        scalar(@{$search->{matchlines}->{WARNING}}) ||
        scalar(@{$search->{matchlines}->{UNKNOWN}})) {
      if ($self->{report} eq "html") {
        $line =
            sprintf "<tr valign=\"top\"><td class=\"service%s\">tag %s</td></tr>",
                ((scalar(@{$search->{matchlines}->{CRITICAL}}) && "CRITICAL") ||
                 (scalar(@{$search->{matchlines}->{WARNING}}) && "WARNING") ||
                 (scalar(@{$search->{matchlines}->{UNKNOWN}}) && "UNKNOWN")),
                $search->{tag};
      } else {
        $line =
            sprintf "tag %s %s\n",
                $search->{tag},
                ((scalar(@{$search->{matchlines}->{CRITICAL}}) && "CRITICAL") ||
                 (scalar(@{$search->{matchlines}->{WARNING}}) && "WARNING") ||
                 (scalar(@{$search->{matchlines}->{UNKNOWN}}) && "UNKNOWN"));
      }
      if ($messagelen + length($line) < $maxlength) {
        $self->{long_exitmessage} .= $line;
        $messagelen += length($line);
      } else {
        last;
      }
      foreach my $level qw(CRITICAL WARNING UNKNOWN OK) {
        foreach my $message (@{$search->{matchlines}->{$level}}) {
          if ($self->{report} eq "html") {
            $message =~ s/</&lt;/g;
            $message =~ s/>/&gt;/g;
            $line =
                sprintf "<tr valign=\"top\"><td nowrap width=\"100%%\" class=\"service%s\" style=\"border: 1px solid black;\">%s</td></tr>",
                $level, $message;
          } else {
            $line = sprintf "%s\n", $message;
          }
          if ($messagelen + length($line) < $maxlength) {
            $self->{long_exitmessage} .= $line;
            $messagelen += length($line);
          } else {
            last;
          }
        }
      }
    }
  }
  if ($self->{long_exitmessage}) {
    $self->{long_exitmessage} = sprintf "%s%s%s\n",
        $prefix, $self->{long_exitmessage}, $suffix;
  }
}

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

sub reset {
  my $self = shift;
  $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
  foreach my $level qw(OK CRITICAL WARNING UNKNOWN) {
    $self->{lastmsg}->{$level} = "";
  }
  foreach my $search (@{$self->{searches}}) {
    $search->reset();
  }
}

sub cleanup_protocols {
  my $self = shift;
  #
  #  cleanup old protocol files
  #
  #
  if ($self->{protocololdfiles} =~ /[^\\][ ]/) {
    # because Core::glob splits the argument on whitespace
    $self->{protocololdfiles} =~ s/( )/\\$1/g;
  }
  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_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 => "/usr/local/nagios/etc/send_nsca.cfg",
  };
  if (defined(&Win32::LoginName)) {
    $DEFAULTMACROS->{CL_USERNAME} = &Win32::LoginName();
    $DEFAULTMACROS->{CL_HAS_WIN32} = 1;
  } else {
    $DEFAULTMACROS->{CL_USERNAME} = scalar getpwuid $>;
    $DEFAULTMACROS->{CL_HAS_WIN32} = 0;
  }
  if (defined(&Net::Domain::hostname)) {
    $DEFAULTMACROS->{CL_HOSTNAME} = &Net::Domain::hostname();
    $DEFAULTMACROS->{CL_DOMAIN} = &Net::Domain::hostdomain();
    $DEFAULTMACROS->{CL_FQDN} = &Net::Domain::hostfqdn();
    $DEFAULTMACROS->{CL_HAS_NET_DOMAIN} = 1;
  } else {
    $DEFAULTMACROS->{CL_HOSTNAME} = POSIX::uname();
    $DEFAULTMACROS->{CL_DOMAIN} = "localdomain";
    $DEFAULTMACROS->{CL_FQDN} = POSIX::uname().'.'.'localdomain';
    $DEFAULTMACROS->{CL_HAS_NET_DOMAIN} = 0;
  }
#printf STDERR "%s\n", Data::Dumper::Dumper($DEFAULTMACROS);
  $DEFAULTMACROS->{CL_IPADDRESS} =
      scalar gethostbyname($DEFAULTMACROS->{CL_HOSTNAME}) ?
      inet_ntoa(scalar gethostbyname($DEFAULTMACROS->{CL_HOSTNAME})) :
      '127.0.0.1';
  #
  #  Add self-defined macros to the defaultmacros structure or overwrite
  #  already defined macros.
  #
  if ($self->{macros}) {
    foreach (keys %{$self->{macros}}) {
      $DEFAULTMACROS->{$_} = $self->{macros}->{$_};
    }
  }
  #
  #  Add self-defined macros from the command line 
  #  --macro CL_KAAS="so a kaas" --macro CL_SCHMARRN="so a schmarrn"
  #
  if ($self->{cmdlinemacros}) {
    foreach (keys %{$self->{cmdlinemacros}}) {
      $DEFAULTMACROS->{$_} = $self->{cmdlinemacros}->{$_};
    }
  }
  #
  #  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 default_options {
  my $self = shift;
  my $defaults = shift;
  while (my($key, $value) = each %{$defaults}) {
    $self->{options}->{$key} = $value;
  }
}

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 $privatestate = 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 {
      our $CHECK_LOGFILES_PRIVATESTATE = $privatestate;
      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 || "";
      $output = defined $stdoutvar ?  $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 {
    my $pathsep = ($^O =~ /MSWin/) ? ';' : ':';
    foreach my $dir (split(/$pathsep/, $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;
          if ($cmd =~ /\s/) {
            if (defined(&Win32::GetShortPathName)) {
              $cmd = &Win32::GetShortPathName($cmd);
            } else {
              $cmd = sprintf "\"%s\"", $cmd;
            }
          }
        } else {
          # need to escape blanks
          if ($cmd =~ /\s/) {
            $cmd =~ s/([ ])/\\$1/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 {
  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;
  if ($^O =~ /MSWin/) {
    # -r is not reliable when working with cacls
    my $fh = new IO::File;
    if ($fh->open($file, "r")) {
      $fh->close();
      return 1;
    } else {
      return undef;
    }
  } elsif (-r $file) {
    return 1;
  } else {
    use filetest 'access';
    $self->trace("stat (%s) failed, try access instead", $file);
    if (-r $file) {
      return 1;
    } else { # i'm catholic. i believe in miracles.
      my $fh = new IO::File;
      if ($fh->open($file, "r")) {
        $fh->close();
        return 1;
      } else {
        return 0;
      }
    }
  }
}

sub old_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 run_as_daemon {
  my $self = shift;
  my $delay = shift;
  if ($^O =~ /MSWin/) {
    if ($ENV{PROMPT}) { # i was called from a shell
      # vielleicht irgendwas mit detach
      die "not yet implemented";
    } else {
      eval "require Win32::Daemon;";
      if (defined(&Win32::Daemon::StartService)) {
        import Win32::Daemon;
        my $svc_callback = sub {
          my( $event, $context ) = @_;
          #
          # entgegen der DRECKSDOKU enthaelt $event NICHT den Status
          # 
          $event = Win32::Daemon::State();
          $context->{last_event} = $event;
          if ($event == SERVICE_RUNNING()) {
            # main loop
            $self->trace("Entering main loop");
            do {
              $self->run();
              $self->trace(sprintf "%s%s\n%s", $self->{exitmessage},
                  $self->{perfdata} ? "|".$self->{perfdata} : "",
                  $self->{long_exitmessage} ?
                      $self->{long_exitmessage}."\n" : "");
              $self->reset();
              foreach (1..$delay) {
                if (Win32::Daemon::State() == SERVICE_RUNNING()) {
                  sleep 1;
                } else {
                  last;
                }
              }
            } while(Win32::Daemon::State() == SERVICE_RUNNING());
            $self->trace("Leaving main loop");
          } elsif ($event == SERVICE_START_PENDING()) {
            # Initialization code
            $self->trace("Service initialized");
            $context->{last_state} = SERVICE_RUNNING();
            Win32::Daemon::State(SERVICE_RUNNING());
          } elsif ($event == SERVICE_PAUSE_PENDING()) {
            $self->trace("Service makes a break");
            $context->{last_state} = SERVICE_PAUSED();
            Win32::Daemon::State(SERVICE_PAUSED());
          } elsif ($event == SERVICE_CONTINUE_PENDING()) {
            $self->trace("Service continues");
            $context->{last_state} = SERVICE_RUNNING();
            Win32::Daemon::State(SERVICE_RUNNING());
          } elsif ($event == SERVICE_STOP_PENDING()) {
            $self->trace("Service stops");
            $context->{last_state} = SERVICE_STOPPED();
            $self->trace("Daemon exiting...");
            Win32::Daemon::State(SERVICE_STOPPED());
            Win32::Daemon::StopService();
          } else {
            # Take care of unhandled states by setting the State()
            # to whatever the last state was we set...
            $self->trace("Service got an unhandled call");
            Win32::Daemon::State( $context->{last_state} );
          }
          return();
        };
        Win32::Daemon::RegisterCallbacks($svc_callback);
        my %context = (
            count   =>  0,
            start_time => time(),
            keep_going => 0,
            make_a_break => 0,
        );
        # Start the service passing in a context and
        # indicating to callback using the "Running" event
        # every 2000 milliseconds (2 seconds).
        Win32::Daemon::StartService(\%context, 2000);
      } else {
        die "omeiomeiomei nix Win32::Daemon";
      }
    }
  } else {
    chdir '/';
    die "cannot detach from controlling terminal" unless POSIX::setsid();
    exit if (fork());
    exit if (fork());
    open STDIN, '+>/dev/null';
    open STDOUT, '+>&STDIN';
    open STDERR, '+>&STDIN';
    my $keep_going = 1;
    # $self->{pidfile};
    $self->trace(sprintf "Daemon running with pid %d", $$);
    foreach my $signal (qw(HUP INT TERM QUIT)) {
      $SIG{$signal}  = sub {
        $self->trace("Caught SIG%s:  exiting gracefully", $signal);
        $keep_going = 0;
      };
    }
    $self->trace("Entering main loop");
    do {
      $self->run();
      $self->trace(sprintf "%s%s\n%s", $self->{exitmessage},
          $self->{perfdata} ? "|".$self->{perfdata} : "",
          $self->{long_exitmessage} ? $self->{long_exitmessage}."\n" : "");
      $self->reset();
      foreach (1..$delay) {
        if ($keep_going) {
          sleep 1;
        } else {
          last;
        }
      }
    } while($keep_going);
    $self->trace("Daemon exiting...");
  }
}

sub install_windows_service {
  my $self = shift;
  my $servicename = shift || 'check_logfiles';
  my $cfgfile = shift;
  my $username = shift;
  my $password = shift;
  if ($^O =~ /MSWin/) {
    eval "require Win32::Daemon;";
    if (defined(&Win32::Daemon::StartService)) {
      import Win32::Daemon;
      my $fullpath = Win32::GetFullPathName($0);
      my ($cwd, $base, $ext) = ( $fullpath =~ /^(.*\\)(.*)\.(.*)$/ ) [0..2] ;
      my $servicepath = ($ext eq 'exe') ?
          "\"$fullpath\"" : "\"$^X\"";
      my $serviceparameters = ($ext eq 'exe') ?
          "--daemon --config \"$cfgfile\"" :
          " \"$fullpath\" --daemon --config \"$cfgfile\"";
      my $service = {
        machine => '',
        name => $servicename,
        display => $servicename,
        path => $servicepath,
        parameters => $serviceparameters,
        user => ($username || ''),
        password => ($password || ''),
        description => 'This is the Nagios plugin check_logfiles',
      };
      if (Win32::Daemon::CreateService($service)) {
        $self->{exitmessage} = 'Successfully added service';
        $self->{exitcode} = 0;
      } else {
        $self->{exitmessage} = 'Failed to add service: '.
            Win32::FormatMessage(Win32::Daemon::GetLastError());
        $self->{exitcode} = 3;
      }
    } else {
      die "nix Win32::Daemon, nix Service, nix install";
    }
  } else {
    $self->{exitmessage} = 'You just installed a Windows service on a Unix machine. Good luck.';
    $self->{exitcode} = 0;
  }
}

sub deinstall_windows_service {
  my $self = shift;
  my $servicename = shift || 'check_logfiles';
  if ($^O =~ /MSWin/) {
    eval "require Win32::Daemon;";
    if (defined(&Win32::Daemon::StartService)) {
      import Win32::Daemon;
      if (Win32::Daemon::DeleteService('', $servicename)) {
        $self->{exitmessage} = 'Successfully deinstalled service';
        $self->{exitcode} = 0;
      } else {
        $self->{exitmessage} = 'Failed to deinstall service: '.
            Win32::FormatMessage(Win32::Daemon::GetLastError());
        $self->{exitcode} = 3;
      }
    }
  } else {
    $self->{exitmessage} = 'Congrats. You just deinstalled a Windows service on a Unix machine.';
    $self->{exitcode} = 0;
  }
}

package Nagios::CheckLogfiles::Search;

use strict;
use Exporter;
use File::Basename;
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->{archivedir} = $params->{archivedir};
  $self->{scriptpath} = $params->{scriptpath};
  $self->{macros} = $params->{macros};
  $self->{tracefile} = $params->{tracefile};
  $self->{prefilter} = $params->{prefilter};
  $self->{trace} = -e $self->{tracefile} ? 1 : 0;
  $self->{report} = $params->{report};
  if (exists $params->{tivolipatterns}) {
    my $tivoliparams = { };
    my $tivolipatterns = [];
    my $tivoliformatfiles = [];
    my $tivoliformatstrings = [];
    if (ref($params->{tivolipatterns}) ne 'ARRAY') {
      $tivolipatterns = [$params->{tivolipatterns}];
    } else {
      push(@{$tivolipatterns}, @{$params->{tivolipatterns}});
    }
    foreach my $pattern (@{$tivolipatterns}) {
      if (scalar(@{[split /\n/, $pattern]}) == 1) {
        push(@{$tivoliparams->{formatfile}}, $pattern);
      } else {
        #push(@{$tivoliparams->{formatstring}}, $pattern);
        # erstmal nur skalar moeglich
        $tivoliparams->{formatstring} = $pattern;
      }
    }
    if (exists $params->{tivolimapping}) {
      foreach (keys %{$params->{tivolimapping}}) {
        $tivoliparams->{severity_mappings}->{lc $_} = 0 if 
            $params->{tivolimapping}->{$_} =~ /(?i)ok/;
        $tivoliparams->{severity_mappings}->{lc $_} = 1 if 
            $params->{tivolimapping}->{$_} =~ /(?i)warning/;
        $tivoliparams->{severity_mappings}->{lc $_} = 2 if 
            $params->{tivolimapping}->{$_} =~ /(?i)critical/;
        $tivoliparams->{severity_mappings}->{lc $_} = 3 if 
            $params->{tivolimapping}->{$_} =~ /(?i)unknown/;
        $tivoliparams->{severity_mappings}->{lc $_} =
            $params->{tivolimapping}->{$_} if 
            $params->{tivolimapping}->{$_} =~ /\d/;
      }
    }
    if ($self->{tivoli}->{object} = Nagios::Tivoli::Config::Logfile->new(
        $tivoliparams )) {
    } else {
      die "could not create tivoli object from $params->{tivolipatterns}";
    }
  }
  if (! $self->{type}) {
    if ($self->{rotation}) {
      $self->{type} = "rotating";
    } else {
      $self->{type} = "simple";
    }
  }
  $self->{privatestate} = {};
  my $class = sprintf "Nagios::CheckLogfiles::Search::%s",
      join "::", map {
          (uc substr($_, 0, 1)).substr($_, 1);
      } split(/::/, $self->{type});
  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->{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->{linenumber} = 0; # used for context
  $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->default_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, maxlength => 0, 
      lookback => 0, context => 0,
      warningthreshold => 0, criticalthreshold => 0, unknownthreshold => 0 } );
  $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}) {
    my $pattern = $self->{prefilter};
    $self->resolve_macros_in_pattern(\$pattern);
    $pattern = '(?i)'.$pattern unless $self->{options}->{case};
    $self->addfilter(1, $pattern);
  }
  if ($self->{options}->{syslogclient}) {
    my $pattern = $self->{options}->{syslogclient};
    $self->resolve_macros_in_pattern(\$pattern);
    $pattern = '(?i)'.$pattern unless $self->{options}->{case};
    $self->addfilter(1, $pattern);
  }
  if ($self->{options}->{syslogserver}) {
    my $pattern = '($CL_HOSTNAME$|localhost)';
    $self->resolve_macros_in_pattern(\$pattern);
    $pattern = '(?i)'.$pattern unless $self->{options}->{case};
    $self->addfilter(1, $pattern);
  }
  #
  # the guy who begged me for the encoding option never wrote me a mail again.
  # this means for me, encoding works perfect. if it does not work for you
  # then it's not my problem.
  #
  if ($self->{options}->{encoding}) {
    #require Encode qw(encode decode);
    require Encode;
  }
  #
  #  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 ((! $self->{options}->{(lc $level).'threshold'}) &&
          ($params->{(lc $level).'threshold'})) {
        $self->{options}->{(lc $level).'threshold'} =
            $params->{(lc $level).'threshold'};
      }
      if ($self->{options}->{(lc $level).'threshold'}) {
        $self->{threshold}->{$level} = $self->{options}->{(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);
    }
  }
  if (exists $self->{tivoli}->{object}) {
    $self->{patterns} = { OK => [], WARNING => [],
        CRITICAL => ['.*'], UNKNOWN => [] };
    $self->{tivoli}->{object}->set_format_mappings(
      hostname => $self->{macros}->{CL_HOSTNAME},
      fqhostname => $self->{macros}->{CL_FQDN},
      origin => $self->{macros}->{CL_IPADDRESS},
      FILENAME => (ref($self) eq 'Nagios::CheckLogfiles::Search::Eventlog') ?
          'EventLog' : $self->{macros}->{CL_LOGFILE},
          # oder SysLogD
      LABEL => $self->{macros}->{CL_HOSTNAME}, # NON-TME
    );
  }
  $self->construct_seekfile();
  $self->{NH_detection} = ($^O =~ /MSWin/) ? 0 : 1;
  return $self;
}

sub construct_seekfile {
  my $self = shift;
  # since 2.0 the complete path to the logfile is mapped to the seekfilename
  $self->{seekfilebase} = $self->{logfile};
  $self->{seekfilebase} =~ s/\//_/g;
  $self->{seekfilebase} =~ s/\\/_/g;
  $self->{seekfilebase} =~ s/:/_/g;
  $self->{seekfilebase} =~ s/\s/_/g;
  $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{seekfilebase},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
  $self->{pre3seekfile} = sprintf "/tmp/%s.%s.%s",
      $self->{cfgbase}, $self->{seekfilebase},
      $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 finish {
  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}) 
        if $self->{newstate}->{servicestateid}; # maybe this was the 1st time
  }
  $self->savestate();
  $self->finish();
  $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 pre2seekfile %s instead", $self->{pre2seekfile});
    if (-f $self->{pre2seekfile}) {
      $self->trace("pre-2.0 seekfile %s found. rename it to %s",
          $self->{pre2seekfile}, $self->{seekfile});
      mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir};
      rename $self->{pre2seekfile}, $self->{seekfile};
      $self->trace("and call load_state again");
      $self->loadstate() if -f $self->{seekfile};
      return $self;
    }
    $self->trace("try pre3seekfile %s instead", $self->{pre3seekfile});
    if (-f $self->{pre3seekfile}) {
      $self->trace("pre-3.0 seekfile %s found. rename it to %s",
          $self->{pre3seekfile}, $self->{seekfile});
      mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir};
      rename $self->{pre3seekfile}, $self->{seekfile};
      $self->trace("and call load_state again");
      $self->loadstate() if -f $self->{seekfile};
      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});
  }
  if (exists $self->{laststate}->{privatestate}) {
    $self->{privatestate} = $self->{laststate}->{privatestate};
    $self->trace("found private state %s", 
        Data::Dumper::Dumper($self->{privatestate}));
  }
  if (! $self->{laststate}->{runcount}) {
    $self->{laststate}->{runcount} = 1;
  } else {
    $self->{laststate}->{runcount}++;
  }
  if (! $self->{laststate}->{runtime}) {
    $self->{laststate}->{runtime} = 0;
  }
  $self->{privatestate}->{lastruntime} = $self->{laststate}->{runtime};
  $self->{privatestate}->{runcount} = $self->{laststate}->{runcount};
  $self->{macros}->{CL_LAST_RUNTIME} = $self->{privatestate}->{lastruntime};
  $self->{macros}->{CL_RUN_COUNT} = $self->{privatestate}->{runcount};
  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;
  my $now = time;
  $self->searchresult(); # calculate servicestateid and serviceoutput
  if ($self->{options}->{sticky}) {
    if ($self->{report} ne 'short') {
      $self->{newstate}->{matchlines} = $self->{matchlines};
    }
    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} = $now;
        $self->trace("refresh laststicked");
        # dont forget to count the sticky error
        if ($self->{report} ne 'short') {
          foreach my $level (qw(OK WARNING CRITICAL UNKNOWN)) {
            my $servicestateid =
                {'OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3}->{$level};
            foreach my $event (
                reverse @{$self->{laststate}->{matchlines}->{$level}}) {
              $self->addfirstevent($servicestateid, $event);
            }
          }
        } else {
          $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} = "";
          if ($self->{report} ne 'short') {
            delete $self->{newstate}->{matchlines};
          }
        } else {
          # newstate is 0 because nothing happened in this scan
          # after maxstickytime do not carry on with this error.
          if (($now - $self->{laststate}->{laststicked}) >
              $self->{maxstickytime}) {
            $self->trace("maxstickytime %d expired", $self->{maxstickytime});
            $self->{newstate}->{laststicked} = 0;
            $self->{newstate}->{servicestateid} = 0;
            $self->{newstate}->{serviceoutput} = "";
            if ($self->{report} ne 'short') {
              delete $self->{newstate}->{matchlines};
            }
          } 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})); 
            if ($self->{report} ne 'short') {
              foreach my $level (qw(OK WARNING CRITICAL UNKNOWN)) {
                my $servicestateid =
                  {'OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3}->{$level};
                foreach my $event (
                    reverse @{$self->{laststate}->{matchlines}->{$level}}) {
                  $self->addfirstevent($servicestateid, $event);
                }
              }
            } else {
              $self->addevent($self->{newstate}->{servicestateid},
                  $self->{newstate}->{serviceoutput});
            }
          }  	  		
        }
      }
    } else {
      $self->trace("no sticky error from last run");
      if ($self->{newstate}->{servicestateid}) {
        $self->{newstate}->{laststicked} = $now;
        $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};
  $self->{newstate}->{privatestate} = $self->{privatestate};
  $self->{newstate}->{runcount} = $self->{laststate}->{runcount};
  $self->{newstate}->{runtime} = $now;
  mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir};
  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 ($self->{options}->{maxlength}) {
    $errormessage = substr $errormessage, 0, $self->{options}->{maxlength};
  }
  if ($level =~ /^\d/) {
    $level = (qw(OK WARNING CRITICAL UNKNOWN))[$level];
  }
  push(@{$self->{matchlines}->{$level}}, $errormessage);
  $self->{lastmsg}->{$level} =
      ${$self->{matchlines}->{$level}}[$#{$self->{matchlines}->{$level}}];
  #if ($self->{report} ne "short") {
  #  push(@{$self->{history}}, 
  #      sprintf "%s:%d", $level, scalar(@{$self->{matchlines}->{$level}}) - 1);
  #}
}

sub update_context {
  my $self = shift;
  my $follow = shift;
  my $line = shift;
  
}

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}}];
  #if ($self->{report} ne "short") {
  #  unshift(@{$self->{history}}, sprintf "%s:%d", $level, 0);
  #}
}

#
#  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::encode("ascii", 
            Encode::decode($self->{options}->{encoding}, $line));
        # the input stream is somewhat binary, so chomp doesn't know
        # it neads to remove \r\n on windows.
        $line =~ s/$1/\n/g if $line =~ /(\r\n?|\n\r?)/;
      }
      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;
      $self->{linenumber}++;
      $self->update_context(0, $line); # store this line as before
      foreach my $nagioslevel qw(CRITICAL WARNING UNKNOWN) {
        my $level = $nagioslevel; # because it needs to be modified
        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->{tivoli}->{object}) {
              $self->{tivoli}->{match} = 
                  $self->{tivoli}->{object}->match($line);
              $self->{privatestate}->{tivolimatch} = $self->{tivoli}->{match};
              $level = (qw(OK WARNING CRITICAL UNKNOWN))[$self->{tivoli}->{match}->{exit_code}];
              next if $self->{tivoli}->{match}->{format_name} eq 'NO MATCHING RULE';
              $line = $self->{tivoli}->{match}->{subject};
            }
            if ($self->{options}->{script}) {
              $self->{macros}->{CL_SERVICESTATE} = $level;
              $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}, $self->{privatestate});
              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);
            }
            if ($self->{tivoli}->{object}) {
              delete $self->{privatestate}->{tivolimatch};
            }
          }
        }
        #  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}, $self->{privatestate});
            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} = "";
  }
  if ($self->{option}->{sticky} && $self->{report} ne 'short') {
    # damit long/html output erhalten bleibt und nicht nur der letzte treffer
    $self->{newstate}->{matchlines} = $self->{matchlines};
  }
}

sub reset {
  my $self = shift;
  $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" };
  $self->{negpatterncnt} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] };
  $self->{thresholdcnt} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 };
  #$self->{preliminaryfilter} = { SKIP => [], NEED => [] };
  $self->{perfdata} = "";
  foreach my $level qw(CRITICAL WARNING UNKNOWN) {
    foreach my $pat (@{$self->{negpatterns}->{$level}}) {
      push(@{$self->{negpatterncnt}->{$level}}, 0);
    }
  }
  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};
  }
  delete $self->{lastlogoffset};
  delete $self->{lastlogtime};
  delete $self->{lastlogoffset};
  delete $self->{lastlogfile};
  delete $self->{newlogoffset};
  delete $self->{newlogtime};
  delete $self->{newdevino};
  delete $self->{newlogfile};
  $self->{relevantfiles} = [];
  $self->{logrotated} = 0;
  $self->{logmodified} = 0;
  $self->{linesread} = 0;
  $self->{relevantfiles} = [];
  if (exists $self->{options}->{sticky}) {
    $self->{options}->{sticky} = 1 if ($self->{options}->{sticky} > 1);
  }
  return $self;
}


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;
  # sollte mal raus, da gibts kein sub dazu.
  # hier kommt eh keiner her, weil eins hoeher geblesst wird
  # $self->rotationpattern();
  return $self->init(shift);
}
 
sub analyze_situation {
  my $self = shift;
  $self->{logrotated} = 0;
  $self->{logmodified} = 0;
  if (! $self->{NH_detection}) {
    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;
  } else {
    # Nigel Harnimans mtime-based algorithm
    my $filetime = (stat $self->{logfile})[9];
    my $lastfiletime = $self->{laststate}->{logtime};
  
    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;
  
      # Ok, we need to make some changes here to handle a situation where the
      # inode is not changed on file rotation (since the writing app need
      # continuity)
      # 1)    The last modified time is the same as that of the previously scanned 
      #       log file. Therefore it is the same file. No rotation or modification
      # 2)    The last modified time is different, and the file is zero bytes:
      #       - Modified = false
      #       - Rotated = true
      # 3)    The last modified time is different, and the file is not zero bytes
      #       and is less than previous:
      #       - Modified = true
      #       - Rotated = true
      # 4)    The last modified time is different, and the file is not zero bytes
      #       and is more than previous:
      #       - Modified = true
      #       - Rotated = true (we can't actually tell, so need to play safe)
    } elsif ($filetime == $lastfiletime) {
      $self->trace(sprintf "Log file has the same modified time: %s ",
          scalar localtime($filetime));
      $self->{laststate}->{logtime} = $filetime;
    } elsif ($filetime != $lastfiletime) {
      $self->trace(sprintf "Log file modified time: %s, last modified time: %s",
          scalar localtime($filetime),
          scalar localtime($lastfiletime));
      if ($self->getfilesize($self->{logfile}) == 0) {
        $self->trace(sprintf "Log file is zero bytes");
        $self->{logrotated} = 1;
      } else {
        $self->trace(sprintf "Log file is not zero bytes");
        $self->{logrotated} = 1;
        $self->{logmodified} = 1;
      }
    } else {
      $self->trace("I HAVE NO IDEA WHAT HAPPENED");
    }
    $self->trace(sprintf "Log offset: %i",
        $self->{laststate}->{logoffset});
    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.
      #
      if (! $self->{NH_detection}) {
        $self->{laststate}->{logoffset} = 0;
      } else {
        # NH Commented this out, as we may find no rotated files,
        # in which case we need to use the current file offset again
      }
      $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});
    if ($self->{NH_detection}) {
      # NH In this case, we have replaced the files, so set to beginning
      $self->{laststate}->{logoffset} = 0;
    } else {
      $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 = ();
  if (! $self->{filenamepattern}) {
    $self->{filenamepattern} = $self->{rotation};
    $self->resolve_macros_in_pattern(\$self->{filenamepattern});
  }
  # 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->{macros}->{CL_LOGFILE} = $self->{logfile};
    $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->{seekfilebase} = dirname($self->{logfile}).'/uniformlogfile';
  $self->{seekfilebase} =~ s/\//_/g;
  $self->{seekfilebase} =~ s/\\/_/g;
  $self->{seekfilebase} =~ s/:/_/g;
  $self->{seekfilebase} =~ s/\s/_/g;
  $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir},
      $self->{cfgbase}, $self->{seekfilebase},
      $self->{tag} eq "default" ? "seek" : $self->{tag};
  $self->{pre3seekfile} = sprintf "/tmp/%s.%s.%s",
      $self->{cfgbase}, $self->{seekfilebase},
      $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->default_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}, $self->{privatestate});
  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->{privatestate} = $params->{privatestate};   
  $self->default_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}, $self->{privatestate});
  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}) {
      # strings containing 0 must be treated like a true value
      #$actionoutput = "postscript" if ! $actionoutput;
      $actionoutput = "postscript" 
          unless $actionoutput || $actionoutput =~ /0[0\.]*/;
      $self->addevent($actionrc, $actionoutput);
    }
  }
  $self->{exitcode} = $actionrc;
}


package Nagios::CheckLogfiles::Search::Dummy;

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/dummy.%s", $self->{seekfilesdir},
      $self->{tag};
  $self->SUPER::init($params);
}
  
sub prepare {
  my $self = shift;
  $self->{options}->{nologfilenocry} = 1;
}

sub loadstate {
  my $self = shift;
  $self->SUPER::loadstate();
}

sub savestate {
  my $self = shift;
}

sub analyze_situation {
  my $self = shift;
}

sub collectfiles {
  my $self = shift;
}

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},
    errorresource => $params->{errpt}->{errorresource},
  };
  $self->addfilter(0, 'IDENTIFIER TIMESTAMP');
}
  
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;
  foreach (keys %{$self->{laststate}}) {
    $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 %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->{clo}->{errorresource} ? '-N '.$self->{clo}->{errorresource} : "";
    $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::Ipmitool;

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

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} : "/usr/bin/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(Digest::MD5::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->{oraalert}->{tns} = {
    connect => $params->{oraclealertlog}->{connect} ||
        $params->{oraclealertlog}->{sid},
    username => $params->{oraclealertlog}->{username},
    password => $params->{oraclealertlog}->{password},
  };
  $self->{logfile} = sprintf "%s/alertlog.%s.%s", $self->{seekfilesdir},
      $self->{tag},
      $self->{oraalert}->{tns}->{connect};
  $self->SUPER::init($params);
  $self->resolve_macros(\$self->{oraalert}->{tns}->{connect});
  $self->resolve_macros(\$self->{oraalert}->{tns}->{username});
  $self->resolve_macros(\$self->{oraalert}->{tns}->{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}->{highestfound} = 0;
}

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.
  # hopefully the clocks are synchronized
  $self->{laststate}->{logtime} = $self->{laststate}->{logtime} ?
      $self->{laststate}->{logtime} : time - 300;
}

sub savestate {
  my $self = shift;
    foreach (keys %{$self->{laststate}}) {
      $self->{newstate}->{$_} = $self->{laststate}->{$_};
    }
  # remember the last second scanned.
  $self->{newstate}->{logtime} = $self->{oraalert}->{highestfound} ?
      $self->{oraalert}->{highestfound} : $self->{laststate}->{logtime};
  $self->SUPER::savestate();
}

sub analyze_situation {
  my $self = shift;
  $self->trace("last scanned until %s",
      scalar localtime $self->{laststate}->{logtime});
  $self->{logmodified} = 1;
}

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 now (db now, not plugin now)
    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 })) {
        $dbh->do(q{ ALTER SESSION SET NLS_NUMERIC_CHARACTERS=".," });
        # suchen bis zur letzten abgeschlossenen sekunde (inklusive)
        my $sql = q{
            SELECT alert_timestamp, alert_text FROM alert_log 
            WHERE ROUND(alert_timestamp) > ? AND alert_date <= SYSDATE - 1/86400
            ORDER BY alert_timestamp
        };
        if (my $sth = $dbh->prepare($sql)) {
          $self->trace(sprintf "select events between %d and now (%s and sysdate())",     
              $self->{laststate}->{logtime},
              scalar localtime $self->{laststate}->{logtime});
          $sth->execute($self->{laststate}->{logtime});
          if (my $fh = new IO::File($self->{logfile}, "w")) {
            while(my($alert_timestamp, $alert_text) = $sth->fetchrow_array()) {
              next if ! $alert_text; # es gibt auch leere Zeilen
              # bei ora-perl-conversion gibts manchmal 1234567890.999999999
              $alert_timestamp = int(0.5 + $alert_timestamp);
              $fh->printf("%s %s\n", scalar localtime $alert_timestamp, $alert_text);     
              $self->{oraalert}->{highestfound} = $alert_timestamp;
              $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 => time, # not relevant because already analyzed
            fingerprint => "0:0" });
      }
    }
  }
}


package main;

use strict;
use utf8;
use File::Basename;
use File::Find;
use Getopt::Long;

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

Getopt::Long::Configure qw(no_ignore_case); # compatibility with old perls
use vars qw (%commandline);
my @cfgfiles = ();
my $needs_restart = 0;
my $enough_info = 0;

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

sub print_version {
  printf "%s v3.0.4\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.

You can find the complete documentation at 
http://www.consol.com/opensource/nagios/check-logfiles
or
http://www.consol.de/opensource/nagios/check-logfiles

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
}

%commandline = ();
my @params = (
    "timeout|t=i",
    "version|V",
    "help|h",
    "debug|d",
    "verbose|v",
    #
    # 
    #
    "environment|e=s%",
    "daemon:i",
    "report=s",
    "reset",
    #
    #
    #
    "install",
    "deinstall",
    "service=s",
    "username=s",
    "password=s",
    #
    # which searches
    #
    "config|f=s",
    "configdir|F=s",
    "searches=s",
    "selectedsearches=s",
    #
    # globals
    #
    "seekfilesdir=s",
    "protocolsdir=s",
    "protocolsretention=i",
    "macro=s%",
    #
    # search
    #
    "template=s",
    "tag=s",
    "logfile=s",
    "rotation=s",
    "tivolipattern=s",
    "criticalpattern=s",
    "criticalexception=s",
    "warningpattern=s",
    "warningexception=s",
    "okpattern=s",
    "type=s",
    "archivedir=s",
    #
    # search options
    #
    "noprotocol",
    "nocase",
    "nologfilenocry",
    "maxlength=i",
    "syslogserver",
    "syslogclient=s",
    "sticky:s",
    "noperfdata",
    "winwarncrit",
    "lookback=s",
    "context=i",
    "criticalthreshold=i",
    "warningthreshold=i",
    "encoding=s",
);
if (! GetOptions(\%commandline, @params)) {
  print_help();
  exit $ERRORS{UNKNOWN};
} 

if (exists $commandline{version}) {
  print_version();
  exit UNKNOWN;
}

if (exists $commandline{help}) {
  print_help();
  exit UNKNOWN;
}

if (exists $commandline{config}) {
  $enough_info = 1;
} elsif (exists $commandline{configdir}) {
  $enough_info = 1;
} elsif (exists $commandline{logfile}) {
  $enough_info = 1;
} elsif (exists $commandline{type} && $commandline{type} =~ /^eventlog/) {
  $enough_info = 1;
} elsif (exists $commandline{deinstall}) {
  $commandline{type} = 'dummy';
  $enough_info = 1;
}
if (exists $commandline{lookback}) {
  if ($commandline{lookback} =~ /^(\d+)(s|m|h|d)$/) {
    if ($2 eq 's') {
      $commandline{lookback} = $1;
    } elsif ($2 eq 'm') {
      $commandline{lookback} = $1 * 60;
    } elsif ($2 eq 'h') {
      $commandline{lookback} = $1 * 60 * 60;
    } elsif ($2 eq 'd') {
      $commandline{lookback} = $1 * 60 * 60 *24;
    }
  } else {
    printf STDERR "illegal time interval (must be <number>[s|m|h|d]\n";
    print_usage();
    exit UNKNOWN;
  }
}

if (! $enough_info) {
  print_usage();
  exit UNKNOWN;
}

if (exists $commandline{daemon}) {
  my @newargv = ();
  foreach my $option (keys %commandline) {
    if (grep { /^$option/ && /=/ } @params) {
      push(@newargv, sprintf "--%s", $option);
      push(@newargv, sprintf "%s", $commandline{$option});
    } else {
      push(@newargv, sprintf "--%s", $option);
    }
  }
  $0 = 'check_logfiles '.join(' ', @newargv);
  if (! $commandline{daemon}) {
    $commandline{daemon} = 300;
  }
}
if (exists $commandline{environment}) {
  # if the desired environment variable values are different from
  # the environment of this running script, then a restart is necessary.
  # because setting $ENV does _not_ change the environment of the running script.
  foreach (keys %{$commandline{environment}}) {
    if ((! $ENV{$_}) || ($ENV{$_} ne $commandline{environment}->{$_})) {
      $needs_restart = 1;
      $ENV{$_} = $commandline{environment}->{$_};
    }
  }
}
if ($needs_restart) {
  my @newargv = ();
  foreach my $option (keys %commandline) {
    if (grep { /^$option/ && /=/ } @params) {
      if (ref ($commandline{$option}) eq "HASH") {
        foreach (keys %{$commandline{$option}}) {
          push(@newargv, sprintf "--%s", $option);
          push(@newargv, sprintf "%s=%s", $_, $commandline{$option}->{$_});
        }
      } else {
        push(@newargv, sprintf "--%s", $option);
        push(@newargv, sprintf "%s", $commandline{$option});
      }
    } else {
      push(@newargv, sprintf "--%s", $option);
    }
  }
  exec $0, @newargv;
  # this makes sure that even a SHLIB or LD_LIBRARY_PATH are set correctly
  # when the perl interpreter starts. Setting them during runtime does not
  # help loading e.g. libclntsh.so
  exit;
}

if (exists $commandline{configdir}) {
  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, $commandline{configdir});
  @cfgfiles = sort { $a cmp $b } @cfgfiles;
}
if (exists $commandline{config}) {
  # -f is always first
  unshift(@cfgfiles, $commandline{config});
}
if (scalar(@cfgfiles) == 1) {
  $commandline{config} = $cfgfiles[0];
} elsif (scalar(@cfgfiles) > 1) {
  $commandline{config} = \@cfgfiles;
}
if (exists $commandline{searches}) {
  $commandline{selectedsearches} = $commandline{searches};
}
if (! exists $commandline{selectedsearches}) {
  $commandline{selectedsearches} = "";
}
if (exists $commandline{type}) {
  my ($type, $details) = split(":", $commandline{type});
}
if (exists $commandline{criticalpattern}) {
  $commandline{criticalpattern} = '.*' if
      $commandline{criticalpattern} eq 'match_them_all';
  delete $commandline{criticalpattern} if
      $commandline{criticalpattern} eq 'match_never_ever';
}
if (exists $commandline{warningpattern}) {
  $commandline{warningpattern} = '.*' if
      $commandline{warningpattern} eq 'match_them_all';
  delete $commandline{warningpattern} if
      $commandline{warningpattern} eq 'match_never_ever';
}
if (my $cl = Nagios::CheckLogfiles->new({
      cfgfile => $commandline{config} ? $commandline{config} : undef,
      searches => [ 
          map {
            if (exists $commandline{type}) {
              # "eventlog" or "eventlog:eventlog=application,source=cdrom"
              my ($type, $details) = split(":", $commandline{type});
              $_->{type} = $type;
              if ($details) {
                $_->{$type} = {};
                foreach my $detail (split(",", $details)) {
                  my ($key, $value) = split("=", $detail);
                  $_->{$type}->{$key} = $value;
                }
              }
            }
            $_;
          }
          map { # ausputzen
              foreach my $key (keys %{$_}) { 
      	      delete $_->{$key} unless $_->{$key}}; $_;
          } ({
          tag => 
              $commandline{tag} ? $commandline{tag} : undef,
          logfile => 
              $commandline{logfile} ? $commandline{logfile} : undef,
          type => 
              $commandline{type} ? $commandline{type} : undef,
          rotation => 
              $commandline{rotation} ? $commandline{rotation} : undef,
          tivolipatterns =>
              $commandline{tivolipattern} ?
                  $commandline{tivolipattern} : undef,
          criticalpatterns =>
              $commandline{criticalpattern} ?
                  $commandline{criticalpattern} : undef,
          criticalexceptions =>
              $commandline{criticalexception} ?
                  $commandline{criticalexception} : undef,
          warningpatterns =>
              $commandline{warningpattern} ?
                  $commandline{warningpattern} : undef,
          warningexceptions =>
              $commandline{warningexception} ?
                  $commandline{warningexception} : undef,
          okpatterns =>
              $commandline{okpattern} ?
                  $commandline{okpattern} : undef,
          options => join(',', grep { $_ }
              $commandline{noprotocol} ? "noprotocol" : undef,
              $commandline{nocase} ? "nocase" : undef,
              $commandline{noperfdata} ? "noperfdata" : undef,
              $commandline{winwarncrit} ? "winwarncrit" : undef,
              $commandline{nologfilenocry} ? "nologfilenocry" : undef,
              $commandline{syslogserver} ? "syslogserver" : undef,
              $commandline{syslogclient} ? "syslogclient=".$commandline{syslogclient} : undef,
              $commandline{maxlength} ? "maxlength=".$commandline{maxlength} : undef,
              $commandline{lookback} ? "lookback=".$commandline{lookback} : undef,
              $commandline{context} ? "context=".$commandline{context} : undef,
              $commandline{criticalthreshold} ? "criticalthreshold=".$commandline{criticalthreshold} : undef,
              $commandline{warningthreshold} ? "warningthreshold=".$commandline{warningthreshold} : undef,
              $commandline{encoding} ? "encoding=".$commandline{encoding} : undef,
              defined $commandline{sticky} ? "sticky".($commandline{sticky} ? "=".$commandline{sticky} : "") : undef ),
          archivedir =>
              $commandline{archivedir} ?
                  $commandline{archivedir} : undef,
      })],
      selectedsearches => [split(/,/, $commandline{selectedsearches})],
      dynamictag => $commandline{tag} ? $commandline{tag} : undef,
      report => $commandline{report} ? $commandline{report} : undef,
      cmdlinemacros => $commandline{macro},
      seekfilesdir => $commandline{seekfilesdir} ? $commandline{seekfilesdir} : undef,
      protocolsdir => $commandline{protocolsdir} ? $commandline{protocolsdir} : undef,
      protocolsretention => $commandline{protocolsretention} ? $commandline{protocolsretention} : undef,
      reset => $commandline{reset} ? $commandline{reset} : undef,
  })) {
  $cl->{verbose} = $commandline{verbose} ? 1 : 0;
  $cl->{timeout} = $commandline{timeout} ? $commandline{timeout} : 60;
  if ($commandline{install}) {
    $cl->install_windows_service($commandline{service}, $commandline{config},
        $commandline{username}, $commandline{password});
  } elsif ($commandline{deinstall}) {
    $cl->deinstall_windows_service($commandline{service});
  } elsif ($commandline{daemon}) {
    $cl->run_as_daemon($commandline{daemon});
  } else {
    $cl->run();
  }
  printf "%s%s\n%s", $cl->{exitmessage},
      $cl->{perfdata} ? "|".$cl->{perfdata} : "",
      $cl->{long_exitmessage} ? $cl->{long_exitmessage}."\n" : "";
  exit $cl->{exitcode};
} else {
  printf "%s\n", $Nagios::CheckLogfiles::ExitMsg;
  exit $Nagios::CheckLogfiles::ExitCode;
}

