#!/usr/bin/perl
# IBM_PROLOG_BEGIN_TAG 
# This is an automatically generated prolog. 
#  
# bos720 src/bos/usr/sbin/install/suma/lib/SUMA/Messenger.pm 1.10 
#  
# Licensed Materials - Property of IBM 
#  
# Restricted Materials of IBM 
#  
# COPYRIGHT International Business Machines Corp. 2004,2008 
# All Rights Reserved 
#  
# US Government Users Restricted Rights - Use, duplication or 
# disclosure restricted by GSA ADP Schedule Contract with IBM Corp. 
#  
# IBM_PROLOG_END_TAG 

package SUMA::Messenger;

# code starts after '=cut'

=head1 NAME

SUMA::Messenger - Class for displaying messages and reporting status
or errors via e-mail or logfile.

=head1 SYNOPSIS

$rc = SUMA::Messenger->start($policy_obj);

$rc = mesg(LVL_MACRO, MSG_MACRO, arg1 ... argN);

$rc = SUMA::Messenger->complete;

$rc = SUMA::Messenger->log_download(filename);

=head1 DESCRIPTION

Provides an OO interface for reporting errors, warnings, and
information related to the Service Update Management Assistant, as
well as making the information available in a log file, on the screen,
and/or via e-mail notification. All information is reported in
a style defined by maximum threshold values, as specified by either
the command line or global configuration specifications.

=cut

use strict;

use lib qw(/usr/suma/lib);

use POSIX qw(strftime);

sub ckPrivate {
  my @cklist = @_;
  my $callee = caller(0);
  push @cklist, $callee;
  my $caller = caller(1);
  for(@cklist) {
    return 1 if(($caller eq $_) || $caller->isa($_));
  }
  return 0;
} # ckPrivate

use constant MEGABYTE => 1024 * 1024;

use SUMA::GConfig;

use locale;

our(@ISA, @EXPORT_OK);

my($msgmap, $msgmapfh, $macro, $screenlevel, $loglevel, $notifylevel, $logfile, $logfh,
   $dlogfile, $dlogfh, $maxsize, $email, $ua, $STARTED, $notifier_pid, $notify_writer);


$STARTED = 0;	# This is set true when "start" is called

my $fh_by_pid = {};

my $called_from = $ENV{'_SUMA'} || '';

sub _getMsgFH() {
  return $fh_by_pid->{$$} if (exists($fh_by_pid->{$$}) && defined($fh_by_pid->{$$}));
  my $fh;
  open($fh, $msgmap) or die "Fatal error in messenger: couldn't open map file '$msgmap': $!";
  $fh_by_pid->{$$} = $fh;
  return $fh;
}

$msgmapfh = _getMsgFH();
local $/ = "\n,";
my($pos, $evalstr);
while(<$msgmapfh>) {
  if(($macro) = /^(MSG_\w+)\s/) {
    $pos = (tell($msgmapfh) - length) or die("Fatal error importing message $macro - tell() failed: $!");
    $pos =~ /(\d+)/;
    $pos = $1;
    # *$macro = eval(qq|sub() {"$pos"}|); # The old way - required 'no strict'
    ($evalstr) = "use constant $macro => $pos" =~ /(.*)/;
    eval($evalstr);
    push @EXPORT_OK, $macro;
  }
}

# Always export 'mesg'.
sub import {
  SUMA::Messenger->export_to_level(1, @_, 'mesg');
}

BEGIN {
  require Exporter;
  our @ISA = qw(Exporter);
  our @EXPORT_OK = qw( mesg start complete log_download );

  $msgmap = getGlobal('MSG_MAPFILE_PATH');
  die "Message map file '$msgmap' doesn't exist." unless -e $msgmap;

  # use subs ("_getMsgFH");
  $msgmapfh = _getMsgFH();

}

=head1 METHODS

=over 4

=item mesg LVL_MACRO, MSG_MACRO, [ ARGS ]

Accepts a verbosity level, a message macro (representing the message to
display), and any arguments (variables) related to the message to
display, and returns true or false. If the value of LVL_MACRO is above
the current verbosity settings, the message is ignored. Behavior of the
ARGS values is comparable to that of the dspmsg command.
                  
The MSG_MACRO values are defined by the message map file (/usr/suma/lib/msg.map).
Each entry in the file contains all information necessary for the messenger
function to produce output, and properly display any translated messages.
                  
Acceptable values for LVL_MACRO are as follows:

=over 4

=item LVL_ERROR

Highly important messages and critical failures. These messages are
always reported. A system using this level will have the smallest
logfiles/least verbose notices. May also use "undef" when calling 
the mesg function.

=item LVL_WARNING
  
Non-critical failures/error messages/warnings. These messages would
be less severe than those at level 0, but may still be an indicator
of a problem to investigate.

=item LVL_INFO

Informational messages. One could reasonably expect an amount of data
comparable to that of smitty. A system with a NOTIFY_VERBOSE (a.k.a.
e-mail notices) above LVL_INFO is not recommended, as it could generate
a lot of mail traffic!
    
