#!/usr/bin/perl
#
# nsc.pl - curses-based tty console monitor for Nagios
# 000206shj 
#
# Copyright (C) Stig H. Jacobsen & Gothix 2000-2005
#
# E-mail: nsc@gothix.biz
#


# --- Standard & CPAN modules ---------------------------------------------

use strict;
use warnings;

use Getopt::Long           qw( GetOptions );
use File::Spec::Functions  qw( catfile );
use Sys::Hostname          qw( hostname );
use Sys::Syslog;


# --- My modules ----------------------------------------------------------

use FindBin qw( $RealBin );
use lib $RealBin;

#whilst debugging, testing, developing, look for gothix:: in cwd first
use lib '.';

use gothix::misc qw( @daytab_long @montab_long center ctrl mmax numeric rpad tdif trim);
use gothix::subversion;
use gothix::xcurses;
use gothix::area;
use gothix::cpusage;

use nsc_nagios;


# =========================================================================
#                           G L O B A L S         
# =========================================================================

# --- Constants -----------------------------------------------------------

my $SVN_REVISION = undef;
{
   my $svn = svn_idtag('$Id: nsc.pl 2901 2005-05-23 13:59:12Z shj $');
   $SVN_REVISION = $svn->{revision};
}

my $VERSION = svn_release('$HeadURL: svn://dax/tags/shj-devel/nsc/0.80-2/nsc.pl $');
$VERSION = 'v' . $VERSION if $VERSION;
$VERSION = '(unreleased)' unless $VERSION;

my $myname = 'nsc';     #as displayed to users
my $me = $myname;       #as used in .conf etc.

my $myversion = $VERSION;
$myversion .= sprintf(' (r%02d.%02d)', ${SVN_REVISION}/100, ${SVN_REVISION}%100)
   if ($SVN_REVISION && $VERSION =~ /^.*(a|b).*$/); #don't show for final versions

my $fnConfig = catfile($ENV{'HOME'}, ".$me.conf");


# --- Global variables ----------------------------------------------------

my $statuslog = undef;        #status.log object

my $MIN_LINES = 10;
my $MIN_COLS = 50;

my ($a_version,
   $a_clock,
   $a_debug1,
   $a_update,
   $a_headers,
   $a_services,
   $a_blank,
   $a_options,
   $a_nodecnt,
   $a_msgs,
   $a_svccnt
);

my $opt_colour;
my $num_msglines;
my $first_msgline;
#my $BOTLINE;

#Replacement terminal-types and their defaults
my %TERMINALS = (
   'linux.vt100'   => 'xterm-color',
   'sunos.vt100'   => 'xterm-color',
   'freebsd.vt100' => 'vt340-color',
);
my @uname;

my $lastBeep = time();        #now on startup, to avoid initial bell
my $listoffs = 0;
my $display_dirty = 1;
my $num_displays = 0;         #number of times service list has been shown
my %AT = ();
my $LogName = (getpwuid($>))[0];
my $boot_time = time();

my $shorthost = hostname();
$shorthost =~ s/\..*$//g;     #loose domain, tld

my $cpusage = undef;

my $filter_sub = undef;
my @filter_hosts;

my %sVals = (
      'CRITICAL',    0,
      'HOST DOWN',   1,
      'UNREACHABLE', 2,
      'WARNING',     3,
      'UNKNOWN',     4,
      'ACK/',        5,
      'PENDING',     6,
      'RECOVERY',    7,
      'OK',          8
      );

$ENV{'EDITOR'} = 'vi' unless $ENV{'EDITOR'};
$ENV{'VISUAL'} = $ENV{'EDITOR'} unless $ENV{'VISUAL'};
$ENV{'PAGER'} = 'more' unless $ENV{'PAGER'};


# --- Global status data --------------------------------------------------

my @statusList;               #list of hash-refs

my $pgmState;


# label: text used for headings
# width: factory standard with for 80 cols
# cw: current width adjusted for current window width
# right: true/set if content is to be right-justified (numbers)
my %DataCols = (
   '0_node'      => {
      'label'  => 'Host',
      'width'  => 10
      },
   '1_service'   => {
      'label'  => 'Service',
      'width'  => 16
      },
   '2_status'    => {
      'label'  => ' Status',
      'width'  => 8
      },
   '3_updated'   => {
      'label'  => 'Upd/Chg',
      'width'  => 7,
      'align'  => '/',
      },
   '4_tries'     => {
      'label'  => 'Try',
      'width'  => 3,
      'right'  => 1
      },
   '5_message'   => {
      'label'  => 'Service information',
      'width'  => -1
      }
);

#init cw col
foreach my $k (keys %DataCols) {
   $DataCols{$k}->{'cw'} = mmax($DataCols{$k}->{'width'},
                                length( $DataCols{$k}->{label} ));
   #align data with '/' in header
   if ($DataCols{$k}->{align}) {
      my $pos = index($DataCols{$k}->{label}, $DataCols{$k}->{align});
      $pos = 0 if ($pos < 0);    #not found...
      $DataCols{$k}->{align_pos} = $pos;
   }
}


# --- Debugging support ---------------------------------------------------

{ #localize debug stack and friends

my @DebugStack = ();
my @DebugTM = ();
our $DebugStackSize = 500;

sub show_debug {

   return unless cfg('debug');

   drawScreen();

#   my $line = 0;
#   for (my $i=$#DebugStack; $i>=0 && $line<=$a_services->maxy(); $line++, $i--) {
#      mLine($line, AT('normal'), tdif(time(), $DebugTM[$i]) . ' ' . $DebugStack[$i]);
#      }

   my $spos = $#DebugStack-$a_services->maxy();
   $spos = 0 if ($spos < 0);  #not enough lines
   for (my $lno = 0; $lno <= $a_services->maxy(); $lno++, $spos++) {
      last unless $DebugStack[$spos];
      mLine($lno, AT('normal'), tdif(time(), $DebugTM[$spos]) . ' ' . $DebugStack[$spos]);
   }

   botline("Press any key to continue..");
   rdKey(60);
   botline();

} #show_debug


sub dm {

   return unless cfg('debug');

   if ($#DebugStack > $DebugStackSize) {
      #recycle to avoid consuming all swap within the next 6 years
      @DebugStack = @DebugStack[0..int($DebugStackSize/2)];
      dm('Debug stack reset');
   }
   my $s = join('', @_);
   chomp($s);
   push(@DebugStack, $s);
   push(@DebugTM, time());

} #dm


# Write debug messages to syslog
sub debug_save {

   return unless cfg('debug');

   dm("Debug messages syslogged");     #predict the future?

   openlog($me, 'pid', 'user');

   foreach my $i (0..$#DebugStack) {
      my $logtext = localtime($DebugTM[$i]) . ' ' . $DebugStack[$i];
      syslog('info', $logtext);
   }

} #debug_save


} #end localization


