#! /usr/bin/perl
# $Id: logdrake 238729 2008-03-06 15:15:47Z tv $

# Copyright (C) 2001-2008 Mandriva
# Yves Duret <yduret at mandriva.com>
# some code is Copyright: (C) 1999, Michael T. Babcock <mikebabcock@pobox.com>
#
# 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# TODO: consider switching from TreeView to gtkhtml

use strict;
use lib qw(/usr/lib/libDrakX);
use standalone;     #- warning, standalone must be loaded very first, for 'explanations'
use c;
use common;
use interactive;
use mygtk2 qw(gtknew); #- do not import gtkadd which conflicts with ugtk2 version
use ugtk2 qw(:create :dialogs :helpers :wrappers);

$ugtk2::wm_icon = "/usr/share/mcc/themes/default/logdrake-mdk.png";
my $in = 'interactive'->vnew('su');

my ($isExplain, $Explain, $isFile, $File, $isWord, $Word);

#- parse arguments list.
foreach (@ARGV) {
    /^--explain=(.*)$/ and do { $isExplain = ($Explain) = $1; $isFile = 1; $File = "/var/log/explanations"; next };
    /^--file=(.*)$/ and do { $isFile = ($File) = $1; next };
    /^--word=(.*)$/ and do { $isWord = ($Word) = $1; next };
    /^--alert$/ and do { alert_config(); quit() };
}

my $isTail = $isFile;
$| = 1 if $isTail;
my $h = chomp_(`hostname -s`);

$ugtk2::wm_icon = "logdrake";
my $explain_title = N("%s Tools Logs", N("Mageia"));
my $my_win = ugtk2->new($isExplain ? $explain_title : N("Logs"));

unless ($::isEmbedded) {
    $my_win->{window}->set_border_width(5);
    #$my_win->{window}->set_default_size(540,460);
}
$my_win->{window}->signal_connect(delete_event => \&quit);

my $cal = gtkset_sensitive(Gtk2::Calendar->new, 0);
my $mday = (localtime(time()))[3];
$cal->select_day($mday);
my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
my $cal_mode = 0;
my $cal_butt = gtksignal_connect(Gtk2::CheckButton->new(N("Show only for the selected day")), clicked => sub { $cal_mode = !$cal_mode; gtkset_sensitive($cal,$cal_mode) });

### menus definition
# the menus are not shown
# but they provides shiny shortcut like C-q (Broken: FIXME)
my @menu_items = (
		  { path => N("/_File"), item_type => '<Branch>' },
		  { path => N("/File/_New"), accelerator => N("<control>N") },
		  { path => N("/File/_Open"), accelerator => N("<control>O") },
		  { path => N("/File/_Save"), accelerator => N("<control>S"), callback => \&save },
		  { path => N("/File/Save _As") },
		  { path => N("/File/-"), item_type => '<Separator>' },
		  { path => N("/File/_Quit"), accelerator => N("<control>Q"), callback => \&quit },
		  { path => N("/_Options"), item_type => '<Branch>' },
		  { path => N("/Options/Test") },
		  { path => N("/_Help"), item_type => '<LastBranch>' },
		  { path => N("/Help/_About...") } 
		 );
my $_menubar = $::isEmbedded ? create_factory_menu($my_win->{rwindow}, @menu_items) : undef;
######### menus end


########## font and colors

    
# Define global terms:
# Define good notables:
my @word_good = ("starting\n", "Freeing", "Detected", "starting.", "accepted.\n", "authenticated.\n", "Ready", "active", "reloading", "saved;", "restarting", "ONLINE\n");
my @word_warn = ("dangling", "closed.\n", "Assuming", "root", "root\n", "exiting\n", "missing", "Ignored", "adminalert:", "deleting", "OFFLINE\n");
my @word_bad = "bad";
my @word_note = ("LOGIN", "DHCP_OFFER", "optimized", "reset:", "unloaded", "disconnected", "connect", "Successful", "registered\n");
#my @line_good = ("up", "DHCP_ACK", "Cleaned", "Initializing", "Starting", "success", "successfully", "alive", "found", "ONLINE\n");
#my @line_warn = ("warning:", "WARNING:", "invalid", "obsolete", "bad", "Password", "detected", "timeout", "timeout:", "attackalert:", "wrong", "Lame", "FAILED", "failing", "unknown", "obsolete", "stopped.\n", "terminating.", "disabled\n", "disabled", "Lost");
#my @line_bad = ("DENY", "lost", "shutting", "dead", "DHCP_NAK", "failure;", "Unable", "inactive", "terminating", "refused", "rejected", "down", "OFFLINE\n", "error\n", "ERROR\n", "ERROR:", "error", "ERROR", "error:", "failed:");

