#!/usr/bin/perl -w
#
#  "SystemImager"
#
#  Copyright (C) 2005-2006 Andrea Righi <a.righi@cineca.it>
#  Copyright (C) 2005-2006 Bernard Li <bli@bcgsc.ca>

use lib "/usr/lib/systemimager/perl";
use strict;
use Tk::HList;

# Hack the HList object to properly handle the single click on a list
# element.
package Tk::MyHList;
@Tk::MyHList::ISA = qw(Tk::HList);
Construct Tk::Widget 'MyHList';

sub ButtonRelease_1 {
}

package main;

# set version number
my $VERSION = "3.7.5";
my $program_name = "si_monitortk";

use Socket;
use Tk;
use Tk::Label;
use Tk::Balloon;
use Tk::ItemStyle;
use Tk::ROText;
use Tk::ProgressBar;
use Tk::widgets qw/Dialog ErrorDialog/;
use Fcntl ':flock';
use XML::Simple;
use Getopt::Long qw(:config no_ignore_case bundling);

my $version_info = << "EOF";
perl (v$])
Tk (v$Tk::VERSION)

$program_name (part of SystemImager) v$VERSION

Copyright (C) 2005-2006 Andrea Righi <a.righi\@cineca.it>
Copyright (C) 2005-2006 Bernard Li <bli\@bcgsc.ca>

This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF

my $get_help = "\n       Try \"--help\" for more options.";

my $help_info = $version_info . <<"EOF";

Usage: $program_name [OPTION]...

Options: (options can be presented in any order and may be abbreviated)
 --help                 Display this output.

 --version              Display version and copyright information.

 --db DATABASENAME      Get clients informations from the XML file
                        DATABASENAME.

 --progress             Display a graphical progress bar, instead of
                        numerical percentages.

 --refresh RATE         Set the refresh rate (in sec).

Download, report bugs, and make suggestions at:
http://systemimager.org/
EOF

my $CONFDIR = '/etc/systemimager';

# load resources
my %conf;
my $conffile = "$CONFDIR/$program_name";
if (-r $conffile) {
    Config::Simple->import_from($conffile, \%conf);
}

# Get the lock directory. 
$conf{'lock_dir'} ||= "/var/lock/systemimager";
die "No such lock directory '$conf{'lock_dir'}'\n"
    if (! -d $conf{'lock_dir'});

# Get the database file.
$conf{'monitor_db'} ||= "/var/lib/systemimager/clients.xml";

my ($help, $version, $quiet, $database, $refresh_rate, $progress); 
GetOptions(
        "help"          => \$help,
        "version"       => \$version,
        "db=s"          => \$database,
        "progress"      => \$progress,
        "refresh=i"     => \$refresh_rate,
) or die "$help_info";
if ($help) {
    print "$help_info";
    exit(0);
}
if ($version) {
    print "$version_info";
    exit(0);
}

# Create the database if it doesn't exist.
$database = $conf{'monitor_db'};
unless (-f $database) {
    open(DB, '>', "$database") or
    die "error: cannot open file \"$database\" for writing!\n";
    close(DB);
}

# Set the refresh rate.
unless ($refresh_rate) {
    $refresh_rate = 5000;
} else {
    $refresh_rate *= 1000;
}

# Check if X is available.
unless ($ENV{DISPLAY}) {
    print STDERR "WARNING: this program requires a X server to run!\n";
}

# Define lock files.
my $lock_file = $conf{'lock_dir'} . "/db.si_monitor.lock";

# Graphic stuff.
my (
    # Main window.
    $window,
    $tab, $timer, $status_bar, 
    $start_refresh_button, $stop_refresh_button,
    $clear_clients_button,
    $quit_button,
    
    # Selected node entry in the list.
    $selected_mac_addr,
    
    # Check if refreshing clients.
    $is_refreshing,

    # Field used to sort the nodes table.
    $sort_field,
    $sort_order,

    # Virtual console stuff.
    $sub_win,
    $sub_tab,
    $sub_title,
    $sub_mac_addr,
    $sub_ip,
    $sub_hostname,
    $sub_cons_timer,
    $sub_conn_failed,
);

# List to keep track of status ProgressBar objects.
my @status_progress_list = ();

# List of current messages on the virtual console.
my @curr_logmsg = ();

# Initialize refresing status.
$is_refreshing = 0;

# Set default sort options.
$sort_field = 'ip';
$sort_order = 1;

