#!/usr/bin/perl -T

#------------------------------------------------------------------------------
# This is amavisd-agent, a demo program to display
# SNMP-like counters updated by amavisd-new.
#
# Author: Mark Martinec <mark.martinec@ijs.si>
# Copyright (C) 2004  Mark Martinec,  All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
# * Neither the name of the author, nor the name of the "Jozef Stefan"
#   Institute, nor the names of contributors may be used to endorse or
#   promote products derived from this software without specific prior
#   written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#(the license above is the new BSD license, and pertains to this program only)
#
# Patches and problem reports are welcome.
# The latest version of this program is available at:
#   http://www.ijs.si/software/amavisd/
#------------------------------------------------------------------------------

use strict;
use re 'taint';

use Time::HiRes ();
use BerkeleyDB;

use vars qw($VERSION);  $VERSION = 1.02;
use vars qw(%keys %virus %types %history $avg_int $uptime);

$avg_int = 5*60;  # 5 minute interval

sub p1($$@) {
  my($k,$avg,@tot_k) = @_;
  printf("%-23s %6d %6.0f/h", $k, $keys{$k}, $avg*3600);
  for my $tot_k (@tot_k) {
    if ($keys{$tot_k} <= 0) {
      printf("    --- %%")
    } else {
      printf(" %6.1f %%", 100*$keys{$k}/$keys{$tot_k})
    }
    print " ($tot_k)";
  }
  print "\n";
}

sub p2($$$) {
  my($k,$avg,$tot_k) = @_;
  printf("%-23s %6d %6.0f/h %6.1f %% (%s)\n",
         $k, $virus{$k}, $avg*3600, 100*$virus{$k}/$keys{$tot_k}, $tot_k);
}

sub enqueue($$$$) {
  my($name,$now,$val,$hold_time) = @_;
  if (ref $history{$name} ne 'ARRAY') { $history{$name} = [] }
  my($oldest_useful);
  for my $j (0..$#{$history{$name}}) {
    if ($history{$name}->[$j][0] + $hold_time >= $now)
      { $oldest_useful = $j; last }
  }
  if (defined $oldest_useful) {
    @{$history{$name}} =
      @{$history{$name}}[$oldest_useful..$#{$history{$name}}];
  }
  push(@{$history{$name}}, [$now,$val]);
  my($average,$dv,$dt); my($n) = scalar(@{$history{$name}});
  my($oldest) = $history{$name}->[0];
  my($latest) = $history{$name}->[$n-1];
  $dt = $latest->[0] - $oldest->[0];  $dv = $latest->[1] - $oldest->[1];
  if ($n < 2 || $dt < $hold_time/2) {
    $dt = $uptime; $dv = $val;  # average since the start time
  }
  if ($dt > 0) { $average = $dv/$dt }
  ($average, $dv, $dt, $n);
}

sub fmt_ticks($) {
  my($t) = @_;
  my($hh)= $t % 100; $t = int($t/100);
  my($s) = $t % 60;  $t = int($t/60);
  my($m) = $t % 60;  $t = int($t/60);
  my($h) = $t % 24;  $t = int($t/24);
  my($d) = $t;
  sprintf("%d days, %d:%02d:%02d.%02d", $d,$h,$m,$s,$hh);
};

# main program starts here
  $SIG{INT} = sub { die "\n" };  # do the END code block
  my($env) = BerkeleyDB::Env->new(
    '-Home'=>'/var/amavis/db', '-Flags'=> DB_INIT_CDB | DB_INIT_MPOOL);
  defined $env or die "BDB no env: $BerkeleyDB::Error $!";
  my($db) = BerkeleyDB::Hash->new(
    '-Filename'=>'snmp.db', '-Flags'=>DB_RDONLY, '-Env'=>$env );
  defined $db or die "BDB no dbS 1: $BerkeleyDB::Error $!";
  my($cursor);

  $| = 1;
  my($stat,$key,$val);
  for (;;) {
    %keys = (); %virus = (); %types = ();
    my($now); my($eval_stat,$interrupt); $interrupt = '';
    print "\n";
    { my($h1) = sub { $interrupt = $_[0] };
      local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
      eval {
        $cursor = $db->db_cursor;  # obtain read lock
        defined $cursor or die "db_cursor error: $BerkeleyDB::Error";
        $now = Time::HiRes::time;
        while ( ($stat=$cursor->c_get($key,$val,DB_NEXT)) == 0 ) {
          if    ($key =~ /^virus\.byname\.(.*)\z/s) { $virus{$1} = $val }
          else { $keys{$key} = $val }
        }
        $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error $!";
        $cursor->c_close==0 or die "c_close error: $BerkeleyDB::Error";
        $cursor = undef;
      };
      $eval_stat = $@;
      if (defined $db) {
        $cursor->c_close  if defined $cursor;  # unlock, ignoring status
        $cursor = undef;
      }
    }
    if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
    elsif ($eval_stat ne '') { chomp($eval_stat); die "BDB $eval_stat\n" }
    for my $k (sort keys %keys) {
      if ($keys{$k} =~ /^C32 (.*)\z/) {
        $keys{$k} = $1;
      } elsif ($k eq 'sysUpTime' && $keys{$k} =~ /^INT (.*)\z/) {
        $uptime = $now - $1; my($ticks) = int($uptime*100);
        printf("%-15s %s %s (%s)\n",
               $k,'Timeticks', $ticks, fmt_ticks($ticks));
        delete($keys{$k});
      } else {
        printf("%-15s %s\n", $k,$keys{$k});
        delete($keys{$k});
      }
    }
    for (sort keys %keys) {
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $keys{$_}, $avg_int);
      if    (/^OpsDecTyp/)    {}  # later
      elsif (/^CacheHitsVirusMsgs$/)  { p1($_,$avg,'ContentVirusMsgs') }
      elsif (/^CacheHitsBannedMsgs$/) { p1($_,$avg,'ContentBannedMsgs') }
      elsif (/^CacheHitsSpamMsgs$/)   { p1($_,$avg,'ContentSpamMsgs') }
      elsif (/^Cache/)        { p1($_,$avg,'CacheAttempts') }
      elsif (/^Content/)      { p1($_,$avg,'InMsgs') }
      elsif (/^Quar/)         { p1($_,$avg,'QuarMsgs') }
      elsif (/^OpsSql/)       { p1($_,$avg,'InMsgsRecips') }
      elsif (/^(InMsgs|Ops)/) { p1($_,$avg,'InMsgs') }
      elsif (/^Out/)          { p1($_,$avg,'OutMsgs') }
      else                    { p1($_,$avg,undef) }
    }
    for (sort { $keys{$b} <=> $keys{$a} } grep {/^OpsDecTyp/} keys %keys) {
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $keys{$_}, $avg_int);
      p1($_,$avg,'InMsgs');
    }
    for (keys %virus) { $virus{$_} = $1 if $virus{$_} =~ /^C32 (.*)\z/ }
    for (sort { $virus{$b} <=> $virus{$a} } keys %virus) {
      my($avg,$dv,$dt,$n) = enqueue($_, $now, $virus{$_}, $avg_int);
      p2($_,$avg,'ContentVirusMsgs');
    }
    sleep 10;
  }

END {
  if (defined $db) {
    $cursor->c_close  if defined $cursor;  # ignoring status
    $db->db_close==0 or die "BDB db_close error: $BerkeleyDB::Error $!";
  }
  print STDERR "exited\n";
}
