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

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

use lib "USR_PREFIX/lib/systemimager/perl";
use strict;
use Tk;
use Tk::Label;
use Tk::Balloon;
use Tk::HList;
use Tk::ItemStyle;
use Tk::ROText;
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";
$program_name\n(part of SystemImager) v$VERSION

Copyright (C) 2005 Andrea Righi <a.righi\@cineca.it>
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.

 --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); 
GetOptions(
        "help"          => \$help,
        "version"       => \$version,
        "db=s"          => \$database,
        "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;
}

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

# Graphic stuff.
my (
	$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,
);

# Initialize refresing status.
$is_refreshing = 0;

# Set default sort options.
$sort_field = 'mac';
$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 => 'I~mage',    -description => 'Image installed in the client' },
    chr(0x04) => { -name => 'status', -label => '~Status',   -description => 'Status of installation' },
    chr(0x05) => { -name => 'time',   -label => '~Time',     -description => 'Elapsed time for installation' },
    chr(0x06) => { -name => 'speed',  -label => 'S~peed',    -description => 'Network bandwidth (KB/s)' },
    chr(0x07) => { -name => 'mem',    -label => '~RAM',      -description => 'Physical RAM installed in the client' },
    chr(0x08) => { -name => 'tmpfs',  -label => 'RAM ~used', -description => 'Allocated RAM' },
    chr(0x09) => { -name => 'cpu',    -label => '~CPU',      -description => 'CPU model' },
};
# 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);

# Use this font.
my $FONT = '-*-Helvetica-Bold-R-Normal--*-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,
);
my $sort_menu = $edit_menu->cascade(
	-label     => 'Sort',
	-underline => 0,
	-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 clients",
	-bitmap	    	=> 'question',
	-default_button => 'No',
	-buttons        => [qw/Yes No/],
	-text           => "Are you sure to clear all collected clients informations?",
);
my $DIALOG_NO_CLIENTS = $window->Dialog(
	-title          => "Clear all collected clients",
	-bitmap		    => 'warning',
	-default_button => 'OK',
	-buttons        => ['OK'],
	-text           => "No clients collected.",
);
my $DIALOG_ABOUT = $window->Dialog(
	-title          => "About $program_name",
	-bitmap 		=> 'info',
	-default_button => 'OK',
	-buttons        => ['OK'],
    -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(
	'HList',
	-header		=> 1,
	-columns	=> 10,
	-scrollbars	=> 'se',
	-width		=> 110,
	-height		=> 25,
	-selectmode	=> 'browse',
	-selectbackground => 'yellow',
	-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',
    -font       => $FONT,
    -anchor     => 'center',
);
# Failed.
my $fail_style = $tab->ItemStyle('text');
$fail_style->configure(
    -foreground => '#FF0000',
    -font       => $FONT,
    -anchor     => 'center',
);
# Done.
my $done_style = $tab->ItemStyle('text');
$done_style->configure(
    -foreground => '#009000',
    -font       => $FONT,
    -anchor     => 'center',
);
# Installing.
my $inst_style = $tab->ItemStyle('text');
$inst_style->configure(
    -foreground => '#000000',
    -font       => $FONT,
    -anchor     => 'center',
);
# Unknown.
my $unkn_style = $tab->ItemStyle('text');
$unkn_style->configure(
    -foreground => 'darkgrey',
    -font       => $FONT,
    -anchor     => 'center',
);

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

# Show the body.
display_nodes();

# 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);

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

	# Open the console in a new window.
	$sub_win = MainWindow->new();

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

	# Store MAC address of the selected client.
    $sub_mac_addr = $mac;

	# Console body.
	$sub_tab = $sub_win->Scrolled(
		'ROText',
		-scrollbars => 'e',
		-wrap       => 'word',
		-background => 'black',
		-foreground => 'lightgrey',
		-font       => $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);
	$sub_tab->insert('end',
		"$hostname($ip):> virtual console started for client $mac.\n",
		'title'
	);

	# Refresh console view (default 100ms).
	my $cons_timer = Tk::After->new(
			$sub_win, 
			100,
			'repeat', 
			\&refresh_console
	);
}

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

	# 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);

	# Parse clients data.
	my %client = %{$xml->{'client'}};

	# Correct the host name in the title of the console.
	if ($client{$mac}->{'host'}) {
		$sub_win->title('Client: ' . $client{$mac}->{'host'});
	}

	# Generate the last message.
	my $line = ($client{$mac}->{'host'} || '') . 
		'(' . $client{$mac}->{'ip'} . ')' .
		":> " .
		($client{$mac}->{'log'} || '') . "\n";

    # Replace escape sequences for the special field separator ':'.
    $line =~ s/\\x3a/:/g;

	# Get previous message.
	my $last_line = $sub_tab->get('end - 2 lines', 'end - 1 lines');

	# Display the client message only if it is different from
	# the previous one.
	if ($last_line ne $line) {
		$sub_tab->insert('end', $line);
		$sub_tab->see('end');
	}
}