# Client attributes.
my $attrs_struct = {
### KEY ###     ### NAME ###       ### LABEL ###          ### DESCRIPTION ###
    chr(0x00) => { -name => 'mac',       -label => '~MAC',         -description => 'Network MAC address' },
    chr(0x01) => { -name => 'ip',        -label => '~IP',          -description => 'Network IP address' },
    chr(0x02) => { -name => 'host',      -label => '~Hostname',    -description => 'Name of the client' },
    chr(0x03) => { -name => 'os',        -label => 'Ima~ge',       -description => 'Image installed in the client' },
    chr(0x04) => { -name => 'kernel',    -label => '~Kernel',      -description => 'SystemImager Kernel version' },
    chr(0x05) => { -name => 'status',    -label => '~Progress',    -description => 'Progress of installation' },
    chr(0x06) => { -name => 'time',      -label => '~Time',        -description => 'Elapsed time for installation' },
    chr(0x07) => { -name => 'speed',     -label => 'Sp~eed',       -description => 'Network bandwidth' },
    chr(0x08) => { -name => 'mem',       -label => '~RAM',         -description => 'Physical RAM installed in the client' },
    chr(0x09) => { -name => 'tmpfs',     -label => 'RAM ~used',    -description => 'Allocated RAM' },
    chr(0x0a) => { -name => 'ncpus',     -label => '~nCPUs',       -description => 'Number of CPUs' },
    chr(0x0b) => { -name => 'cpu',       -label => '~CPU',         -description => 'CPU model' },
    chr(0x0c) => { -name => 'first_timestamp', -label => 'Image ~start time', -description => 'Timestamp of the first report' },
    chr(0x0d) => { -name => 'timestamp', -label => '~Last update', -description => 'Timestamp of the last report' },
};
# Cache attribute names.
my @attrs = grep !/mac/, map {$attrs_struct->{$_}->{'-name'}} sort keys %{$attrs_struct};

# Create the main window.
$window = MainWindow->new();
$window->title("$program_name: client installation monitoring system");
$window->configure(-menu => my $menubar = $window->Menu(-border => 0));
$window->gridRowconfigure(0, -weight => 1);
$window->gridColumnconfigure(0, -weight => 1);
$window->protocol('WM_DELETE_WINDOW' => \&quit);

# Image resources.
my $server_init_icon  = $window->Photo(-file => '/usr/share/systemimager/icons/serverinit.gif');
my $server_inst_icon  = $window->Photo(-file => '/usr/share/systemimager/icons/serverinst.gif');
my $server_ok_icon    = $window->Photo(-file => '/usr/share/systemimager/icons/serverok.gif');
my $server_error_icon = $window->Photo(-file => '/usr/share/systemimager/icons/servererror.gif');

# Use this font.
my $FONT       = '-*-helvetica-bold-r-normal-*-12-120-*-*-*-*-*-*';
my $FIXED_FONT = '-*-monospace-bold-r-normal-*-12-120-*-*-*-*-*-*';

# Generate the "File" menu bar.
my $file_menu = $menubar->cascade(
    -label     => 'File',
    -underline => 0,
    -tearoff   => 0,
);
my $refresh_file_menu = $file_menu->command(
    -label    => '~Refresh',
    -command  => \&start_refresh,
    -state    => 'disabled',
);
my $stop_refresh_file_menu = $file_menu->command(
    -label    => '~Stop refresh',
    -command  => \&stop_refresh,
    -state    => 'normal',
);

$file_menu->separator();

my $clear_clients_file_menu = $file_menu->command(
    -label    => '~Clear clients',
    -command  => \&clear_clients,
    -state    => 'normal',
);

$file_menu->separator();

my $quit_file_menu = $file_menu->command(
    -label    => '~Quit',
    -command  => \&quit,
);

# Generate the "Edit" menu bar.
my $edit_menu = $menubar->cascade(
    -label     => 'Edit',
    -underline => 0,
    -tearoff   => 0,
);
$edit_menu->checkbutton(
    -label     => 'Show ~progress bars',
    -command   => \&refresh,
    -variable  => \$progress,
);
my $sort_menu = $edit_menu->cascade(
    -label     => 'Sort',
    -underline => 1,
    -tearoff   => 0,
);
# Show sort attributes.
foreach (sort keys %{$attrs_struct}) {
    $sort_menu->radiobutton(
        -label    => $attrs_struct->{$_}->{'-label'},
        -command  => \&refresh,
        -variable => \$sort_field,
        -value    => $attrs_struct->{$_}->{'-name'},
        -state    => 'normal',
    );
}
$sort_menu->separator();
$sort_menu->radiobutton(
    -label    => '~Ascending',
    -command  => \&refresh,
    -variable => \$sort_order,
    -value    => 1,
    -state    => 'normal',
);
$sort_menu->radiobutton(
    -label    => '~Descending',
    -command  => \&refresh,
    -variable => \$sort_order,
    -value    => 0,
    -state    => 'normal',
);

# Generate the "About" menu bar.
my $help_menu = $menubar->cascade(qw/-label Help -underline 0 -tearoff 0 -menuitems/ => [
    [command => '~About'],
]);