# Now define what we want to use when:
my $col_good = 'green4';
my $col_warn = 'yellow4';
my $col_bad = 'red';
my $col_note = 'purple';
my $col = 'darkcyan';

######### font and colors end

my %files = (
	     "auth" => { file => "/var/log/auth.log", desc => N("_:this is the auth.log log file\nAuthentication") },
	     "user" => { file => "/var/log/user.log", desc => N("_:this is the user.log log file\nUser") },
	     "messages" => { file => "/var/log/messages", desc => N("_:this is the /var/log/messages log file\nMessages") },
	     "syslog" => { file => "/var/log/syslog", desc => N("_:this is the /var/log/syslog log file\nSyslog") },
	     "explanations" => { file => "/var/log/explanations", desc => $explain_title }
);

my $yy = gtkset_sensitive(gtksignal_connect(Gtk2::Button->new(N("search")) , clicked => \&search),0);

my $log_text = gtktext_insert(Gtk2::TextView->new, [ [ '' ] ]);

my $log_buf = $log_text->get_buffer;
my $refcount_search;
#### far from window

my %toggle;

gtkadd($my_win->{window},
       gtkpack_(Gtk2::VBox->new(0,0),
		if_(!$::isEmbedded, 0, gtknew('Title1', label => N("A tool to monitor your logs"))),
		if_(!$isFile, 0, gtkpack__(Gtk2::VBox->new(0,2),
                                     gtknew('Title2', label => N("Settings")),
							      # N("Show lines"),
							      gtkpack__(Gtk2::HBox->new(0,0),
									" " . N("Matching") . " ", my $e_yes = Gtk2::Entry->new,
									" " . N("but not matching") . " ", my $e_no = Gtk2::Entry->new
									),
						    gtkpack_(Gtk2::HBox->new(0,0),
							     1, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Choose file")),2),
								       gtkpack(gtkset_border_width(Gtk2::VBox->new(0,0),0),
										map { $toggle{$_} = gtksignal_connect(Gtk2::CheckButton->new($files{$_}{desc}), 
														      clicked => sub {
															  $refcount_search++; 
															  gtkset_sensitive($yy, $refcount_search);
                                                                         });
                                                        $toggle{$_}->set_sensitive(0) if !-f $files{$_}{file};
                                                        $toggle{$_};
                                                    } sort keys %files,
										)
								       ),
							     0, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Calendar")),2),
								       gtkpack__(gtkset_border_width(Gtk2::VBox->new(0,0),5),
										 $cal_butt, $cal
										 )
								       )
							     ),
						    $yy,
						    )
		    ),
		!$isExplain ? (0, gtknew('Title2', label => N("Content of the file")),
					   1, create_scrolled_window($log_text)
					   ) : (1, create_scrolled_window($log_text)),
		0, gtkadd(gtkset_border_width(gtkset_layout(Gtk2::HButtonBox->new, 'end'), 5),
					      if_(!$isFile, gtksignal_connect(Gtk2::Button->new(N("Mail alert")), 
										clicked => sub { 
                                                      eval { alert_config() };
                                                      my $err = $@;
                                                      $::WizardWindow->destroy if defined $::WizardWindow;
                                                      undef $::WizardWindow;
                                                      if ($err && $err !~ /wizcancel/) {
                                                          err_dialog(N("Error"), N("The alert wizard has failed unexpectedly:")
                                                                     . "\n\n" . $err);
                                                      }
                                                  })),
					      gtksignal_connect(Gtk2::Button->new(N("Save")), clicked => \&save),
					      gtksignal_connect(Gtk2::Button->new($::isEmbedded ? N("Cancel") : N("Quit")), clicked => \&quit)
					      )
		)
       );

