#!/usr/local/bin/perl -w
# -*- perl -*-

#
# $Id: pingomatic,v 1.5 2002/02/21 09:11:45 eserte Exp $
# Author: Slaven Rezic
#
# Copyright (C) 1999, 2000, 2001, 2002 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# Mail: slaven.rezic@berlin.de
# WWW:  http://user.cs.tu-berlin.de/~eserte/
#

use Event;
use IO::Pipe;
use Getopt::Long;
use Term::ReadKey;
use Term::ReadLine;
use Sys::Hostname;
use strict;
use vars qw($VERSION @nameserver %pipe);

$VERSION = sprintf("%d.%03d", q$Revision: 1.5 $ =~ /(\d+)\.(\d+)/);

package
    IO::Pipe::End;
sub pid {
    my $me = shift;
    ${*$me}{'io_pipe_pid'};
}
package main;

package PingDef;

use constant MAXTIME => 20000;

use vars qw(%try_later);

sub new {
    my $self = { @_[1..$#_] };
    $self->{Val} = [];
    bless $self, $_[0];
}

sub host {
    $_[0]->{Host};
}

sub active {
    !$_[0]->{Canceled};
}

sub time {
    my($self, $n) = @_;
    my $timesum = 0;
    my $count = 0;
    my $start = $#{$self->{Val}}-$n+1;
    $start = 0 if ($start < 0) ;
    for(my $i=$start; $i<=$#{$self->{Val}}; $i++) {
	$timesum+=$self->{Val}[$i]{Time};
	$count++;
    }
    if ($count) {
	$timesum/$count;
    } else {
	0; # XXX
    }
}

sub add {
    my($self, $line) = @_;
    if ($line =~ /icmp_seq=(\d+).*time=([\d.]+)/) {
	my($seq, $time) = ($1, $2);
## noch nicht korrekt...
#  	my $last_line = $self->{Val}[$#{$self->{Val}}];
#  	if (defined $last_line && defined $last_line->{Seq}) {
#  	    foreach ($last_line->{Seq} .. $seq-1) { # verlorene Pakete
#  		push @{ $self->{Val} }, {Time => MAXTIME};
#  	    }
#  	}
	push @{ $self->{Val} }, {Seq => $seq,
				 Time => $time};
	$self->{Canceled} = 0;
    } elsif ($line =~ /ret=-1/) {
	push @{ $self->{Val} }, {Time => MAXTIME}; #XXX
	$self->{Canceled} = 1;
    } else {
	warn "Can't parse $line";
    }
    shift @{ $self->{Val} } if (@{ $self->{Val} } > 10);
}

sub addempty {
    my($self) = @_;
    push @{ $self->{Val} }, {Time => MAXTIME}; #XXX
    shift @{ $self->{Val} } if (@{ $self->{Val} } > 10);
}

sub canceled {
    my $self = shift;
    my $watcher = shift;
    $self->addempty;
    $self->{Canceled} = 1;
    $try_later{$self->{Host}} = $self;
    $self->{Watcher} = $watcher;
}

package main;

my $hostgroup = "normal";
my @addhost;
my $do_ns = 1;
my $hostfile;
GetOptions("hosts=s" => \$hostgroup,
	   "hostfile=s" => \$hostfile,
	   "host=s@" => \@addhost,
	   "ns|nameserver!" => \$do_ns);

my @hosts;
if (defined $hostfile && -r $hostfile) {
    open(H, $hostfile) or die "Can't open $hostfile";
    while(<H>) {
	chomp;
	push @hosts, $_;
    }
    close H;
} elsif (open(H, "$ENV{HOME}/.pingomatic.hosts")) {
    chomp(@hosts = <H>);
    close H;
} elsif ($hostgroup =~ /^cpan$/i) {
    @hosts = qw(ftp.gmd.de
		ftp.gwdg.de
		ftp.leo.org
		ftp.mpi-sb.mpg.de
		ftp.rz.ruhr-uni-bochum.de
		ftp.uni-erlangen.de
		ftp.uni-hamburg.de
		ftp.digital.com
		ftp.cdrom.com
		www.perl.org
		www.cpan.org
		);
} elsif ($hostgroup =~ /^freebsd$/i) {
    @hosts = qw(ftp.cs.tu-berlin.de
		ftp.freebsd.org
		ftp.freebsd.de
		ftp2.freebsd.org
		ftp3.freebsd.org
	       );
} elsif ($hostgroup =~ /^(search|such)/i) {
    @hosts = qw(www.altavista.com
		www.altavista.de
		www.yahoo.com
		www.infoseek.com
		www.hotbot.com
		www.lycos.com
		www.lycos.de
		www.dejanews.com
		search.opentext.com
		home.netscape.com
		webcrawler.com
		netguide.de
		www.web.de
		www.fireball.de
		www.hurra.de
		suchen.com
		mserv.rrzn.uni-hannover.de
	       );
} else {
    @hosts = qw(www.onlineoffice.de
		smtp.www-service.de
		www.altavista.com
		www.altavista.de
		www.google.de
		user.cs.tu-berlin.de
		ftp.cs.tu-berlin.de
		ftp.fu-berlin.de
		www.linux.org
		ftp.perl.org
		mail.cs.tu-berlin.de
		ftp.cs.tu-berlin.de
		www.rezic.de
		bbbike.sourceforge.net
		);
    if (hostname =~ /onlineoffice.de/) {
	unshift @hosts, qw(hobbes.intra.onlineoffice.de
			   rosalyn.intra.onlineoffice.de
			   calvin.intra.onlineoffice.de
			   dad.intra.onlineoffice.de
			   chucky.intra.onlineoffice.de
			   spiff.intra.onlineoffice.de
			   homer.intra.onlineoffice.de
			  );
    }

# ping ausgeschaltet:
#		www.netscape.com
#		www.microsoft.com
#		mail.pixelpark.com
#		ftp.freebsd.org
#		www.cpan.org
# uninteressant
#		adidas.pixelpark.de

}

push @hosts, @addhost;
push @hosts, @ARGV if @ARGV;

if ($do_ns) {
    get_nameserver();
    push @hosts, @nameserver;
}

my $clearchr = `clear`;

my $update = 1;

my $term = new Term::ReadLine 'pingomatic';

my %pingdefs;

open(STDERR, ">/dev/null");

foreach my $host (@hosts) {
    new_ping($host);
}

my $update_w =
    Event->timer(desc => "update",
		 interval => $update,
		 cb => \&show_ping_stat,
		 );

Event->timer(
	     interval => 20,
	     cb => \&try_later,
	    );

Event->io(
	  fd => \*STDIN,
	  poll => 'r',
	  cb => \&handle_key,
	  repeat => 1,
	 );

ReadMode 3;

Event::loop();

sub get_ping_line {
    my $e = shift;
    my $got = $e->got;
    my $fd = $e->w->fd;
    my $host = $e->w->desc;
    my $pingdef = $pingdefs{$host};
    if ($got eq "r") {
	if (eof $fd) {
	    $pingdef->canceled($e->w);
	    $e->w->stop;
	} else {
	    my $line = scalar <$fd>;
	    chomp $line;
	    $pingdef->add($line);
	}
    } else {
	$pingdef->addempty;
    }
}

sub show_ping_stat {
    my $res = '';
    foreach my $pingdef (map {
	$_->[1]
	} sort {
	    $a->[0] <=> $b->[0]
	} map {
	    [$_->time(10), $_]; 
	} values %pingdefs) {
	$res .= sprintf
	    "%-30s %-1s %-10s %-10s\n",
	    $pingdef->host,
	    $pingdef->active ? 'x' : ' ',
	    $pingdef->time(1),
	    $pingdef->time(10);
    }

    my $statusline = "*** " . scalar(localtime) . " update: $update s ***\n";
    print "$clearchr$statusline$res";
}

sub get_nameserver {
    @nameserver = ();
    if (open(NS, "/etc/resolv.conf")) {
	while(<NS>) {
	    s/#.*//g;
	    if (/nameserver\s+(.*)\s*$/) {
		push @nameserver, $1;
	    }
	}
	close NS;
    } else {
	warn "Can't open resolv.conf\n";
    }
    if (!@nameserver) {
	warn "Can't get any nameserver\n";
    }
}

sub try_later {
#XXX geht nicht
#     foreach my $host (keys %PingDef::try_later) {
# 	if (fork == 0) {
# 	    system("ping", "-c", "1", $host);
# 	    if (!$?) {
# 		my $pingdef = $PingDef::try_later{$host};
# 		$pingdef->{Canceled} = 0;
# 		$pingdef->{Watcher}->start;
# 		delete $PingDef::try_later{$host};
# 	    }
# 	}
#     }
}

sub handle_key {
    my $e = shift;
    my $got = $e->got;
    if ($got eq 'r') {
	if (defined(my $key = ReadKey(-1))) {
	    my $update_changed;
	    if ($key =~ /^[qx]$/) {
		exit(0);
	    } elsif ($key eq '-' and $update > 1) {
		$update--;
		$update_changed++;
	    } elsif ($key eq '+') {
		$update++;
		$update_changed++;
	    } elsif ($key =~ /^\d$/ and $key > 0) {
		$update = $key;
		$update_changed++;
	    } elsif ($key eq 'a') {
		add_host();
	    }

	    if ($update_changed) {
		$update_w->interval($update);
	    }
	}
    }
}

sub add_host {
    my @stopped;
    foreach (Event::all_running()) {
	$_->stop;
	push @stopped, $_;
    }
    ReadMode 0;
    my $host = $term->readline("\nadd host: ");
    if (defined $host and $host !~ /^\s*$/) {
	new_ping($host);
    }
    ReadMode 3;
    $_->start foreach @stopped;
}

sub new_ping {
    my $host = shift;

    $pipe{$host} = new IO::Pipe;
    $pipe{$host}->reader(qw(ping), $host);

    my $pingdef = new PingDef Host => $host;
    $pingdefs{$host} = $pingdef;

    Event->io(
	      fd => $pipe{$host},
	      poll => 'r',
	      timeout => 5,
	      repeat => 1,
	      desc => $host,
	      cb => \&get_ping_line,
	      );

}

__END__

=head1 NAME

pingomatic - multiple ping to a number of hosts

=head1 DESCRIPTION

This utility pings to a number of hosts in parallel.

=head2 OPTIONS

=over 4

=item -hosts hostgroup

Use a predefined host group. Predefined host groups are: C<cpan>,
C<freebsd>, and C<search>.

=item -hostfile filename

Use another file with host names instead of the default
C<~/.pingomatic.hosts>.

=item -host hostname

Add the named hostname to the list of hosts. This option may be
specified multiple times.

=item -ns

Add the locally configured name servers to the list of hosts.

=back

=head2 KEYS

While the script is pinging, the user can use the following keys:

=over 4

=item a

Add interactively another host to the list.

=item +

Add one second to the current update interval. The default update
interval is one second.

=item -

Subtract one second from the current update interval.

=item Ctrl-C

Terminate the script.

=back

=head1 README

This utility pings to a number of hosts in parallel.

=head1 FILES

    ~/.pingomatic.hosts     - a list of host names to ping

=head1 PREREQUISITES

C<Event>, C<Term::ReadKey>.

=head1 OSNAMES

only tested on Unix

=head1 SCRIPT CATEGORIES

Networking

=head1 AUTHOR

Slaven Rezic <slaven.rezic@berlin.de>

=head1 SEE ALSO

ping(1).

=cut