=item LVL_VERBOSE
    
Informational messages. These include things such as "contacting server"
and "server contacted" (handshaking). A system using this level should  
expect to log a lot of data - and plan maximum log sizes accordingly.
        
=item LVL_DEBUG
          
Debug verbosity. This should be used if a message is useful only for debug
purposes. A system using this level should expect a lot of data to be
recorded in the logfile. Increasing the maximum logfile size is strongly
encouraged. Such messages are not necessarily translated, and may be
identified by the string "DEBUG".
          
=back
       
All of the level macros are exported, and may be used by all parts of the
Service Update Management Assistant.
   
=cut

## Method lookup ################################################

sub lookup($@) {
  my $pos = shift;
  seek($msgmapfh, $pos, 0) or die "Fatal error in messenger: couldn't seek in map file: $!";
  my $line;
  {
    local $/ = "\n,";
    $line = <$msgmapfh>;
    chomp $line;
  }
  my($cat, $set, $msg, $def) = $line =~ /^\w+\s+(\S+)\s+(\d+)\s+(\d+)\s+(.*)/s;
  # $ENV{LANG} =~ m/(\w+)/;
  # $cat = "/usr/lib/nls/msg/$1/$cat";
  require LibExt::CatGetS;
  LibExt::CatGetS->import('catgets');
  my $fmt = catgets($cat, $set, $msg, $def);
  chomp $fmt;
  $fmt =~ s/%(\d+\$)?(.)/%$2/g;
  $fmt .= "\n";
  my $str = sprintf($fmt, @_);
  chomp $str;
  return $str;
} # lookup

## Method mesg ##################################################

sub mesg($$@) {
  my($lvl, $pos) = (shift, shift);
  $msgmapfh = _getMsgFH();

  # Validate $lvl within meaningful ranges:
  $lvl = LVL_ERROR  unless defined($lvl);
  $lvl = -1         if     ($lvl < -1);
  $lvl = LVL_DEBUG  if     ($lvl >= LVL_DEBUG);

  local $, = "\n";
  local $\ = "\n";

  my $str;
  my $strprefix = $lvl == LVL_DEBUG ? 'DEBUG: ' : '';

  my $type;

  SWITCH: {
    $type = "E", last SWITCH if ($lvl == LVL_ERROR);
    $type = "W", last SWITCH if ($lvl == LVL_WARNING);
    $type = "I", last SWITCH if ($lvl == LVL_INFO);
    $type = "V", last SWITCH if ($lvl == LVL_VERBOSE);
    $type = "D", last SWITCH if ($lvl == LVL_DEBUG);
  }

  my($callP,$callF,$callL) = caller;

  unless($called_from eq 'cron') { # Only print to screen if not scheduled
    if($called_from eq 'SMIT') { # Ensure F4 lists (etc.) get the right output
      $screenlevel = LVL_WARNING;
    }
    else {
      $screenlevel = getGlobal('SCREEN_VERBOSE');
      unless(defined($screenlevel)) { $screenlevel = LVL_WARNING }
    }
    ## Print to the screen (STDOUT or STDERR) ################
    if (($screenlevel >= $lvl) && ($pos != MSG_LOG_STOP()) && ($pos != MSG_LOG_START())) {
      start();
      my $fh = $lvl <= LVL_WARNING ? \*STDERR : \*STDOUT;
      $str = lookup($pos, @_) unless $str;
      if (($lvl == LVL_DEBUG) || ($screenlevel == LVL_DEBUG)) {
        $str .= " ($callP, $callF \[$callL\])";
      }
      print($fh "$strprefix$str");
    }
  }

  $loglevel    = getGlobal('LOGFILE_VERBOSE');
  unless(defined($loglevel)) { $loglevel = LVL_INFO }
  ## Write to the log file #################################
  if($loglevel >= $lvl) {
    start();
    unless($logfh) {
      open $logfh, ">>$logfile" or die "Unable to open log:  $logfile";
    }
    $str = lookup($pos, @_) unless $str;
    if (($lvl == LVL_DEBUG) || ($loglevel == LVL_DEBUG)) {
      $str .= " ($callP, $callF \[$callL\])";
    }
    my $time = strftime("%Y" . "%m" . "%d %H:%M:%S",localtime);
    print($logfh "$time $type $strprefix$str");
  }

  if($notify_writer) { # Only email if scheduled AND the pipe is open
    $notifylevel = getGlobal('NOTIFY_VERBOSE');
    unless(defined($notifylevel)) { $notifylevel = LVL_INFO }
    ## Add to the notify/e-mail body  ########################
    if (($notifylevel >= $lvl) && (defined($email))) {
      start();
      $str = lookup($pos, @_) unless $str;
      if (($lvl == LVL_DEBUG) || ($notifylevel == LVL_DEBUG)) {
        $str .=  " ($callP, $callF \[$callL\])";
      }
      print $notify_writer "$strprefix$str";
    }
  }

  return 1;
} # mesg