#Debug output to stderr
sub d {

   return unless cfg('debug');

   print STDERR @_, "\n";

} #d




# --- UI stuff ------------------------------------------------------------

my @gAttribs = ();

sub AT {
   fatal("$_[0] is not a defined colour") if !defined($AT{$_[0]});
   return $AT{$_[0]};
}

my $last_botline = '';

sub rdKey {

   #set attr, otherwise cursor will paint in last(?) colour
   #TODO: it doesn't normalize :-(
   #attron(AT('normal'));
   term_attron(AT('botline'));

   my $xp = length($last_botline);
   $xp++ unless ($xp == 0);

   if ($xp >= term_cols()) {
      dm("xp=$xp, reset");
      $xp = 0;
   }

#   abs_gotoxy($xp,$BOTLINE);
   $a_msgs->gotoxy($xp, 0);
   term_refresh();

   my $res = read_key($_[0]);
   dm(sprintf("got key <$res> %d", ord($res))) if defined( $res );

   term_attroff(AT('botline'));

   if (defined($res) and $res eq KEY_RESIZE) {     #SIGWINSZ whilst waiting for a key?
#      check4resize(); 
      resize_term();
      dm(sprintf('Term lines=%d cols=%d', term_lines(), term_cols));
      $res = undef;        #nothing happened...
   }

   return $res;

} #rdKey


sub botline {

   my $new_botline = shift;
   $new_botline = '' unless $new_botline;
   my $forced = shift;

   if ($new_botline ne $last_botline
       or $forced) {
      dm("botline: '$new_botline'");
      $a_msgs->cprint(0, 0, $new_botline, AT('botline'));
      $last_botline = $new_botline;
   }

   term_refresh();

} #botline


sub mLine {

   my ($line,$attrib,$msg) = @_;

   $attrib = AT('normal') if (!defined($attrib));
   $a_services->cprint(0, $line, $msg, $attrib);

} #mLine


# =========================================================================
#                   C O N F I G U R A T I O N
# =========================================================================


# --- Configuration stuff -------------------------------------------------