$isFile && !$::isEmbedded and gtkset_size_request($log_text, 400, 500);

$my_win->{window}->show_all;
search() if $isFile;
$my_win->main;

sub quit() { ugtk2->exit(0) }

#-------------------------------------------------------------
# search functions
#-------------------------------------------------------------
sub search() {
    return if !$log_text->window;
    $log_text->window->freeze_updates;
    $log_buf->set_text('');
    if ($isFile) {
	parse_file($File, $File);
    } else {
	foreach (keys %files) {
	    parse_file($files{$_}{file}, $files{$_}{desc}) if $toggle{$_}->get_active;
	}
    }
    $log_text->window->thaw_updates;
    $log_text->show;
    gtkflush();
}

my $timer;

my @logs;

my $F;

sub parse_file {
  my ($file, $descr) = @_;

  $file =~ s/\.gz$//;
  my ($pbar, $win_pb);
  unless ($::isEmbedded && $isExplain) {
      local $::main_window = $my_win->{real_window};
      gtkadd($win_pb = gtkset_modal(Gtk2::Window->new('toplevel'), 1),
             gtkpack(Gtk2::VBox->new(2,0),
                     Gtk2::Label->new(" " . N("please wait, parsing file: %s", $descr) . " "),
                     $pbar = Gtk2::ProgressBar->new
                     )
             );
      $win_pb->set_transient_for($my_win->{real_window});
      $win_pb->set_position('center');
      $win_pb->realize;
      $win_pb->show_all;
      gtkflush();
  }
  my $ey = $e_yes->get_chars(0, -1);
  my $en = $e_no->get_chars(0, -1);
  $ey =~ s/ OR /|/;
  $ey =~ s/^\*$//;
  $en =~ s/^\*$/.*/;
  $ey = $ey . $Word if $isWord;

  if ($cal_mode) {
      my (undef, $month, $day) = $cal->get_date;
      $ey = $months[$month] . "\\s{1,2}$day\\s.*$ey.*\n";
  }

  my @all = -e $file ? catMaybeCompressed($file) : N("Sorry, log file isn't available!");

  if ($isExplain) {
      my (@t, $t);
      while (@all) {
	  $t = pop @all;
	  next if $t =~ /logdrake/;
	  last if $t !~ /$Explain/;
	  push @t, $t;
      }
      @all = reverse @t;
  }

  my $taille = @all;
  my $i = 0;
  my $test;
  if ($en && !$ey) {
      $test = sub { $_[0] !~ /$en/ };
  } elsif ($ey && !$en) {
      $test = sub { $_[0] =~ /$ey/ };
  } else {
      $test = sub { $_[0] =~ /$ey/ && $_[0] !~ /$en/ };
  }

  foreach (@all) {
      $i++;
      if ($pbar && $i % 10) { 
	  $pbar->set_fraction($i/$taille);
       $win_pb->window->process_updates(1); # no gtkflush() because we do not want to refresh the TextView
      }
      
      logcolorize($_) if $test->($_);
  }
  $win_pb->destroy if !$::isEmbedded || !$isExplain;

  if ($isTail) {
      close $F if $F;
      if (open $F, $file) {
          local $_;
          while (<$F>) {}       #to prevent to output the file twice..
          #      $log_text->set_point($log_text->get_length());
          $timer ||= Glib::Timeout->add(1000, sub {
                                            logcolorize($_) while <$F>;
                                            seek $F, 0, 1;
                                        });
      } else {
          my $error = $!;
          my $string = chomp_(`LC_ALL=C date '+%b %d %T'`) . " " .
            N("Error while opening \"%s\" log file: %s\n", $file, $error);
          # `` return non utf8 and concat of non utf8 & utf8 is non utf8:
          c::set_tagged_utf8($string);
          logcolorize($string);
      }
  }     
  insert_text_n_scroll();
}


##########################################################################################

