#!/usr/bin/perl
# OCHP_daemon - Obsessive Compulsive Host Processor daemon for Nagios
#
# Copyright (C) 2007 Thomas Guyot-Sionnest <tguyot@gmail.com>
# Original code Copyright (C) 2007 Mark Steele
#       http://www.control-alt-del.org/code
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
use Event::Lib;
use Getopt::Std;
use POSIX;
use strict;
use warnings;
use vars qw($PROGNAME $VERSION $READ_SIZE $MAX_LINE_LENGTH %args);

$PROGNAME = 'OCHP_daemon';
$VERSION = '1.0rc2';
$READ_SIZE = 4096;
$MAX_LINE_LENGTH = 4096;
getopts("f:n:H:p:t:c:r:m:h", \%args);

# Print usage if missing options or -h
if (!$args{'f'} || !$args{'H'} || $args{'h'}) {
  usage();
}

# Process options
my $fifo = $args{'f'};
my $nsca = $args{'n'} || '/usr/local/nagios/bin/send_nsca';
my $host = $args{'H'};
my $reaper_delay = $args{'r'} || 1;
my $max_queue = $args{'m'} || 0;

my $nsca_extra_args = '';
$nsca_extra_args .= " -p $args{'p'}" if $args{'p'};
$nsca_extra_args .= " -to $args{'t'}" if $args{'t'};
$nsca_extra_args .= " -c $args{'c'}" if $args{'c'};

# Construct send_nsca command
$nsca .= " -H $host" . $nsca_extra_args;

# Sanity checks
if ($reaper_delay !~ /^\d+$/) {
  print "reaper_delay must be an integer greater or equal to 0!\n\n";
  usage();
}

if ($max_queue !~ /^\d+$/) {
  print "max_queue must be an integer greater or equal to 0!\n\n";
  usage();
}
if ($reaper_delay == 0) {
  $max_queue = 0;
}

# send_nsca test run
system ("$nsca </dev/null >/dev/null 2>/dev/null");
if ($? != 0) {
  print "Failed to run '$nsca', bailing out!\n";
  exit 1;
}

# Now the fun stuff :)

$0 = $PROGNAME;

# Set up a zombie reaper
my $signal = signal_new(SIGCHLD, \&reap_chld);
$signal->add;

my @queue;

## VERY IMPORTANT: You have to open the pipe in O_RDWR, POSIX has rules about 
##                 using polling calls on pipes, and can't do any on O_RDONLY
##
sysopen(FIFO, $fifo, O_RDWR | O_NONBLOCK) || die "couldn't open fifo: $!";
my $reader = event_new(\*FIFO, EV_READ, \&reader);
$reader->add;

my $timer;
if ($reaper_delay > 0) {
  $timer = timer_new(\&reaper);
  $timer->add($reaper_delay);
}

event_mainloop();

sub reap_chld {
  while (waitpid(-1, WNOHANG) > 0) {
  }
}

sub reaper {
  my $event = shift;

  if (@queue > 0) {
    my $fork;
    if (($fork = fork) == 0) {
      # We're a child, make sure we don't stay around too long...
      alarm(60);
      $0 = "$0 child";
      open(NSCA, "|$nsca >/dev/null 2>/dev/null") or die "Failed to spawn send_nsca: $!";
      for(@queue) {
        print NSCA $_;
      }
      close(NSCA);
      exit;
    } elsif (!defined($fork)) {
      # Fork failed, no free resources?
      die "Fork failed, no free resources?"
    } else {
      # We're the parent, enpty the queue
      undef @queue;
    }
  }
  $event->add($reaper_delay) if $event;
}


sub reader {
  my $event = shift;
  my $fh = $event->fh;
  my $self = shift;
  my $data;

  if (scalar($event->args()) > 3) { ## Recursively called ourselves with data passed to function
    $data = $_[3];
  }

  my $ret = sysread ($fh, my $buf, $READ_SIZE);

  if (defined ($ret) && $ret == 0) { ## Shouldn't happen
    #print scalar localtime, " ACK: Got EOF?\n";
    die;
  } elsif (!defined ($ret)) { ## Shouldn't happen
    #print scalar localtime, " ACK: Error condition? $!\n";
    die;
  } elsif (!$buf) { ## Shouldn't happen
    #print scalar localtime, " ACK: Not EOF, not error, but nothing in buffer\n";
    die;
  }

  # Write might not be atomic if Nagios was asked to process an external file
  # Lets be safe here...

  $data .= $buf;

  while (my $marker = index ($data, "\n") + 1) {
    push (@queue, substr ($data, 0, $marker));
    $data = substr ($data, $marker);
      
    if ($max_queue && $max_queue <= @queue) {
      $timer->remove; # Reaper will re-add itself
      reaper($timer);
    }
  }

  # Process queue now if there's no timer
  if (!$reaper_delay) {
    reaper(0);
  }

  if ($data && length ($data) < $MAX_LINE_LENGTH) {   ## Incomplete line
    #print "DATA LEFT AFTER PARSING: ------------\n$data\n-------------\n";
    $event->args($event->fh, EV_READ, $self, $data);
    $event->add;
    return;
  }

  $event->args($event->fh, EV_READ, $self);
  $event->add;
}

sub usage {
  print "$PROGNAME v.$VERSION - Obsessive Compulsive Host Processor daemon\n";
  print "Usage:\n";
  print "  $PROGNAME -f <fifo_file> -H <nsca_host> \n";
  print "  [ -n <nsca_bin> ] [ -p <nsca_port> ] [ -t <nsca_timeout> ] [ -c <nsca_config> ]\n";
  print "  [ -r <reaper_delay> ] [ -m <max_queue> ]\n\n";

  print "Options:\n";
  print "  -f <file>\tFifo file to read from, must be a pipe\n\n";

  print "  -n <nsca_bin>\tsend_nsca command path\n";
  print "\t\tDefaults to /usr/local/nagios/bin/send_nsca\n\n";

  print "  -H,-p,-t,-c\tSee corresponding send_nsca command\n\n";

  print "  -r <seconds>\tHow long to wait between each nsca flushes\n";
  print "\t\t0 = as data arrive. Default: 1 second\n";
  print "\t\tWARNING: Setting this to 0 can be very resource-consuming!\n\n";

  print "  -m <slots>\tMax queue size if reaper_delay is greater than 0\n";
  print "\t\tA flush will be forced if the queue reach this size\n\n";

  exit 1;
}