{ #brace to localize %CFG and friends


my %NSC_KEYWORDS = (

   'nslog',          '/var/lib/nagios/status.dat',
   'hostcfg',        '/etc/nagios/services.cfg',
   'reloadcmd',      '/sbin/service nagios reload',
   'showall',        '1',
   'details',        '1',
   'reverse',        '0',
   'bell',           '1',
   'bell_interval',  '180',         #in seconds
   'sortbynode',     '0',	         # sort by: 0=>status 1=>node
   'debug',          '0',
   'updated',        0,             #last write, time(2)
   'cpumeter',       0,
   'noupd_warn',     60,            #how old status.log can be before we warn
   'noupd_err',      5*60,          #how old status.log can be before we err
   'popart',         0,             #alternate behavior
   #TODO: this should be seconds per 24 lines (..)
   'roll',           60,            #how often to roll, -1=disable
   'roll_wait',      5*60,          #seconds of no user input before roll starts
   'stat_runtime',   0,
   'stat_screens',   0,
   'version',        $VERSION,
   'usercmd_T',      'top',

   'devmode'         => 0,          #When false: Disable assertions

   #H=host, F=fqfn, D=destfile
   'remote.copy_cmd' => 'rsync --compress --rsh="ssh" %H:%F %D >%L 2>&1',
   'remote.copy_interval' => '60',

   #monochrome attributes
   'mono.ack'        => 'underline',
   'mono.botline'    => 'bold',
   'mono.critical'   => 'reverse + bold',
   'mono.dim'        => 'dim',
   'mono.heading'    => 'underline',
   'mono.isok'       => '!normal',
   'mono.noisy'      => 'reverse + bold + blink',
   'mono.normal'     => 'normal',
   'mono.notok'      => 'bold',
   'mono.total'      => 'bold',
   'mono.warning'    => 'reverse',

   #colour attributes
   'colour.ack'      => 'white on magenta',
   'colour.botline'  => '!normal',
   'colour.critical' => 'white on red',
   'colour.dim'      => '!normal',
   'colour.heading'  => 'white on blue + bold',
   'colour.isok'     => 'white on green',
   'colour.noisy'    => 'white on red',
   'colour.normal'   => 'white on blue',
   'colour.notok'    => '!warning',
   'colour.total'    => '!botline',
   'colour.warning'  => 'white on yellow + bold',

   #obsolete keywords
   'upd_freq', '2',

);



my %CFG;                #configuration items
my $cfg_updated = 0;    #time_t of last %CFG change
my $cfg_loaded = 0;     #time_t of last %CFG load from disk
my $cfg_changes = 0;    #number of changes to in-memory %CFG


# cfg($varname) returns value of var
# cfg($varname, $newval) sets value of var, returns new value
sub cfg {

   my($var,$newval) = @_;

   if (defined($newval)) {
      if (defined($CFG{$var})
          && ($CFG{$var} ne $newval)) {   #update only on changes..
         $CFG{$var} = $newval;
         $cfg_updated = time();
         $cfg_changes++;
      } else {
         dm("no cfg change: $var => $newval\n");
      }
   }
   return $CFG{$var};

} #cfg


sub init_gAttribs {

   foreach my $k (keys %NSC_KEYWORDS) {
      next unless ($k =~ /^mono\.(.+)$/);
      push(@gAttribs, $1);
   }

} #initgAttribs


# Return Curses attribute value (for attron()) for named nsc highlighting type
#   $a = decode_mono('botline');    #returns perhaps A_REVERSE
#   attron($a);
sub decode_mono {

   my ($attr) = @_;

   my $ky = 'mono.' . $attr;

   fatal("Unknown monochrome highlighting '$attr'")
      unless defined($CFG{$ky});

   #recurse for indirect colours
   if ($CFG{$ky} =~ /^!(.+)$/) {
      return decode_mono($1);
   }

   return mono2attr($CFG{$ky});

} #decode_mono


# decode_colour()
# As decode_mono(), but for colour attributes
sub decode_colour {

   my ($attr) = @_;
   my $res = undef;

   my $ky = 'colour.' . $attr;

   fatal("Unknown colour highlighting '$attr'")
      unless defined($CFG{$ky});

   #recurse for indirect colours
   if ($CFG{$ky} =~ /^!(.+)$/) {
      return decode_colour($1);
   }

   return colour2attr($CFG{$ky});

} #decode_colour


# --- Terminal configuration ----------------------------------------------

sub cfg_terminal {

   my ($term,$val) = @_;
   my ($os,$type) = split(/\./, $term);

   dm("replacing terminal $os.$type with $val");

   $TERMINALS{"${os}.${type}"} = $val;

} #cfg_terminal


# --- Configuration -------------------------------------------------------

sub SaveConfig {

   my $mtime = (stat($fnConfig))[9];
   if ($mtime && $mtime > $cfg_loaded) {
      #TODO: waiting for a better solution
#      print STDERR "\n***** $me: Configuration file $fnConfig has been changed (and not by me)\n";
#      print STDERR "***** $me: Refusing to overwrite alien configuration changes, sorry\n";
      return;
   }

   #TODO: ?Only write, if there were any changes?
   ##not really practical, if stat_xxx are to be kept up2date

   cfg('updated', localtime() . sprintf(", %d changes", $cfg_changes));

   my $runtime = time() - $boot_time;
   cfg('stat_runtime', cfg('stat_runtime') + $runtime);
   cfg('stat_screens', cfg('stat_screens') + $num_displays);

   if (! open(CFG, ">$fnConfig")) {
      print STDERR "\n***** $me: Can't open $fnConfig for writing: $!\n";
      print STDERR "***** $me: Unable to save $me configuration, sorry\n";
      return;
   }
   foreach my $ky (sort keys %CFG) {
      if (! defined( $NSC_KEYWORDS{$ky} )) {
         1; #print CFG "# no default for '$ky'?\n";   # term.xx and such
      } elsif ($CFG{$ky} eq $NSC_KEYWORDS{$ky}) {
         print CFG '# ';        # comment out, if it has default value
      }
      print CFG "$ky=$CFG{$ky}\n" || fatal("Can't write to $fnConfig: $!");
   }
   close(CFG);

   $cfg_changes = 0;
   $cfg_loaded = time();

} #SaveConfig


# --- Configuration -------------------------------------------------------

sub LoadConfig {

   my $errcnt = 0;

   %CFG = ();

   #TODO: Version-check on loaded configfile?
   if (open(CFG, "$fnConfig")) {
      while (<CFG>) {
         s/#.*$//s;              #comment?
         $_ = trim($_);
         next if ($_ eq '');

         my ($kw,$val) = split(/=/);

         #always store to avoid loss on rewrite (typos, terms, etc.)
         $CFG{$kw} = $val;

         if ($kw =~ /^term\.(.*)$/) {     #special handling of term.*
            # term.linux.vt100=blargh
            cfg_terminal($1,$val);
         } elsif (defined($NSC_KEYWORDS{$kw})) {
            1;
         } else {
            print STDERR "$me: Unknown keyword $kw\n";
            # $errcnt++;
            }
         } #eof CFG
      close(CFG);
      }

   #apply default values
   foreach my $kw (keys %NSC_KEYWORDS) {
      $CFG{$kw} = $NSC_KEYWORDS{$kw}
         if (!defined($CFG{$kw}));
      }

   #override, cause this version to written when exiting
   $CFG{'version'} = $VERSION;

   $cfg_loaded = time();

   if ($errcnt) {
      print STDERR "\n*** $errcnt errors found in configuration file $fnConfig\n";
      #hmm.. no good prompting on stderr, when it goes to a file. so croak instead.
      #print STDERR "Press Enter to continue ..";
      #my $s = <STDIN>;
      exit(1);
   }

} #LoadConfig


# --- End configuration ---------------------------------------------------


} #end configuration brace


# =========================================================================
#                   S T A T I C   S C R E E N     
# =========================================================================

# --- Help ----------------------------------------------------------------
#TODO: highlight links

sub help {

   my $HELP = <<EOF;
Keys available
--------------
  <Space>  Next page
     b     Previous page
     ^     Move to top of service list
     a     Toggle between showing all services and troubled ones
     d     Toggle service details on/off
     h     Toggle sorting by host/status
     q     Quit
     r     Reverse sort order
     g     Toggle bell on/off
  <Ctrl-L> Redraw screen
     C     Toggle colour on/off
     U     Toggle CPU-meter on/off
     V     View status.log
     E     Edit services.cfg and reload
     ?     This help

nsc is copyright Stig H. Jacobsen & Gothix 2000-2005 <nsc\@gothix.biz> and
is freely available under the Perl Artistic License.

Homepage: http://nsc-gothix.sourceforge.net/

Download: http://freshmeat.net/projects/nsc/ (stable versions)
EOF

   my @HLP = split(/\n/, $HELP);
   drawScreen();

   my $i;
   for ($i=0; defined($HLP[$i]) && ($i<$a_services->lines()); $i++) {
      mLine($i, AT('normal'), $HLP[$i]);
      }

   botline("Press any key to exit help...");
   rdKey(300);
   botline();

} #help


# -------------------------------------------------------------------------

sub draw_version {

   my $s = '';
   $s .= $statuslog->{application} . ' '
      if ($statuslog and $statuslog->{application});
   $s .= 'v' . $statuslog->{version} . ' '
      if ($statuslog and $statuslog->{version});
   $s .= '/ ' if ($s ne '');
   $s .= "$myname $myversion";
   $a_version->cprint(0, 0, $s, AT('dim'));

} #draw_version


# -------------------------------------------------------------------------

sub drawScreen {

   #force update with background colour
   #by various means

   #nb.. v0.80a1: none of these appears to be needed..
   if (0) {
      gothix::area::clr_all();
   } elsif (1) {
      clear_screen(AT('normal'));
   } elsif (0) {
      for (my $i=0; $i<term_lines(); $i++) {
         aat(0, $i, AT('normal'), ' ' x term_cols());
         }
   } else {
      1;
   }

   draw_version();

#unneeded & causes "blinking"
#   term_refresh();

} #drawScreen