# 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 to refresh the status of the nodes",
	);
	$bal->attach($stop_refresh_button,
		-statusmsg  => "Stop to refresh 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
{
    $selected_mac_addr = $tab->itemCget($_[0], 0, 'text');
}

# 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_ok = 0;
    my $num_fail = 0;
    my $num_inst = 0;
    my $num_unkn = 0;

    # Delete all clients.
    $tab->delete('all');
    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') {
        @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);
            } else {
                ($sort_order) ? ($x cmp $y) : ($y cmp $x);
            }
        } keys %{$client};
    }
	foreach my $mac (@list) {
		# Create the entry.
		my $row = $tab->addchild('');
		# Evaluate the status and adjust the style of the entry.
		my $style;
		my $status = $client->{$mac}->{'status'} || 0;
		if (!$status) {
            $num_inst++;
			$client->{$mac}->{'status'} = 'initializing...';
			$style = $init_style;
		} elsif ($status < 0) {
            $num_fail++;
			$client->{$mac}->{'status'} = 'error!';
			$style = $fail_style;
		} elsif ($status == 100) {
            $num_ok++;
			$client->{$mac}->{'status'} = 'done';
			$style = $done_style;
		} elsif (($status > 0) && ($status < 100)) {
            $num_inst++;
            $client->{$mac}->{'status'} = sprintf('%.2f%%', $status);
			$style = $inst_style;
		} else {
            $num_unkn++;
            $client->{$mac}->{'status'} = '???';
			$style = $unkn_style;
		}
		# Format the uptime.
        if (defined($client->{$mac}->{'time'})) {
            $client->{$mac}->{'time'} .= 'min';
        }
		# Format the bandwidth.
        if ($client->{$mac}->{'speed'}) {
            $client->{$mac}->{'speed'} .= 'KB/s'; 
        } else {
            $client->{$mac}->{'speed'} = '-';
        }
		# Format the total amount of RAM.
        if (defined($client->{$mac}->{'mem'})) {
            $client->{$mac}->{'mem'} .= 'MB';
        }
		# Display the node identity (MAC address).
		$tab->itemCreate(
			$row,
			0,
			-itemtype => 'text',
			-style    => $style,
			-text     => $mac,
		);
		# Display other node attributes.
		for (0 .. $#attrs) {
            $tab->itemCreate(
			    $row,
    			$_ + 1,
    			-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 ###
    $bal->attach($tab,
        -statusmsg  => "Managed nodes: " . ($num_ok  + $num_inst + $num_unkn + $num_fail) .
            " ($num_ok ok, $num_inst installing, $num_unkn unknown, $num_fail error)",
    );
    # 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;
	}

	# 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);

	# 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<--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 2003 by Andrea Righi <a.righi@cineca.it>.

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

