#!/usr/bin/perl -w
# File: atsar-tk
# Mark Overmeer, AT Computing, The Netherlands
# Example for YAPC::Europe 2001
#
# This example program demonstrates a graphical interface built over
# the output of the linux version of the command `atsar'.  It is *not*
# a full implementation, nor robust.  The main limitation is that
# it can not handle multi-network-interface and multi-processor
# information as presented by atsar (when applicable)
#
# Atsar is available from ftp://ftp.ATComputing.nl/pub/tools/linux

use strict;

use Tk;
use Tk::Dialog;
use IO::File;

use lib 'examples';
use vumeter;

my $atsar    = 'atsar';
my $interval = 1;

# Fields of the header.
# Linux  meteor  2.2.13  #4 Tue Mar 28 18:54:06 CEST 2000  i686  04/23/2000

my @headerfields = qw/sysname nodename release runlevel
   buildwday buildmonth buildmday buildtime buildtimezone buildyear
   processor date/;
my @showheader   = qw/nodename date sysname release processor/;

my @tables =
( { button  => 'Processor'
  , title   => 'Processor information'
  , command => "$atsar -u"
  , extras  => \&cpu_create_bar
  , update  => \&cpu_update_bar
  }
, { button  => 'Disk util'
  , title   => 'Disk Utilization'
  , command => "$atsar -d"
  }
, { button  => 'Disk part'
  , title   => 'Disk Partition Utilization'
  , command => "$atsar -D"
  }
, { button  => 'Memory'
  , title   => 'Memory and Swap'
  , command => "$atsar -r"
  , convert => sub { $_ = ($`*1024).'K' if $_[0] ne 'time' && /M$/ }
  }
, { button  => 'Paging'
  , title   => 'Paging and Swapping'
  , command => "$atsar -p"
  }
, { button  => 'Interrupts'
  , title   => 'Interrupts per seconds'
  , command => "$atsar -I"
  }
, { button  => 'Kernel'
  , title   => 'Utilization of kernel-tables'
  , command => "$atsar -v"
  }
, { button  => 'Networking'
  , title   => 'Network interfaces'
  , command => "$atsar -l"
  }
, { button  => 'Net errors'
  , title   => 'Network interface errors'
  , command => "$atsar -l"
  }
, { button  => 'IP traffic'
  , title   => 'IP network traffic'
  , command => "$atsar -w"
  }
, { button  => 'IP errors'
  , title   => 'IP network errors'
  , command => "$atsar -W"
  }
, { button  => 'ICMP layer'
  , title   => 'General ICMP layer'
  , command => "$atsar -m"
  }
, { button  => 'ICMP per-type'
  , title   => 'Per-type ICMP layer'
  , command => "$atsar -M"
  }
, { button  => 'TCP traffic'
  , title   => 'TCP network traffic'
  , command => "$atsar -t"
  }
, { button  => 'TCP errors'
  , title   => 'TCP traffic errors'
  , command => "$atsar -T"
  }
);

# FILLHASH
# Split the line in fields according to the column-names.

sub fillhash($$@)
{   my ($hash, $line, @fieldorder) = @_;
    @$hash{ @fieldorder } = split " ", $line;
}

# FIELDLIST
# Create Tk-list combining fieldname and related value.

sub fieldlist($$$;@)
{   my ($mw, $hash, $anchor, @fieldselection) = @_;

    # If we received a list of options, we take that list, otherwise
    # all fields are taken in the order of the fields as found in the
    # atsar output.
    my @order = @fieldselection
              ? @fieldselection
              : $hash->{_default_order};

    my $f = $mw->Frame;

    foreach (@order)
    {
        # Label showing the name of the field.
        my $name    = $f->Label
           ( -text    => $_
           , -anchor  => 'w'
           );

        # Be sure the field is initialised before the tie for
        # the button is created.
        $hash->{$_} = ''
           unless exists $hash->{$_};

        # Label showing the actual value for the field.
        my $value   = $f->Label
          ( -textvariable => \$hash->{$_}
          , -anchor  => $anchor 
          , -width   => 12
          );

        # Put both labels next to each other as one row within the frame.
        $name->grid($value, -sticky => 'we');
    }

    $f;
}

# CREATE_HEADER(parent)
# General system information window.

sub create_header($)
{   my $mw     = shift;
    my $header = {};

    # Get first line with content.
    open IN, "$atsar -r 1 0 |" or die;
    while(<IN>) {last if length > 10}
    close IN;

    # Combine content of line with variables.
    fillhash $header, $_, @headerfields;

    # Create the visible representation for this data.  Not
    # all fields are shown.
    my $window = fieldlist $mw, $header, 'w', @showheader;

    ($header, $window);
}

sub start()
{   my $table  = shift;
    my $fields = {};

    my $in = new IO::File;

    # Start atsar with the right flags.  To have atsar run unlimited,
    # you have to request for a huge number of intervals.
    open $in, "$table->{command} $interval 100000 |" or die;

    # Get the table description line.
    while(<$in>) { last if /_\s*$/ }
    my @line_order = split;       # get the column-names.
    $line_order[0] = 'time';      # replace start-time by a name.
    pop @line_order;              # name of value-set not needed.

    # Create the window to display the requested kind of atsar output.
    my $popup = new MainWindow;
    $popup->title($table->{title});

    # Create the (vertical) list of fieldnames -- values.
    my $l = fieldlist $popup, $fields, ($table->{anchor} || 'e'), @line_order;

    # We need some info later, when displaying the lines of values.
    $fields->{_default_order} = \@line_order;
    $fields->{_update}        = $table->{update} if exists $table->{update};
    $fields->{_convert}       = $table->{convert}
        if exists $table->{convert};

    # You may want to stop once...
    my $d = $popup->Button
      ( -text    => 'Dismiss'
      , -command => [ sub {close $in; $popup->destroy} ]
      );

    # Show description of value-set.
    $popup->Label
      ( -text    => $table->{title}
      , -width   => 20
      , -anchor  => 'c'
      )->grid(-sticky => 'ew');

    # Show how often we update.
    $popup->Label
      ( -text    => "Interval: $interval secs"
      , -anchor  => 'c'
      )->grid(-sticky => 'ew');

    # Hook for tricks.
    if(exists $table->{extras})
    {   my $extras = $table->{extras}->($popup);
        $l->grid($extras);
        $d->grid(-pady => '3m');
    }
    else
    {   $l->grid();
        $d->grid(-pady => '3m');
    }

    # Start waiting for new datalines to arrive.
    $popup->fileevent($in, 'readable', [ \&line, $popup, $in, $fields ]);

    $popup;
}

#
# LINE
# Process one arrived line.
#

sub line($$)
{   my ($popup, $file, $hash) = @_;
    my $line = <$file>;

    if($line =~ /not supported/)
    {    $popup->Dialog
           ( -text    => 'Not supported'
           , -buttons => [ 'OK' ]
           )->Show;

         $popup->withdraw;
         $popup->destroy;
    }

    my @fields = split " ", $line;

    if(exists $hash->{_convert})
    {   my @order  = @{$hash->{_default_order}};
        map {$hash->{_convert}->(shift @order)} @fields;
    }

    # Update the values related to the columns.  Because to the tie
    # which is created by Tk, the value displayed on the related labels
    # are automagically updated.
    # A line may only contain a time-update.

    my @line_order = @{$hash->{_default_order}};
    if(@fields==1) { $hash->{$line_order[0]} = $fields[0] }
    else           { @$hash{ @line_order }   = @fields    }

    # Hook for updating extras.
    $hash->{_update}->($popup, $hash)
        if exists $hash->{_update};

    $popup;
}

#
# Create an cpu-bar
#

sub cpu_create_bar($)
{   my $parent  = shift;
    my $cpu_bar = $parent->vumeter
      ( -background  => 'white'
      , -maxsum      => 100
      );

    $parent->Advertise(cpu => $cpu_bar);
    $cpu_bar;
}

sub cpu_update_bar($$)
{   my ($parent, $hash) = @_;
    $parent->Subwidget('cpu')
           ->addValues([ @$hash{qw(%usr %sys %nice)} ]);
}

# Create entry where user can specify the update interval.
sub create_interval($)
{   my $mw = shift;
    my $f  = $mw->Frame;

    my $l  = $f->Label(-text => 'Interval', -anchor => 'e');
    my $e  = $f->Entry(-textvariable => \$interval, -width => 5);
    my $s  = $f->Label(-text => 'secs', -anchor => 'w');
    
    $l->grid($e, $s, -sticky => 'ne');
    $f;
}

# Create a matrix of buttons for all kinds of atsar output.
sub create_buttons($)
{   my $mw  = shift;

    my $buttons = $mw->Frame;
    my @buttons = map { $buttons->Button
      ( -text    => $_->{button}
      , -command => [ \&start, $_ ]
      ) } @tables;

    for(my $i=0; $i<@buttons; $i++)
    {   $buttons[$i]->grid
          ( -row    => $i%4
          , -column => int($i/4)
          , -sticky => 'ew'
          );
    }

    $buttons;
}

#
# Create MainWindow
#

my $mw               = new MainWindow;
my ($header, $frame) = create_header   $mw;
my $set_interval     = create_interval $mw;
my $buttons          = create_buttons  $mw;

my $quit = $mw->Button
  ( -text     => 'Quit'
  , -command  => sub {exit 0}
  );

$frame->grid($buttons, -sticky => 'nsew');
$set_interval->grid($quit, -pady => '3m');

MainLoop;