# --- screen_reset() ------------------------------------------------------
# Reset various nsc vars based on Curses info

sub screen_reset {

   fatal("Too few lines in window, $MIN_LINES required, we have " . term_lines())
      unless (term_lines() >= $MIN_LINES);
   fatal("Too few columns in window, $MIN_COLS required, we have " . term_cols())
      unless (term_cols() >= $MIN_COLS);

   $num_msglines = term_lines() - 5;
#   $first_msgline = 2;
#   $BOTLINE = term_lines()-1;

   if ($opt_colour) {
      colour_init();

      foreach my $attr (@gAttribs) {
         $AT{$attr} = decode_colour($attr);
      }
   } else {
      foreach my $attr (@gAttribs) {
         $AT{$attr} = decode_mono($attr);
      }
   }

   # --- area setup - must wait for screen_init() to set term_cols()

   { #Fudgery, in case term_cols() is odd and such
   my $lwidth = int(term_cols()/2);
   my $rwidth = term_cols() - $lwidth;
   my $rpos = $lwidth;

   $a_version = new gothix::area(name => 'version', x =>  0, y =>      0, width => $rwidth, height =>  1, attr => AT('normal'));
   $a_clock   = new gothix::area(name => 'clock', x =>    $rpos, y =>  0, width => 999,     height =>  1, attr => AT('normal'));
   $a_debug1  = new gothix::area(name => 'debug1', x =>   0, y =>      1, width => $rwidth, height =>  1, attr => AT('normal'));
   $a_update  = new gothix::area(name => 'update', x =>   $rpos, y =>  1, width => 999,     height =>  1, attr => AT('normal'));
   $a_headers = new gothix::area(name => 'headers', x =>  0, y =>      2, width => 999,     height =>  1, attr => AT('normal'));
   $a_services= new gothix::area(name => 'services', x => 0, y =>      3, width => 999,     height => -3, attr => AT('normal'));
   $a_blank   = new gothix::area(name => 'blank', x =>    0, y =>     -3, width => 999,     height =>  1, attr => AT('normal'));
   $a_options = new gothix::area(name => 'options', x =>  0, y =>     -2, width => $rwidth, height =>  1, attr => AT('normal'));
   $a_nodecnt = new gothix::area(name => 'nodecnt', x =>  $rpos, y => -2, width => 999,     height =>  1, attr => AT('normal'));
   $a_msgs    = new gothix::area(name => 'msgs', x =>     0, y =>     -1, width => $rwidth, height =>  1, attr => AT('normal'));
   $a_svccnt  = new gothix::area(name => 'svccnt', x =>   $rpos, y => -1, width => 999,     height =>  1, attr => AT('normal'));
   }

   $a_clock->{right_justify} = 
      $a_nodecnt->{right_justify} = 
         $a_svccnt->{right_justify} = 1;

} #screen_reset


# --- screen_init() -------------------------------------------------------

sub screen_init {

   curses_init();
   if (! cfg('devmode')) {
      term_turbocharge();
      dm('xcurses assertions disabled');
   }
   $opt_colour = term_has_colours();
   screen_reset();

} #screen_init


sub screen_down {

   curses_shutdown();

} #screen_down


# =========================================================================
#                   S E R V I C E   L I S T       
# =========================================================================


# --- filter_statuslog() --------------------------------------------------
# Filtering sub for nsc::statuslog to weed out items that we don't want.

sub filter_statuslog {

   my $row = shift;

   if ( @filter_hosts > 0  and  defined($row->{'node'}) ) {
      foreach my $f (@filter_hosts) {
         return 1 if ($row->{'node'} =~ /^ $f $/xi );
      }
      #nothing in filter matched, so kill this row
      return 0;
   }

   return 1;

} #filter_statuslog



# --- state_colour() ------------------------------------------------------
# Maps host/service status to a colour/attribute in the passed arrayref
sub state_colour {

   my $colours = shift;
   my $i;
   foreach $i (0..$#{$colours}) {
      return $colours->[$i]
         if ($_[$i] > 0);
   }
   return 'notok';    #everything was 0!

} #state_colour



# --- get_sval() ----------------------------------------------------------

sub get_sval {

   my ($r) = @_;

   if ($r->{'ack'}) {
      return $sVals{'ACK/'};
   } else {
      return $sVals{$r->{'status'}};
   }

} #get_sval


# --- Service list ordering -----------------------------------------------

# By status, node, service
sub by_prior {

   my $i = get_sval($a) - get_sval($b);
   if ($i == 0) {
      if (($i = ($a->{'node'} cmp $b->{'node'})) == 0) {
         $i = (lc($a->{'service'}) cmp lc($b->{'service'}));
         }
      }
   $i = -$i if (cfg('reverse'));
   return $i;

} #by_prior


# By node, status, service
sub by_node {

   my $i = ($a->{'node'} cmp $b->{'node'});
   if ($i == 0) {
      if (($i = get_sval($a) - get_sval($b)) == 0) {
         $i = (lc($a->{'service'}) cmp lc($b->{'service'}));
      }
   }
   $i = -$i if (cfg('reverse'));
   return $i;

} #by_node


sub sort_state {
   my @state = @_;
   my $sortby = cfg('sortbynode') ? \&by_node : \&by_prior;
   @state = sort $sortby @state;
   return @state;
}


# --- Display single service line ------------------------------------

my ($lastHost,$lastState) = ('','');