sub logcolorize {
    my ($string) = @_;
    # we get date & time if it is date & time (dmesg)
    $string =~ s/(\D{3} .. (\d\d:\d\d:\d\d ))//;
    my $timestamp = $isExplain ? $2 : $1;
    my @rec = split(/\s/, $string);
    @rec = map { c::set_tagged_utf8($_); $_ } @rec if utf8::is_utf8($string);

    log_output($timestamp,  'Bold', 'darkcyan'); # date & time if any...
    # BUG: $col hasn't yet be reseted
    $isExplain or log_output("$rec[0] ",  'Bold', $rec[0] eq $h ? 'blue' : $col);  # hostname
    
    if ($rec[1] eq "last") {
	log_output(" last message repeated ",  undef, 'green');
	log_output($rec[4],  'Bold', 'green');
	log_output(" times\n",  undef, 'green');
	return;
    }
    # Extract PID if present
    if ($rec[1] =~ /\[(\d+)\]:/) {
	my $pid = $1;
	$rec[1] =~ s/\[$1\]://;
	log_output($rec[1] . "[",  undef, 'green');
	log_output($pid,  'Bold', 'black');
	log_output("]: ",  undef, 'green');
    }
    else {
	log_output($rec[1] . " ",  undef, 'green');
    }

    foreach my $therest (2 .. $#rec) {
	$col = 'darkcyan';

	# Check for keywords to highlight
	foreach (@word_good) { $col = $col_good if $_ eq $rec[$therest] }
	foreach (@word_warn) { $col = $col_warn if $_ eq $rec[$therest] }
	foreach (@word_bad)  { $col = $col_bad  if $_ eq $rec[$therest] }
	foreach (@word_note) { $col = $col_note if $_ eq $rec[$therest] }
	
 	# Watch for words that indicate entire lines should be highlighted
 	#foreach (@line_good) { $col = $col_good if $_ eq $rec[$therest] }
 	#foreach (@line_warn) { $col = $col_warn if $_ eq $rec[$therest] }
	#foreach (@line_bad)  { $col = $col_bad  if $_ eq $rec[$therest] }
 	
	log_output("$rec[$therest] ", undef, $col);
    }
    log_output("\n",  undef, 'black');
    insert_text_n_scroll() if $isExplain;
}


#    log_output (Gtk2::TextView, [ [ ... ] ])
sub log_output {
    my ($text, $font, $col) = @_;
    my $tag = join('', $font, $col);
    push @logs, [ $text, $tag ];
    $log_buf->{tags}{$tag} ||= { foreground => $col }; # if_($font, font => $font), 
}

sub insert_text_n_scroll() {
    ugtk2::gtktext_insert($log_text, \@logs, append => 1);
    $log_text->scroll_to_iter($log_buf->get_end_iter, 0, 1, 0.5, 0.5);
    undef @logs;
}


#-------------------------------------------------------------
# mail/sms alert
#-------------------------------------------------------------