## Method start #################################################

=item start POLICY_OBJ

Signals the messenger to begin logging and, if appropriate, saving
messages for emailing. Causes a 'start' stamp to be written to the
log file. A policy object is required, so that certain data may be
used to display messages according to system settings.
  
=cut
  
sub start {

  # This chunk is before the if($STARTED) because we might have
  # implicit start()s before the first explicit start($policy).  This
  # way we always catch it the first time we get a $policy.
  if(($called_from eq 'cron') && !$notify_writer) {
    # In case we were called implicitly, without a Policy, always
    # check for a policy if $notify_writer isn't set up
    my $pol = shift;
    # Fork Notify Writer:
    FNW: if(defined($pol) && ref($pol) && $pol->isa('SUMA::Policy')) {
      $email = $pol->getField("NotifyEmail");
      if ($email eq '') {
        undef $email;
        last FNW;
      }
      $notifier_pid = open($notify_writer, '|-'); # Safe pipe open
      die "Unable to fork notifier subprocess: $!" unless defined $notifier_pid;
      # Parent continues happily on
      unless($notifier_pid) { # Notifier child process
        $STARTED = 0; # So we don't complete() on END
        require LWP::UserAgent;
        require HTTP::Request::Common;
        HTTP::Request::Common->import('POST');
	my $line;
	my $content=0;        
        my $req = POST("mailto:$email", Subject => lookup(MSG_TASK_RESULTS()), 
                    content_type => "text/plain");
        while($line=<STDIN>) { # Read from the safe pipe
          if ($line ne ''){
		$content++;      
          }
          $req->add_content($line);
        }
        # We get here when the pipe closes; send the email if the body of the mail has data.
        if ($content != 0) {
          $ua = LWP::UserAgent->new(timeout => 30);
          my $resp = $ua->request($req);
          exit $resp->is_success();
	}     
      } # Notifier child process
    }
  }

  # If logging is already started, don't do it again
  if ($STARTED) {
    return $STARTED;
  }

  $STARTED = 1;	# Signal that logging has started

  ######################################################
  # Load up the Global Config's log-related settings   #
  # and store default values, if getGlobal fails on us #
  ######################################################
  unless($logfile) {
    $logfile     = getGlobal('LOGFILE')         || "/var/adm/ras/suma.log";
  }
  unless($dlogfile) {
    $dlogfile    = getGlobal('DL_HIST_LOGFILE') || "/var/adm/ras/suma_dl.log";
  }
  unless(defined($maxsize)) {
    $maxsize     = getGlobal('MAXLOGSIZE_MB')   || 1;
    # Convert to bytes
    $maxsize    *= MEGABYTE;
  }
  ## Check both logs for filesize. If they've gone over our limit
  ## we need to roll them off to a backup copy and create new ones.
  if (-e $logfile && ((-s $logfile) > $maxsize)) {
    rename $logfile,$logfile.".bak"
  }
  if (-e $dlogfile && ((-s $dlogfile) > $maxsize)) {
    rename $dlogfile,$dlogfile.".bak"
  }


  my $time = strftime("%a %b %e %H:%M:%S %Y",localtime);
  mesg(LVL_INFO, MSG_LOG_START(), $time);

  return $STARTED;
} # start
  
## Method complete ##############################################
  
=item complete
    
Signals the end of current logging operations. When executed, this
records an 'end' stamp to be written in the log file, collects all
messages stored for e-mail notification, and sends the e-mail. It
returns either true or false.
   
=cut

sub complete {
  my $time = strftime("%a %b %e %H:%M:%S %Y",localtime);
  mesg(LVL_INFO, MSG_LOG_STOP(), $time);

  mesg(LVL_DEBUG, MSG_GENERIC(), "Closing file handles");
  _destruct();
} # complete

sub _destruct {
  close($logfh) if $logfh;
  close($dlogfh) if $dlogfh;

  if ($notifier_pid && $notify_writer) {
    # Let the notifier subproc know we're done by closing the writer
    close($notify_writer);
    # Now wait for it to send the email
    waitpid($notifier_pid, 0);
  }


  $STARTED = 0;
  return 1;
}
## Method log_download ##########################################

=item log_download FILENAME

Records successful file transfers to the download history logfile,
and may only be called by SUMA::Download. All other requests for
this function result in an error message and failed return code. It
returns either true or false.
  
=cut
  
sub log_download {
  my($fname) = (shift);

  # Only SUMA::Download may call this function!
  return 0  unless ckPrivate('SUMA::Download');

  unless($dlogfh) {
    open $dlogfh, ">>$dlogfile" or die "Unable to open download log:  $dlogfile";
    chmod 0664, $logfile, $dlogfile;
  }

  chomp($fname);
  local $\ = "\n";

  my $time = strftime("%Y" . "%m" . "%d %H:%M:%S",localtime);
  print($dlogfh "$time $fname")  ||  return 0;

  return 1;
} # log_download

END {
  if ($STARTED) {
    _destruct();
  }
}

1;