sub dispService {

   my ($line,$row) = @_;

#   dm('type='.$row->{'type'});
#   foreach my $ky (sort keys %{$row}) {
#      dm("$ky => '$row->{$ky}'");
#   }
#   dm("-----");

   fatal("dispService(): Row is undef") unless defined($row);

   return 0 if (!cfg('showall') && ($row->{'status'} eq 'OK'));

   my $attrib;
   if ($row->{'status'} eq 'OK') {
      $attrib = AT('isok');
   } else {
      if ($row->{'ack'}) {
         $attrib = AT('ack');
      } elsif ($row->{'status'} eq 'CRITICAL') {
         $attrib = AT('critical');
      } elsif ($row->{'status'} eq 'WARNING') {
         $attrib = AT('warning');
      } else {
         $attrib = AT('notok');
         }
      #TODO: use hState/sState instead of this...
      if ( ($row->{'status'} !~ /^(RECOVERY|OK|PENDING)$/) &&
           !$row->{'ack'} &&
           (cfg('bell') && ((time - $lastBeep) > cfg('bell_interval'))) ) {
         bell();
         $lastBeep = time;
         }
      }

   if (cfg('details')) {
      my $dispnode = $row->{'node'};
      $dispnode = ' .' if (($row->{'node'} eq $lastHost) && ($row->{'status'} eq $lastState));

      my $dispstate = $row->{'status'};
      if ($row->{'ack'}) {
         $dispstate = 'ACK/' . $row->{'status'};
      }
      $dispstate = '.' if (($row->{'node'} eq $lastHost) && ($row->{'status'} eq $lastState));
      $dispstate = center($dispstate, 8);

      my $chkchg = '';
      if ($row->{updated} and $row->{updated} > 0) {
         $chkchg = tdif(time(), $row->{updated}, 'y');
      }
      if ($row->{changed}) {
         $chkchg .= '/' . tdif(time(), $row->{changed}, 'y');
      }

      {  #align '/' in data ('2m/7h') to '/' in heading
         my $ak = '3_updated';
         my $apos = index($chkchg, $DataCols{$ak}->{align});
         if ($apos < $DataCols{$ak}->{align_pos}) {
            $chkchg = (' ' x ($DataCols{$ak}->{align_pos} - $apos)) . $chkchg;
         }
         $chkchg = rpad($chkchg, $DataCols{$ak}->{cw});
      }

      #show coloured status
      my $i = $a_services->print(0, $line,
                  sprintf("%-10.10s %-16.16s ", $dispnode,$row->{'service'} ),
                  AT('normal'));
      $i += $a_services->print($i, $line, sprintf("%-8.8s", $dispstate), $attrib);
      $i += $a_services->print($i, $line,
                #sic.. center() woes
                sprintf(" %s %s ", $chkchg,center($row->{'attempts'},3)), AT('normal'));
                # - 1;     #-1='!'

      my $left = term_cols() - $i;

      my $serv_info = substr($row->{'info'}, 0, $left);

      #spacepad to wipe out old stuff displayed
      $serv_info .= ' ' x ($a_services->maxx() + 1 - length($serv_info));

      $i += $a_services->print($i, $line, $serv_info, AT('normal'));
   } else {
      my $l = sprintf("%-18.18s %s", "$row->{'node'}:$row->{'service'}",$row->{'info'});
      $a_services->print(0, $line, $l, $attrib);
      #space-pad to area with to make screen green (whatever)
      $l = ' ' . $row->{'info'};
      $l .= ' ' x ($a_services->maxx() + 1 - length($l));
      $a_services->print(14, $line, $l, AT('normal'));
      }

   ($lastHost,$lastState) = ($row->{'node'},$row->{'status'});

   return 1;

} #dispService


# --- Counts, sorts & weeds servicelist ------------------------------

sub getServiceList {

   my @res = ();

   foreach my $r (@{$statuslog->{rows}}) {
      my $t = $r->{'type'};   # || fatal("item has no type...\n" . Dumper($r));
      if (defined $t && $t eq 'SERVICE') {
         push(@res, $r);
      }
   } #foreach @ol

   return undef unless (scalar(@res) > 0);

   return sort_state(@res);

} #getServiceList


# =========================================================================
#                       S T A T U S   L O G           
# =========================================================================

sub status_err_attr {
   my ($mtime,$default) = @_;

   return 'notok' unless defined($mtime);

   my $diff = time() - $mtime;

   if ($diff >= cfg('noupd_err')) {
      return 'critical';
   } elsif ($diff >= cfg('noupd_warn')) {
      return 'warning';
   } else {
      return ($default ? $default : 'normal');
   }
} #status_err_attr


# mLine() that pads with spaces to colour background...
sub mfLine {
   my @args = @_;
   my $i = length($args[2]);
   $args[2] .= ' ' x ($a_services->cols()-$i);
   mLine(@args);
}

sub status_error {

   my($msg, $arg_filename) = @_;

   my $fn = '(unknown)';
   if ($arg_filename) {
      $fn = $arg_filename;
   } elsif ($statuslog and $statuslog->{filename}) {
      $fn = $statuslog->{filename};
   }

   my $error = undef;

   $error = $statuslog->geterr() if ($statuslog);
   $error = '(n/a)' unless $error;

   my $mtime = undef;
   $mtime = localtime($statuslog->time_loaded())
      if ($statuslog and $statuslog->{time_loaded});

   # --- The vi style rULeZ!
   for (my $i=0; $i<$num_msglines; $i++) {
      mLine($i, undef, '~');
      }

   my $attr = status_err_attr($mtime);

   my $offs = (($num_msglines - 4) / 2) - 2;
   $offs = 1 if ($offs <= 0);

   my $stars = '*' x 5 . ' ';

   mfLine($offs,   AT($attr), $stars . $msg);
   mfLine($offs+1, AT($attr), $stars . 'Location: ' . $fn);
   mfLine($offs+2, AT($attr), $stars . 'Error: ' . $error);
   mfLine($offs+3, AT($attr), $stars . 'Last updated $mtime') if $mtime;

   botline("*** $msg");

} #status_error


# --------------------------------------------------------------------

my $upd_pos = 0;