sub alert_config() {
    local $::isEmbedded = 0;
    undef $::WizardTable;
    undef $::WizardWindow;
    my $conffile = "/etc/sysconfig/mail_alert";
    my %options = getVarsFromSh($conffile);
    $options{LOAD} ||= 3;
    $options{MAIL} ||= "root";
    $options{SMTP} ||= "localhost";
    
    my $service = {
		  httpd => N("Apache World Wide Web Server"), 
		  bind => N("Domain Name Resolver"),
		  ftp => N("Ftp Server"),
		  postfix => N("Postfix Mail Server"),
		  samba => N("Samba Server"),
		  sshd => N("SSH Server"),
		  webmin => N("Webmin Service"),
		  xinetd => N("Xinetd Service")
		 };
    my @installed_d = grep { -e "/etc/init.d/$_" } sort keys %$service;
    my %services_to_check = map { $_ => 1 } split(':', $options{SERVICES});

    $::isWizard = 1;
    my $mode;
    my $cron_file = "/etc/cron.hourly/logdrake_service";
    my %modes = (
                   configure => N("Configure the mail alert system"),
                   disable =>  N("Stop the mail alert system"),
                  );
    require wizards;
    my $wiz = wizards->new({
               defaultimage => "logdrake.png",
               name => N("Mail alert"),
               pages => {
                         welcome => {
                                     name => N("Mail alert configuration") . "\n\n" .
                                     N("Welcome to the mail configuration utility.\n\nHere, you'll be able to set up the alert system.\n"),
                                     no_back => 1,
                                     data => [
                                              { val => \$mode, label => N("What do you want to do?"),
                                                list => [ keys %modes ], format => sub { $modes{$_[0]} },  },
                                              ],

                                     post => sub { $mode eq 'configure' ? 'services' : 'stop' },
                                    },
                         services => {
                                      name => N("Services settings") . "\n\n" .
                                      N("You will receive an alert if one of the selected services is no longer running"),
                                      data => [ map { { label => $_, val => \$services_to_check{$_}, 
                                                          type => "bool", text => $service->{$_} } } @installed_d ],
                                      next => "load",
                                     },
                         load => {
                                  #PO- Here "load" is a noun; that is load refers to the system/CPU) load
                                  name => N("Load setting") . "\n\n" .
                                  N("You will receive an alert if the load is higher than this value"),
                                  data => [ { label => N("_: load here is a noun, the load of the system\nLoad"), 
                                              val => \$options{LOAD}, type => 'range', min => 1, max => 50 } ],
                                  next => "email",
                                 },
                         email => {
                                   name => N("Alert configuration") . "\n\n" .
                                   N("Please enter your email address below ") . "\n" .
                                   N("and enter the name (or the IP) of the SMTP server you wish to use"),
                                   data => [
                                            { label => "Email address", val => \$options{MAIL} },
                                            { label => "Email server", val => \$options{SMTP} },
                                           ],
                                   complete => sub {
                                       if ($options{MAIL} !~ /[\w.-]*\@[\w.-]/ && !member($options{MAIL}, map { $_->[0] } list_passwd())) {
                                           err_dialog(N("Error"), N("\"%s\" neither is a valid email nor is an existing local user!",
                                                                          $options{MAIL}));
                                           return 1;
                                       }
                                       if (member($options{MAIL}, map { $_->[0] } list_passwd()) && $options{SMP} !~ /localhost/) {
                                           err_dialog(N("Error"), N("\"%s\" is a local user, but you did not select a local smtp, so you must use a complete email address!", $options{MAIL}));
                                           return 1;
                                       }
                                   },
                                   next => "end",
                                  },
                         end => {
                                 name => N("Congratulations") . "\n\n" . N("The wizard successfully configured the mail alert."), 
                                 end => 1,
                                 no_back => 1,
                                },
                         stop => {
                                  pre => sub { eval { rm_rf($cron_file) } },
                                  name => N("Congratulations") . "\n\n" . N("The wizard successfully disabled the mail alert."), 
                                  end => 1,
                                  no_back => 1,
                                },
                        },
              });
    $wiz->process($in);
    return if $mode eq 'disable';
    
    $options{SERVICES} = join ':', grep { $services_to_check{$_} } sort keys %services_to_check;

    use Data::Dumper;
    output_with_perm $cron_file, 0755, q(#!/usr/bin/perl
# generated by logdrake
use MDK::Common;
my $r;
my %options = getVarsFromSh("/etc/sysconfig/mail_alert");

#- check services
my ) . Data::Dumper->Dump([ $service ], [qw(*services)]) . q(
foreach (split(':', $options{SERVICES})) {
    next unless $services{$_};
    $r .= "Service $_ ($services{$_} is not running)\\n" unless -e "/var/lock/subsys/$_";
}

#- load
my ($load) = split ' ', first(cat_("/proc/loadavg"));
$r .= "Load is huge: $load\n" if $load > $options{LOAD};

#- report it
if ($r) {
    use Mail::Mailer;
    my $mailer = Mail::Mailer->new('smtp', Server => $options{SMTP});
    $mailer->open({ From    => 'root@localhost',
                    To      => $options{MAIL},
                    Subject => "DrakLog Mail Alert",
                  })
      or die "Cannot open: $!\n";
    print $mailer $r;
    $mailer->close;
}

# EOF);
    setVarsInSh($conffile, \%options);
        
    if (defined $::WizardWindow) {
	$::WizardWindow->destroy;
	undef $::WizardWindow;
    }
}


#-------------------------------------------------------------
# menu callback functions
#-------------------------------------------------------------


sub save() {
    $::isWizard = 0;
    my $y = $in->ask_filename({ title => N("Save as.."), directory => "/root", save => 1 }) or return;
    my $buf = $log_text->get_buffer;
    my ($start, $end) = $buf->get_bounds;
    output($y, $buf->get_text($start, $end, 0));
}