# Dialogs.
my $DIALOG_CLEAR_CLIENTS = $window->Dialog(
    -title          => "Clear all collected client data",
    -bitmap         => 'question',
    -default_button => 'No',
    -buttons        => [qw/Yes No/],
    -font           => $FONT,
    -text           => "Are you sure you want to clear all collected client data?",
);
my $DIALOG_NO_CLIENTS = $window->Dialog(
    -title          => "Clear all collected client data",
    -bitmap         => 'warning',
    -default_button => 'OK',
    -buttons        => ['OK'],
    -font           => $FONT,
    -text           => "No client data collected.",
);
my $DIALOG_ABOUT = $window->Dialog(
    -title          => "About $program_name",
    -bitmap         => 'info',
    -default_button => 'OK',
    -buttons        => ['OK'],
    -font           => $FONT,
    -wraplength     => '4i',
    -text           => "$version_info",
);
$help_menu->cget(-menu)->entryconfigure('About',
    -command => [$DIALOG_ABOUT => 'Show'],
);

# Define the status bar format.
$status_bar = $window->Label(
    -relief => "sunken", 
    -bd => 1, 
    -anchor => 'sw'
);

# Define the balloon structure.
my $bal = $window->Balloon(-statusbar => $status_bar);

# Create the table of nodes status.
$tab = $window->Scrolled(
    'MyHList',
    -header           => 1,
    -columns          => $#attrs + 3, # attributes + primary key + icon
    -scrollbars       => 'se',
    -width            => 110,
    -height           => 28,
    -selectmode       => 'browse',
    -selectbackground => 'yellow',
    -background       => 'white',
    -command          => \&show_virtual_console,
    -browsecmd        => \&update_selected_mac,
)->grid(qw/-sticky nsew/);