{ #localize data

my $last_service_tm = 0;

sub showServiceList {

   my (@svcList) = @_;

   if ($display_dirty) {
      dm('display dirty');
   } else {
      if ($statuslog->time_loaded() <= $last_service_tm) {
         dm("noload, file not changed");
         return 0;
      }
      dm("load!");
   }

   $last_service_tm = $statuslog->time_loaded();

   # --- Show
   if (cfg('details')) {
      my $i = 0;
      my $out = '';
      foreach my $h (sort keys %DataCols) {
         my $hr = $DataCols{$h};
         last if ($i >= term_cols());
         my $fmt = '%' . (defined($hr->{right}) ? '' : '-');
         if ($hr->{cw} > 0) {
            $fmt .= sprintf("%d.%d", $hr->{cw}, $hr->{cw});
            }
         my $s = sprintf($fmt . 's ', $hr->{'label'});
         $i += length($s);
         $out .= $s;
      }
      #pad with spaces to area width to colour it evenly
      $out .= ' ' x ($a_headers->maxx()+1 - length($out))
         if (length($out) < $a_headers->maxx());
      $a_headers->cprint(0, 0, $out, AT('heading'));
   } else {
      $a_headers->cprint(0, 0,
            sprintf("%-14.14s %s", 
                    'Host:Service',
                    'Service information'),
            AT('heading'));
      }

   ($lastHost,$lastState) = ('','');

   my $i = $listoffs;
   my $num_shown;

   if (!defined($svcList[$i])) {             #got shorter?(!)
      $i = $listoffs = 0;                    #reset as needed
      }

   for ($num_shown=0; ($num_shown<($num_msglines-1)) && defined($svcList[$i]); $i++) {
      if ( 
            ($num_shown > 0)  &&
            !cfg('sortbynode') &&
            !cfg('reverse')   &&
            cfg('details')    &&
            ($svcList[$i-1]->{'status'} ne 'OK') &&
            ($svcList[$i]->{'status'} eq 'OK') 
         ) {
         if (cfg('popart')) {
            $a_services->raw_hline(0, $num_shown, $a_services->maxx()+1);
            $num_shown++;
            }
         }
      if (dispService($num_shown, $svcList[$i])) {
         $num_shown++ 
      }
   }

   my $msg = '';
   if ($num_shown == ($num_msglines-1)) {
      if (defined($svcList[$i])) { 
         $msg = 'more';
      } else {
         $msg = 'last page';
      }
   }

   if ($listoffs > 0) {
      $msg .= ',' if ($msg ne '');
      $msg .= "+$listoffs";
      }

   $msg = "($msg)" unless ($msg eq '');
   $a_blank->cprint(0, 0, $msg, AT('botline'));

   # --- The vi style rULeZ!
   for ($i=$num_shown; $i<$num_msglines; $i++) {
      mLine($i, undef, '~');
      }

   # --- Show total host scores ---

   {
      my $problematic =  $statuslog->{hState}->{'DOWN'} + $statuslog->{hState}->{'UNREACHABLE'};

      my $host_score = sprintf("Hosts down/unreach/ack/up: %d/%d/%d/%d",
                                    $statuslog->{hState}->{'DOWN'},
                                    $statuslog->{hState}->{'UNREACHABLE'},
                                    $statuslog->{hState}->{'ACK'},
                                    $statuslog->{hState}->{'UP'},
                              );

      my $host_attr = state_colour( 
                                    ['critical','warning','ack','total'],
                                    $statuslog->{hState}->{'DOWN'},
                                    $statuslog->{hState}->{'UNREACHABLE'},
                                    $statuslog->{hState}->{'ACK'},
                                    $statuslog->{hState}->{'UP'},
                                 );
      if ($host_attr !~ /^( total | ack )$/x and
          $statuslog->{num_host_unacknowledged} == 0 and
          $problematic > 0
         ) {
         $host_attr = 'ack';
      }
      $a_nodecnt->cprint(0, 0, $host_score, AT($host_attr));
   }

   # --- Show total service scores ---

   {
      my $other_cnt = $statuslog->{sState}->{'UNKNOWN'} 
                     + $statuslog->{sState}->{'PENDING'} 
                        + $statuslog->{sState}->{'HOST DOWN'};

      my $problematic = $other_cnt + $statuslog->{sState}->{'CRITICAL'} + $statuslog->{sState}->{'WARNING'};

      my $svc_score = sprintf(" crit/warn/other/ack/ok: %d/%d/%d/%d/%d",
                              $statuslog->{sState}->{'CRITICAL'},
                              $statuslog->{sState}->{'WARNING'},
                              $other_cnt,
                              $statuslog->{sState}->{'ACK'},
                              $statuslog->{sState}->{'OK'}, 
                           );

      # accomodate 80x24
      $svc_score = (($a_svccnt->cols() >= length('Services ' . $svc_score)) ? 'Services' : 'Svcs') . $svc_score;

      my $svc_attr = state_colour( 
                                    ['critical','warning','notok','ack','total'],
                                    $statuslog->{sState}->{'CRITICAL'},
                                    $statuslog->{sState}->{'WARNING'},
                                    $other_cnt,
                                    $statuslog->{sState}->{'ACK'},
                                    $statuslog->{sState}->{'OK'}, 
                                 );

      $svc_attr = 'ack'
         if ($svc_attr !~ /( total | ack )/x and
             $statuslog->{num_svc_unacknowledged} == 0 and
             $problematic > 0
            );

      $a_svccnt->cprint(0, 0, $svc_score, AT($svc_attr));
   }

   $display_dirty = 0;
   $num_displays++;

} #showServiceList

}


# -------------------------------------------------------------------------

sub disp_data {

   my $fn_statuslog = shift;
   my $old_botline = '';

   $old_botline = $last_botline if $last_botline;

   #TODO: close, reopen if $fn_statuslog <> $statuslog->{filename}

   if (! $statuslog ) {
      #only shown on initial load, when $statuslog isn't setup
      botline("Please wait - loading Nagios data ...");
      $statuslog = new nsc::statuslog($fn_statuslog,
                                      cfg('remote.copy_cmd'),
                                      cfg('remote.copy_interval'),
                                      $filter_sub,
                                     );
      }

   if ( !$statuslog or $statuslog->geterr() ) {
      status_error("Error accessing status.log", $fn_statuslog);
      return;
   }

   if ($statuslog->changed()) {
      if ($statuslog->is_remote()) {
         botline('Loading remote status.log ...');
      } else {
         botline('Loading ...') if cfg('debug');
      }
      if (! $statuslog->load()) {
         status_error("Error reading status.log");
         return;
      }
      if (scalar($statuslog->{rows}) <= 0) {
         status_error("No data read from status.log");
         return;
      }

      draw_version();

   }


   # --- Ok, clear to go ---
   #show also, even if not newly loaded, since the user may have toggled uptions or navigated
   if ($statuslog) {
      #filter & sorts service list
      my @svcList = getServiceList();
      dm("Got $#svcList + 1 services..");
      if (defined( $svcList[0] )) {
         #returns true if anything (new) was shown
         showServiceList(@svcList);
      }
   }

   botline($old_botline);     #restore or clear own Loading ... message

} #disp_data


# =========================================================================
#                     M A I N   F L O W   S U P P O R T
# =========================================================================

{ #localize tables

sub disp_clock {

   my @l = localtime(time);
   my $s = sprintf("%s %s %d, %04d, %02d:%02d:%02d",
                   $daytab_long[$l[6]], $montab_long[$l[4]], $l[3], 
                   $l[5] + 1900, $l[2], $l[1], $l[0]);

   # CPU-meter enabled?
   if (defined($cpusage) and cfg('cpumeter')) {
      $s .= ' / cpu ' . $cpusage->read();
   }


   $a_clock->cprint(0, 0, $s, AT('dim'));

   my $OK_attr = 'dim';

   # --- Show Nagios uptime/state
   $s = '';
   my $run_attr = 'noisy';
   if ($statuslog and $statuslog->{appl_status}) {
      $s .= 'run ';
      if ($statuslog->{uptime}) {
         $s .= tdif(time, $statuslog->{uptime},'y') . ' ';
         $run_attr = $OK_attr;
      }
   } else {
      $s .= 'STOPPED';
   }

   # --- 

   my $status_mtime = $statuslog->time_loaded();
   my $upd_attr = status_err_attr($status_mtime, $OK_attr);

   $s =~ s/\s+$//g;
   {
      my $upd;
      if ($status_mtime) {
         $upd = tdif(time(), $status_mtime, 'y');
      } else {
         $upd = 'never';
      }
      if ( ($run_attr eq $OK_attr) and
           ($upd_attr ne $OK_attr) ) {
         $run_attr = $upd_attr;
      }
      my $s2 = "$s $LogName\@";
      if ( $statuslog and $statuslog->is_remote() ) {
         $s2 .= ($statuslog->is_remote())[0];  #hostname only
      } else {
         $s2 .= $shorthost;
      }
      $a_update->cprint(-1, 0, $s2, AT($run_attr));
      $upd_pos = $a_update->maxx() - length($s2);
   }

   if ($upd_pos > 0) {
      my $s2 = '';
      if ( ($upd_attr eq $OK_attr) and $status_mtime) {
         $s2 .= '  upd ';
      } else {
         $s2 .= ' not updated ';
      }
      if (! defined($status_mtime) ) {
         $s2 .= 'never ';
      } else {
         $s2 .= tdif(time(), $status_mtime) . ' ';
      }
      $a_update->print($upd_pos-length($s2), 0, $s2, AT($upd_attr));
   }


} #disp_clock


} #localization


# =========================================================================
#                   M I S C E L L A N O U S       
# =========================================================================


my $dirty_options = 1;


# -------------------------------------------------------------------------

sub xcmd {
   screen_down();
   system $_[0];
   screen_init();
   redraw_screen();
} #xcmd


sub redraw_screen {

   screen_reset();
   drawScreen();
   $dirty_options = 1;
   disp_options();
   botline( $last_botline, 1 );     #does refresh()

} #redraw_screen


my %uiOptList = (
   #UI-text       cfg() value
   'all'       => 'showall',
   'details'   => 'details',
   'host'      => 'sortbynode',
   'reverse'   => 'reverse',
   'gong'      => 'bell',
);


sub disp_options {

   if (! $dirty_options ) {
      return;
   }

   my @olist = ();
   foreach my $uval (sort keys %uiOptList) {
      push( @olist, cfg( $uiOptList{ $uval } ) ? ucfirst( $uval ) : $uval);
   }
   $a_options->cprint(0, 0, join('/', @olist), AT('botline'));

   $dirty_options = 0;

} #disp_options


# -------------------------------------------------------------------------