# Create the tab item styles.
# Initializing.
my $init_style = $tab->ItemStyle('text');
$init_style->configure(
    -foreground => '#0000FF',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Failed.
my $fail_style = $tab->ItemStyle('text');
$fail_style->configure(
    -foreground => '#FF0000',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Done.
my $done_style = $tab->ItemStyle('text');
$done_style->configure(
    -foreground => 'darkcyan',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Done.
my $post_install_style = $tab->ItemStyle('text');
$post_install_style->configure(
    -foreground => '#009000',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Rebooted.
my $rebooted_style = $tab->ItemStyle('text');
$rebooted_style->configure(
    -foreground => '#000000',
    -background => '#B8FFB1',
    -font       => $FONT,
    -anchor     => 'center',
);
# Installing.
my $inst_style = $tab->ItemStyle('text');
$inst_style->configure(
    -foreground => '#000000',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Unknown.
my $unkn_style = $tab->ItemStyle('text');
$unkn_style->configure(
    -foreground => 'darkgrey',
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Image style.
my $image_style = $tab->ItemStyle('imagetext');
$image_style->configure(
    -background => '#FFFFFF',
    -font       => $FONT,
    -anchor     => 'center',
);
# Rebooted image style.
my $rebooted_image_style = $tab->ItemStyle('imagetext');
$rebooted_image_style->configure(
    -background => '#B8FFB1',
    -font       => $FONT,
    -anchor     => 'center',
);
# Window style.
my $window_style = $tab->ItemStyle('window');
$window_style->configure(
    -anchor     => 'center',
);

# Create the table header.
$tab->header('create', 0, -text => ' ');
my $i = 1;
foreach (sort keys %{$attrs_struct}) {
    $_ = $attrs_struct->{$_}->{'-label'};
    s/~//g;
    $tab->header('create', $i++, -text => $_); 
}

# Show the body.
display_nodes();

# Display a legend.
show_legend();

# Display command buttons.
show_command_buttons();

# Main loop.
start_refresh();
MainLoop();

# Well done.
exit(0);

########################################################################
#
#    Functions
#
########################################################################

# Usage:
# show_virtual_console();
# Description:
#    Open the virtual console window.
sub show_virtual_console 
{
    # Allow only one virtual console -AR-
    # TODO: handle more than one console.
    $sub_win->destroy() if Exists($sub_win);

    @curr_logmsg = ();

    # Get info to show in the title.
    my $mac = $tab->itemCget($_[0], 1, 'text');
    my $ip = $tab->itemCget($_[0], 2, 'text');
    my $hostname  = $tab->itemCget($_[0], 3, 'text') || '';

    # Open the console in a new window.
    $sub_win = MainWindow->new();
    $sub_win->bind('<Escape>' => sub { $sub_win->destroy() });

    # Display the hostname or the IP address in the title.
    $sub_win->title('Client: ' .
        ("$hostname" ne '-' ? "$hostname ($ip)" : "$ip"));

    # Store MAC address, ip and hostnames of the selected client.
    $sub_mac_addr = $mac;
    $sub_ip = $ip;
    $sub_hostname = $hostname;

    # Console body.
    $sub_tab = $sub_win->Scrolled(
        'ROText',
        -scrollbars    => 'e',
        -width         => 80,
        -height        => 25,
        -wrap          => 'word',
        -background    => 'black',
        -foreground    => 'lightgrey',
        -font          => $FIXED_FONT,
        -setgrid       => 1,
        -insertofftime => 0,
    )->grid(-in => $sub_win, -row => 1, -column => 1, -sticky => 'nsew');
    $sub_win->gridRowconfigure(1, -weight => 1);
    $sub_win->gridColumnconfigure(1, -weight => 1);

    # Bind mouse wheel events to the virtual console.
    $sub_win->bind('all', '<4>',   => sub { $sub_tab->yview('scroll', -3, 'units') });
    $sub_win->bind('all', '<5>',   => sub { $sub_tab->yview('scroll', +3, 'units') });
    $sub_win->bind('all', '<Home>' => sub { $sub_tab->yviewMoveto(0) });
    $sub_win->bind('all', '<End>'  => sub { $sub_tab->yviewMoveto(1) });

    $sub_tab->insert('end',
        ">>> virtual console started for client $mac <<<\n",
        'title'
    );

    # Gather all log messages from the installing client.
    $sub_tab->insert('end',
        "gathering previous messages...\n",
        'title'
    );
    $sub_win->update();
    if (refresh_console()) {
        # Error: host unreachable. 
        $sub_tab->insert('end',
            "host unreachable: cannot gather previous messages!\n",
            'title'
        );
    } else {
        # Initialize counter of connections failed.
        $sub_conn_failed = 0;

        # Refresh console view (default 5sec).
        $sub_cons_timer = Tk::After->new(
            $sub_win,
            $refresh_rate,
            'repeat',
            \&refresh_console
        );
    }
    $sub_win->update();
}

# Usage:
# $ret = refresh_console();
# Description:
#    Refresh the virtual console view.
sub refresh_console
{
    # Get mac address of the client to monitor.
    my $mac = $sub_mac_addr;
    my $ip = $sub_ip;
    my $hostname = $sub_hostname;

    # Correct the host name in the title of the console.
    if ($hostname) {
        $sub_win->title('Client: ' .
            ("$hostname" ne '-' ? "$hostname ($ip)" : "$ip"));
    }
    $sub_win->update();

    # Get previous messages.
    my @old_logmsg = ();
    eval {
        # Socket timeout (sec).
        my $TMO = 1;
        # Create the socket.
        socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or
            die "error: cannot create TCP socket for client $ip!\n";
        # Set a timeout on the socket.
        setsockopt(SOCK, SOL_SOCKET, SO_SNDTIMEO, pack('L!L!', $TMO, 0)) or 
            die "error: cannot set SO_SNDTIMEO option to socket\n";
        setsockopt(SOCK, SOL_SOCKET, SO_RCVTIMEO, pack('L!L!', $TMO, 0)) or
            die "error: cannot set SO_RCVTIMEO option to socket\n";
        # Connection to the client.
        connect(SOCK, sockaddr_in(8181, inet_aton($ip))) or
            die "error: connection failed\n";
    };
    if ($@) {
        if (++$sub_conn_failed > 10) {
            # Stop refreshing virtual console.
            $sub_cons_timer->cancel();
        }
        # Log gathering failed.
        return -1;
    } else {
        $sub_conn_failed = 0;
        # Gather all logs.
        @old_logmsg = <SOCK>;
        close(SOCK);
        if (@old_logmsg && ($#old_logmsg > $#curr_logmsg)) {
            # Save old cursor position.
            my $view = $sub_tab->index('@0,0');
            my $view_end = ($sub_tab->yview)[1];

            # Fill virtual console only with new messages.
            for (my $i = $#curr_logmsg + 1; $i < $#old_logmsg + 1; $i++) {
                $old_logmsg[$i] =~ s/\x{8}//g; # ignore backspaces
                $old_logmsg[$i] =~ s/\x{7}//g; # ignore system bell
                $sub_tab->insert('end', $old_logmsg[$i]);
            }
            # Update cursor position.
            if ($view_end == 1) {
                $sub_tab->yviewMoveto($view_end);
            } else {
                $sub_tab->yview($view);
            }

            # Update current messages.
            @curr_logmsg = @old_logmsg;
        }
        return 0;
    }
}

# Usage:
# show_legend(); 
# Description:
#    Display the legend panel.
sub show_legend
{
    # Define buttons.
    my $legend = $window->Frame();
    $legend->grid(qw/-pady 2m -sticky ew/);
    $legend->gridColumnconfigure(qw/0 -weight 1/);
    $legend->gridColumnconfigure(qw/1 -weight 1/);
    $legend->gridColumnconfigure(qw/2 -weight 1/);
    $legend->gridColumnconfigure(qw/3 -weight 1/);
    $legend->Label(
        -image => $server_init_icon,
    )->grid(qw/-row 0 -column 0/);
    $legend->Label(
        -text => 'Initialization',
    )->grid(qw/-row 1 -column 0/);
    $legend->Label(
        -image => $server_inst_icon,
    )->grid(qw/-row 0 -column 1/);
    $legend->Label(
        -text => 'Installation in progress',
    )->grid(qw/-row 1 -column 1/);
    $legend->Label(
        -image => $server_ok_icon,
    )->grid(qw/-row 0 -column 2/);
    $legend->Label(
        -text => 'Installation completed',
    )->grid(qw/-row 1 -column 2/);
    $legend->Label(
        -image => $server_error_icon,
    )->grid(qw/-row 0 -column 3/);
    $legend->Label(
        -text => 'Installation failed!',
    )->grid(qw/-row 1 -column 3/);

    # Draw the legend.
    $legend->grid("-", "-", -sticky => "nesw");
}

# Usage:
# show_command_buttons(); 
# Description:
#    Display the command buttons panel.
sub show_command_buttons
{
    # Define buttons.
    my $buttons = $window->Frame;
    $start_refresh_button = $buttons->Button(
        -text      => 'Refresh',
        -underline => 0,
        -command   => [\&start_refresh],
        -width     => 20,
        -pady      => 5,
        -padx      => 5,
        -state     => 'disabled', 
    );
    $stop_refresh_button = $buttons->Button(
        -text      => 'Stop refresh',
        -underline => 0,
        -command   => [\&stop_refresh],
        -width     => 20,
        -pady      => 5,
        -padx      => 5,
        -state     => 'normal', 
    );
    $clear_clients_button = $buttons->Button(
        -text      => 'Clear clients',
        -underline => 0,
        -command   => [\&clear_clients],
        -width     => 20,
        -pady      => 5,
        -padx      => 5,
        -state     => 'normal', 
    );
    $quit_button = $buttons->Button(
        -text      => 'Quit',
        -underline => 0,
        -command   => [\&quit],
        -width     => 20,
        -pady      => 5,
        -padx      => 5,
    );
    $buttons->grid(qw/-pady 2m -sticky ew/);
    $buttons->gridColumnconfigure(qw/0 -weight 1/);
    $buttons->gridColumnconfigure(qw/1 -weight 1/);
    $buttons->gridColumnconfigure(qw/2 -weight 1/);
    $buttons->gridColumnconfigure(qw/3 -weight 1/);
    $start_refresh_button->grid(qw/-row 0 -column 0/);
    $stop_refresh_button->grid(qw/-row 0 -column 1/);
    $clear_clients_button->grid(qw/-row 0 -column 2/);
    $quit_button->grid(qw/-row 0 -column 3/);

    # Bind hot-keys to buttons.
    $window->bind("<Alt-r>", sub {$start_refresh_button->invoke;});
    $window->bind("<Alt-s>", sub {$stop_refresh_button->invoke;});
    $window->bind("<Alt-c>", sub {$clear_clients_button->invoke;});
    $window->bind("<Alt-q>", sub {$quit_button->invoke;});

    # Define status bar messages.
    $bal->attach($start_refresh_button,
        -statusmsg  => "Start refreshing the status of the nodes",
    );
    $bal->attach($stop_refresh_button,
        -statusmsg  => "Stop refreshing the status of the nodes",
    );
    $bal->attach($clear_clients_button,
        -statusmsg  => "Clear all collected clients",
    );
    $bal->attach($quit_button,
        -statusmsg  => "Quit program",
    );
    
    # Draw status bar.
    $status_bar->grid("-", "-", -sticky => "nesw");
}

# Usage:
# update_selected_mac(); 
# Description:
#    Set the selected client entry in the table.
sub update_selected_mac
{
    my $prev_sel = (defined($selected_mac_addr) ? $selected_mac_addr : '');
    $selected_mac_addr = $tab->itemCget($_[0], 1, 'text');
    if ($prev_sel eq $selected_mac_addr) {
        undef($selected_mac_addr);
        $tab->selectionClear();
        $tab->anchorClear();
    }
}

# Usage:
# display_nodes(); 
# Description:
#    Fill the main list with the status of all nodes.
sub display_nodes
{
    ### XXX: Profiling stuff ###
    #use Time::HiRes qw(gettimeofday tv_interval);
    #my $t0 = [gettimeofday];
    ### END of profiling stuff ###

    # Statistic counters.
    my $num_rebooted  = 0;
    my $num_shutdown  = 0;
    my $num_rebooting = 0;
    my $num_beeping   = 0;
    my $num_ok        = 0;
    my $num_fail      = 0;
    my $num_inst      = 0;
    my $num_unkn      = 0;

    # Drop previous selection if nothing is selected.
    if (defined($selected_mac_addr)) {
        undef($selected_mac_addr)
            unless (defined($tab->info('selection')));
    }

    # Delete all clients.
    $tab->delete('all');
    # Explicitly destroy the ProgressBar objects if present.
    foreach (@status_progress_list) {
        $_->destroy();
    }
    @status_progress_list = ();
    unless (-s $database) {
        goto status_bar_update;
    }

    # Open and read lock the database.
    open(LOCK, ">", "$lock_file") or
        die "error: cannot open lock file \"$lock_file\"!\n";
    flock(LOCK, LOCK_SH);
    
    # Parse XML database.
    my $xml = XMLin($database, KeyAttr => {client => 'name'}, ForceArray => 1);

    # Close and unlock database.
    flock(LOCK, LOCK_UN);
    close(LOCK);

    # Display DB entries.
    my $client = $xml->{'client'};
    # Sort clients.
    my @list;
    if ($sort_field eq 'mac') {
        # Primary key.
        @list = sort {($sort_order) ? ($a cmp $b) : ($b cmp $a)} keys %{$client};
    } else {
        @list = sort {
            my $x = $client->{$a}->{$sort_field} || 0;
            my $y = $client->{$b}->{$sort_field} || 0;
            if (($x =~ /^(\d+)%$/) && ($y =~ /^(\d+)%$/)) {
                $x =~ s/%//;
                $y =~ s/%//;
            }
            if ((($x =~ /^(\d+)$/) && ($y =~ /(\d+)$/)) || 
                (($x =~ /^(\d+).(\d+)$/) && ($y =~ /(\d+).(\d+)$/))) {
                ($sort_order) ? ($x <=> $y) : ($y <=> $x);
            } elsif ((my @a = $x =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) && (my @b = $y =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)) {
                  ($sort_order) ?
                      $a[0] <=> $b[0] || $a[1] <=> $b[1] || $a[2] <=> $b[2] || $a[3] <=> $b[3] :
                      $b[0] <=> $a[0] || $b[1] <=> $a[1] || $b[2] <=> $a[2] || $b[3] <=> $a[3];
            } else {
                ($sort_order) ? ($x cmp $y) : ($y cmp $x);
            }
        } keys %{$client};
    }
    foreach my $mac (@list) {
        my $status;
        my $status_progress;
        my $icon_style;

        # Create the entry.
        my $row = $tab->addchild('');
        # Evaluate the status and adjust the style of the entry.
        my ($style, $status_icon);
        $status = $client->{$mac}->{'status'} || 0;
        if (!$status) {
            $num_inst++;
            $client->{$mac}->{'status'} = 'initializing...';
            $style = $init_style;
            $status_icon = $server_init_icon;
            $icon_style = $image_style;
        } elsif ($status < 0) {
            $num_fail++;
            $client->{$mac}->{'status'} = 'error!';
            $style = $fail_style;
            $status_icon = $server_error_icon;
            $icon_style = $image_style;
        } elsif (($status > 0) && ($status < 100)) {
            $num_inst++;
            $client->{$mac}->{'status'} = sprintf('%.2f%%', $status);
            $style = $inst_style;
            $status_icon = $server_inst_icon;
            $icon_style = $image_style;
        } elsif ($status == 100) {
            $num_ok++;
            $client->{$mac}->{'status'} = 'imaged';
            $style = $done_style;
            $status_icon = $server_ok_icon;
            $icon_style = $image_style;
        } elsif ($status == 101) {
            $num_inst++;
            $client->{$mac}->{'status'} = 'finalizing...';
            $style = $inst_style;
            $status_icon = $server_inst_icon;
            $icon_style = $image_style;
        } elsif ($status == 102) {
            $num_rebooted++;
            $client->{$mac}->{'status'} = 'REBOOTED';
            $style = $rebooted_style;
            $status_icon = $server_ok_icon;
            $icon_style = $rebooted_image_style;
        } elsif ($status == 103) {
            $num_beeping++;
            $client->{$mac}->{'status'} = 'beeping';
            $style = $post_install_style;
            $status_icon = $server_ok_icon;
            $icon_style = $image_style;
        } elsif ($status == 104) {
            $num_rebooting++;
            $client->{$mac}->{'status'} = 'rebooting';
            $style = $post_install_style;
            $status_icon = $server_ok_icon;
            $icon_style = $image_style;
        } elsif ($status == 105) {
            $num_shutdown++;
            $client->{$mac}->{'status'} = 'shutdown';
            $style = $post_install_style;
            $status_icon = $server_ok_icon;
            $icon_style = $image_style;
        } else {
            $num_unkn++;
            $client->{$mac}->{'status'} = '???';
            $style = $unkn_style;
            $status_icon = $server_error_icon;
            $icon_style = $image_style;
        }
        # Format the uptime.
        if (defined($client->{$mac}->{'time'})) {
            $client->{$mac}->{'time'} .= 'min';
        }
        # Format the bandwidth.
        if (!$client->{$mac}->{'speed'}) {
            $client->{$mac}->{'speed'} = '-';
        } else {
            if ($client->{$mac}->{'speed'} < 1000) {
                $client->{$mac}->{'speed'} = 
                sprintf("%.0fKB/s", $client->{$mac}->{'speed'});
            } else {
                $client->{$mac}->{'speed'} = 
                sprintf("%.2fMB/s", $client->{$mac}->{'speed'} / 1000);
            }
        }
        # Format the total amount of RAM.
        if (defined($client->{$mac}->{'mem'})) {
            $client->{$mac}->{'mem'} .= 'MB';
        }
        # Display the node icon.
        $tab->itemCreate(
            $row,
            0,
            -itemtype => 'imagetext',
            -style    => $icon_style,
            -image    => $status_icon,
        );
        # Display the node identity (MAC address).
        $tab->itemCreate(
            $row,
            1,
            -itemtype => 'text',
            -style    => $style,
            -text     => $mac,
        );
        # Display other node attributes.
        for (0 .. $#attrs) {
            # Status.
            if (($progress) && ($attrs[$_] eq 'status') && ($status > 0) && ($status < 100)) {
                my $_progress_val;
                $status_progress = $tab->ProgressBar(
                    -borderwidth => 0,
                    -relief      => 'flat',
                    -padx        => 2,
                    -pady        => 8,
                    -variable    => \$_progress_val,
                    -colors      => [
                        0  => '#0000FF',
                        10 => '#0000EF',
                        20 => '#0000DF',
                        30 => '#0000CF',
                        40 => '#0000BF',
                        50 => '#0000AF',
                        60 => '#00009F',
                        70 => '#00008F',
                        80 => '#00007F',
                        90 => '#00006F',
                    ],
                    -troughcolor => 'white',
                    -gap         => 1,
                    -blocks      => 10,
                    -from        => 0,
                    -to          => 100,
                );
                $_progress_val = $status;
                push(@status_progress_list, $status_progress);

                # Display the status using a progress bar.
                $tab->itemCreate(
                    $row,
                    $_ + 2,
                    -itemtype => 'window',
                    -style    => $window_style,
                    -widget   => $status_progress,
                );
            # Timestamp.
            } elsif ($attrs[$_] =~ /timestamp/) {
                my $date;
                if ($client->{$mac}->{$attrs[$_]}) {
                    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
                        localtime($client->{$mac}->{$attrs[$_]});
                    $mon++;
                    $year += 1900;
                    $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
                        $year, $mon, $mday, $hour, $min, $sec);
#                    my $date = strftime("%Y-%m-%d %H:%M:%S", $client->{$mac}->{$attrs[$_]});
                } else {
                    $date = '-';
                }
                $tab->itemCreate(
                    $row,
                    $_ + 2,
                    -itemtype => 'text',
                    -style    => $style,
                    -text     => $date,
                );
            # Others.
            } else {
                $tab->itemCreate(
                    $row,
                    $_ + 2,
                    -itemtype => 'text',
                    -style    => $style,
                    -text     => $client->{$mac}->{$attrs[$_]} || '-',
                );
            }
        }
        # Update selection.
        if (defined($selected_mac_addr) && ($selected_mac_addr eq $mac)) {
            $tab->selectionSet($row);
            $tab->anchorSet($row);
        }
    }
    # Update the status bar.
status_bar_update:
    ### XXX: Profiling stuff ###
    #printf("elapsed time = %0.6f\n", tv_interval($t0, [gettimeofday]));
    ### END of profiling stuff ###

    # Format statistics.
    my $stats_msg = '';
    $stats_msg .= "$num_rebooted rebooted, "   if ($num_rebooted);
    $stats_msg .= "$num_ok imaged, "           if ($num_ok);
    $stats_msg .= "$num_inst installing, "     if ($num_inst);
    $stats_msg .= "$num_beeping beeping, "     if ($num_beeping);
    $stats_msg .= "$num_shutdown shutdown, "   if ($num_shutdown);
    $stats_msg .= "$num_rebooting rebooting, " if ($num_rebooting);
    $stats_msg .= "$num_fail failed, "         if ($num_fail);
    $stats_msg .= "$num_unkn unknown, "        if ($num_unkn);
    $stats_msg =~ s/, $//;
    my $stats_tot = ($num_rebooted + $num_ok  + $num_inst +
                     $num_beeping + $num_shutdown + $num_rebooting +
                     $num_fail + $num_unkn);
    # Show statistics in the status bar.
    $bal->attach($tab,
        -statusmsg  => "Managed nodes: $stats_tot ($stats_msg)",
    );
    # Refresh view.
    $tab->update();
    # Refresh the status bar.
    $bal->update();
}

# Usage:
# refresh();
# Description:
#    Refresh the client label.
sub refresh
{
    # FIXME: maybe a semaphore/spinlock is needed here.
    # This is very ugly, but it is a simple way to implement 
    # something similar to a critical section.
    return if ($is_refreshing);
    $is_refreshing = 1;

    # Refresh clients.
    display_nodes();

    $is_refreshing = 0;
}

# Usage:
# start_refresh();
# Description:
#    Begin to refresh the nodes view.
sub start_refresh
{
    # Refresh nodes visualization.
    $timer = Tk::After->new($window, $refresh_rate, 'repeat', \&refresh);

    # Refresh the buttons look.
    $start_refresh_button->configure(-state => 'disabled');
    $stop_refresh_button->configure(-state => 'normal');

    # Refresh the menu look.
    $refresh_file_menu->configure(-state => 'disabled');
    $stop_refresh_file_menu->configure(-state => 'normal');
}

# Usage:
# stop_refresh();
# Description:
#    Stop to refresh the nodes view.
sub stop_refresh
{
    # Stop to refresh nodes view.
    $timer->cancel();

    # Refresh the interface look.
    $start_refresh_button->configure(-state => 'normal');
    $stop_refresh_button->configure(-state => 'disabled');

    # Refresh the menu look.
    $refresh_file_menu->configure(-state => 'normal');
    $stop_refresh_file_menu->configure(-state => 'disabled');
}

# Usage:
# clear_clients();
# Description:
#    Clear all collected informations
sub clear_clients
{
    # If database is empty simply quit.
    unless (-s $database) {
        $DIALOG_NO_CLIENTS->Show();
        return;
    }

    # Ask for a confirmation before deleting.
    my $answer = $DIALOG_CLEAR_CLIENTS->Show();
    if ($answer eq "No") {
        return;
    }

    # Restart the monitor server if needed.
    my $status = system "/etc/init.d/systemimager-server-monitord status >/dev/null 2>&1";
    if (!$status) {
        system "/etc/init.d/systemimager-server-monitord stop >/dev/null 2>&1";
    }

    # Close the virtual consoles if opened.
    $sub_win->destroy() if Exists($sub_win);

    # Open and read lock the database.
    open(LOCK, '>', "$lock_file") or
        die "error: cannot open lock file \"$lock_file\"!\n";
    flock(LOCK, LOCK_EX);

    # Clear the XML database.
    open(DB, '>', $database) or
        die "error: cannot open \"$database\" for writing!\n";
    close(DB);

    # Close and unlock database.
    flock(LOCK, LOCK_UN);
    close(LOCK);

    if (!$status) {
        system "/etc/init.d/systemimager-server-monitord start >/dev/null 2>&1";
    }

    # Refresh view.
    refresh();
}

# Usage:
# quit();
# Description:
#    Exit program.

sub quit
{
    # Close the virtual console.
    $sub_win->destroy() if Exists($sub_win);
    # Close the main window.
    $window->destroy() if Exists($window);
    # Quit program.
    exit(0);
}

__END__

=head1 NAME

si_monitortk - systemimager monitor Tk-based GUI

=head1 SYNOPSIS

si_monitortk [OPTIONS]... --db DATABASENAME 

=head1 DESCRIPTION

Report a list of all the clients with a detailed real time
status of their installation.

The B<si_monitortk> is a perl-Tk user interface. It does not collect
directly the client informations, but simply gets that informations
in a file generated and updated by the B<si_monitor> daemon.

=head1 OPTIONS

=over 8

=item B<--help>

Display a short help.

=item B<--version>

Display version and copyright information.

=item B<--db DATABASENAME>

Perform a periodical polling to the B<DATABASENAME> where B<si_monitor>
stores the clients informations and the current status of the installation
process.

This file is created and kept updated by the B<si_monitor> daemon, so
to use this interface probably you need to start first the B<si_monitor>
daemon.

For default the file B</var/lib/systemimager/clients.xml> is taken.

=item B<--progress>

Display progress bars instead of textual precentages for installation
status.

=item B<--refresh RATE>

If specified this options sets a different refresh rate for the clients
informations displayed in the GUI. This sets the period (in sec) between
two different accesses to the B<DATABASENAME> XML file.

The default value is 5 seconds.

=head1 SEE ALSO

systemimager(8), si_monitor(1)

=head1 AUTHOR

Andrea Righi <a.righi@cineca.it>.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2005-2006 Andrea Righi <a.righi@cineca.it>
Copyright (C) 2005-2006 Bernard Li <bli@bcgsc.ca>

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.

=cut