sub process_key {

   my ($key) = @_;

   return undef unless ($key);

   #Looking forward to being able to use Switch; ...

   if ($key eq 'a') {
      cfg('showall', !cfg('showall'));
      $dirty_options = 1;
   } elsif ($key eq 'd') {
      cfg('details', !cfg('details'));
      $a_services->clr();
      $dirty_options = 1;
   } elsif ($key eq 'g') {
      cfg('bell', !cfg('bell'));
      $dirty_options = 1;
   } elsif ($key eq 'r') {
      cfg('reverse', !cfg('reverse'));
      $dirty_options = 1;
   } elsif ($key =~ /^( n | h )$/x) {
      #'n' is still here ('n'ode in previous versions)
      cfg('sortbynode', !cfg('sortbynode'));
      $dirty_options = 1;
   } elsif ($key eq 'C') {
      if (!$opt_colour && !term_has_colours()) {
         botline("Your terminal ($ENV{TERM}) can't show colours");
      } else {
         $opt_colour = !$opt_colour;
         redraw_screen();
         botline('Colours are now ' . ($opt_colour ? 'on' : 'off'));
         }
   } elsif ($key eq '^') {    #top of list
      $listoffs = 0;
#   } elsif ($key eq '$') {    #last page of list
# hmm... $svcList is not available here..
#      $listoffs = int (($#svcList+1) / $num_msglines);
   } elsif ($key =~ /^(page| )$/) {
      $listoffs += ($num_msglines-1);
#   } elsif ($key eq 'roll') {
#      $listoffs += ($num_msglines-1);
   } elsif ($key eq 'b') {
      if (($listoffs -= ($num_msglines-1)) < 0) {
         $listoffs = 0;
      }
   } elsif ($key eq 'D') {
      cfg('debug', !cfg('debug'));
      if (! cfg('debug')) {
         aat(0, 1, AT('dim'), ' ' x 30);
      }
   } elsif ($key eq 'U') {
      cfg('cpumeter', !cfg('cpumeter'));
   } elsif ($key eq 'X' && cfg('debug')) {
      show_debug();
      redraw_screen();
   } elsif ($key eq ctrl('L')) {
      if (1) {
         term_redraw();
      } else {
         #use Curses;
         bkgd(AT('normal') | ord(' '));
         erase();
         refresh();
         redraw_screen();
      }
   } elsif ($key eq 'E') {
      xcmd("$ENV{'VISUAL'} cfg('hostcfg'); echo Enter to reload ..; 
            read; cfg('reloadcmd')");             #debug
   } elsif ($key =~ /[h?]/) {
      help();
      redraw_screen();
   } else {
      my $k = 'usercmd_' . $key;
      if (cfg($k)) {
         xcmd(cfg($k));
      } else {
         return undef;
      }
   }
   return 1;

} #process_key


# -------------------------------------------------------------------------

{

my $last_roll = time(); #now, to avoid immediate roll on startup

sub roller {

   my ($last_input) = @_;

   #TODO: don't roll, if we have <=1 page of services...
   if ((cfg('roll') > 0)
       && cfg('showall')
       && ((time() - $last_input) >= cfg('roll_wait'))
       && ((time() - $last_roll) >= cfg('roll'))
      ) {
      process_key('page');          
      $display_dirty = 1;
      dm('rolled! ' . sprintf("%d secs since last roll", time()-$last_roll));
      $last_roll = time();
   }

} #roller

} #roller data


# =========================================================================
#                M A I N   I N I T I A L I Z A T I O N
# =========================================================================

$| = 1;           #unbuffered tty i/o
print "$myname $myversion starting ..";

dm('Greetings and salutations');


# --- Configuration ---

@uname = split(/\s+/, `uname -a`);     #TODO: get rid of this, slow
&LoadConfig();
print '.';

if (cfg('debug')) {
   our @inclist = ();
   foreach my $ik (keys %INC) {
      push(@inclist, sprintf('%s => %s', $ik, $INC{$ik}))
         if ($INC{$ik} =~ m! ( gothix | Term/Console ) !x);
   }
   foreach my $inc (sort @inclist) {
      dm('INC ' . $inc);
   }
}


# --- Commandline options ---

{
   my $opt_hostlist = undef;
   my $opt_term = undef;
   my $opt_disable_termreplace = undef;

   if (! GetOptions("hosts|h=s" => \$opt_hostlist,
                    "term|t=s" => \$opt_term,
                    "disable-terminal-replacement|T" => \$opt_disable_termreplace,
      )) {
      print STDERR <<EOT;
Usage:

   $me [--hosts=<hostlist>] [--term=<termtype>] [-T] [<statuslog>]
   
Stop.
EOT
      exit(1);
   }

   $ENV{'TERM'} = $opt_term
      if defined($opt_term);

   if (defined($opt_hostlist)) {
      @filter_hosts = split(/[,\s]+/, $opt_hostlist);
      foreach my $i (0..$#filter_hosts) {
         dm("orig($i) = '$filter_hosts[$i]'");
         #escape '.' in hostnames
         $filter_hosts[$i] =~ s/\./\\./g;
         #allow for '%' to mean '*', so no shell quoting is needed
         $filter_hosts[$i] =~ s/%/*/g;
         #and '*' (shell glob) becomes '.*' (perl re)
         $filter_hosts[$i] =~ s/\*/.*/g;
         dm("replaced($i) = '$filter_hosts[$i]'");
      }
   }

   if (@filter_hosts > 0) {
      dm("Filtering enabled");
      $filter_sub = \&filter_statuslog;
   }

   # --- Replace terminal-type, if so desired
   if (! $opt_disable_termreplace) {
      my $ky = lc($uname[0]) . '.' . $ENV{TERM};
      if (defined($TERMINALS{$ky})) {
         dm("rt:replacing terminal $ENV{TERM} on $uname[0] with $TERMINALS{$ky}");
         $ENV{'TERM'} = $TERMINALS{$ky};
      } else {
         dm("rt:no replacement terminal for '$ky'");
      }
   }

} #options brace


# ---

print '.';
#override .config w/cmdline arg if given
my $opt_logfile = defined($ARGV[0])?$ARGV[0]:cfg('nslog');

print '.';


# --- Display setup ---

print "!\n";

init_gAttribs();
screen_init();
redraw_screen();

#for profiling
my $num_iterations = 0;

#watch out for window resizes
my $last_nlines = term_lines();
my $last_ncols = term_cols();


sub resize_term {

   dm(sprintf("resize_term(): Resized to cols=%d, lines=%d", term_cols(), term_lines()));
   
   gothix::area::resizer();
#  clear_screen(AT('normal'));
   $display_dirty = 1;
   redraw_screen();
   term_refresh();

   $last_nlines = term_lines();
   $last_ncols = term_cols();

} #resize_term


sub check4resize {

   #dm("check4resize: Last = (cols=$last_ncols, lines=$last_nlines)");

   #User resized our window?
   if ( (term_lines() != $last_nlines) or (term_cols() != $last_ncols) ) {
      resize_term();
   }

} #check4resize


# =========================================================================
#                           M A I N   F L O W
# =========================================================================

dm('Going to operational state');

my $last_input = 0;
my $key;
my $done = 0;
while (!$done) {

#   check4resize();

   if (cfg('debug')) {
      my $s = sprintf('#%d  rt=%s      ', $num_displays, tdif(0, cfg('stat_runtime') + (time() - $boot_time)));
      aat(0, 1, AT('dim'), $s);
   }

   #show curr config before status draw (clreol)
   disp_options();

   #main, live, moving, squirming display!
   disp_data($opt_logfile);

   if (!defined($cpusage)) {
      #create this after first display, so startup costs won't be included
      $cpusage = new gothix::cpusage;
      }

   disp_clock();

   #don't read keys if profiling
   if (defined($ENV{'NSC_ITERATIONS'})) {
      if (++$num_iterations > $ENV{'NSC_ITERATIONS'}) {
         $done = 1;
         last;
      }
      $key = rdKey(0);
   } elsif ($key = rdKey(1)) {    #read once a second to keep clock ticking
      botline();                  #clear message on input
      $display_dirty = 1;
      }

   $last_input = time() if $key;

   # --- Keys handling

   if (!process_key($key)) {
      if ($key && $key =~ /q/i) {
         botline("Please come back soon");
         $done = 1;
      } elsif ($key && $key eq 'V') {
         xcmd("$ENV{'PAGER'} $opt_logfile");
      } elsif (defined( $key )) {
         botline("Invalid key <$key> - type '?' for help");
      } else {
         #Be helpfull, if user is quiet
         if ( !cfg('debug') && $last_input && ((time()-$last_input) > 30) ) {
            botline("Type '?' for help");
         }
         roller($last_input) if (cfg('roll') > 0);
      }
   }
  
} #while not done

dm('Shutting down operations');

END {
   dm('END {}');
   debug_save() if cfg('debug');
}

botline('See you!');
screen_down();

&SaveConfig();
print "\n";
exit 0;


#down here to avoid hiding perl syntax errors in above code(!)
use gothix::capstderr;


# =========================================================================
#                           A L L   D O N E  
# =========================================================================


__END__

# vim:aw:
# $Id: nsc.pl 2901 2005-05-23 13:59:12Z shj $
