#!/usr/bin/perl

# I received this without copyright, but have since been told by the owner
# that it is released into the public domain.
#
# Any changes I make are released into the public domain.
# - M. Drew Streib <dtype@dtype.org>
#
# Similarly to the above, any changes I make are also released into
# the public domain.
# - Peter Pentchev <roam@ringlet.net>

use v5.010;
use strict;
use warnings;

use version; our $VERSION = version->declare("1.1.1");

use Getopt::Std;

my $have_ansi_color;
BEGIN {
	eval {
		require Term::ANSIColor;
		$have_ansi_color = 1;
	};
}

my %prevstat;

sub usage($)
{
	my ($err) = @_;
	my $s = <<EOUSAGE
Usage:	ethstats [-t] [-C | -M] [-c count] [-i iface] [-n period]
	ethstats -V | -h

	-C	color the "total" line in the output
	-c	exit after the specified number of samples
	-h	display program usage information and exit
	-i	specify a single network interface to monitor
	-M	do not color the "total" line in the output
	-n	specify the polling period in seconds (default: 10)
	-t	prefix each line with the Unix timestamp
	-V	display program version information and exit
EOUSAGE
	;

	if ($err) {
		die $s;
	} else {
		print $s;
	}
}

sub version()
{
	say "ethstats $main::VERSION";
}

sub format_line($ $)
{
	my ($name, $dev) = @_;

	return ($name eq 'total'? 'total:     ': sprintf('  %6s:  ', $name)).
	    sprintf('%7.2f Mb/s In  %7.2f Mb/s Out - '.
	    '%8.1f p/s In  %8.1f p/s Out',
	    $dev->{kb}{in}, $dev->{kb}{out},
	    $dev->{packets}{in}, $dev->{packets}{out});
}

sub update_traffic($ $ $)
{
	my ($dev, $odev, $period) = @_;

	for my $what (keys %{$dev}) {
		for my $dir (keys %{$dev->{$what}}) {
			my $diff = $dev->{$what}{$dir} -
			    ($odev->{$what}{$dir} // 0);
			$diff += 4294967296 if $diff < 0;

			$odev->{$what}{$dir} = $dev->{$what}{$dir};
			$dev->{$what}{$dir} = $diff / $period;
		}
	}
}

sub add_up_totals($ $)
{
	my ($total, $dev) = @_;

	for my $what (keys %{$total}) {
		for my $dir (qw(in out)) {
			$total->{$what}{$dir} += $dev->{$what}{$dir};
		}
	}
}

#Inter-|   Receive                                                |  Transmit
# face |bytes    packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
#    lo:    2356      32    0    0    0     0          0         0     2356      32    0    0    0     0       0          0
#  eth0: 1217210    9400    0    0    0     8          0        11  1207648    8019    0    0    0     0       0          0
#  eth1: 2039952   21982    6    0    0     6          0         0 47000710   34813    0    0    0   821       0          0

sub convert($ $ $)
{
	my ($iface, $period, $total) = @_;

	open my $in, '/proc/net/dev' or die "Can't open /proc/net/dev: $!\n";
	<$in>; <$in>;
	my %devs;
	while (my $l = <$in>) {
		chop $l;
		my ($dev, $rest) = split /:/, $l;
		$dev  =~ s/\s//g;
		next if defined $iface && $dev ne $iface;
		$rest =~ s/^\s+//;
		my @devarr = split /\s+/, $rest;
		$devs{$dev} = {
			name => $dev,
			data => {
				bytes => {
					in => $devarr[0],
					out => $devarr[8],
				},
				packets => {
					in => $devarr[1],
					out => $devarr[9],
				},
			},
		};
	}
	close($in);

	%{$total} = (kb => {}, packets => {});
	delete $devs{lo};
	foreach my $dev (values %devs) {
		$prevstat{$dev->{name}} //= { name => $dev->{name} };
		update_traffic $dev->{data}, $prevstat{$dev->{name}}, $period;
		$dev->{data}->{kb} = {
			in => $dev->{data}->{bytes}->{in} / 1000000 * 8,
			out => $dev->{data}->{bytes}->{out} / 1000000 * 8,
		};
		add_up_totals $total, $dev->{data};
	}

	return \%devs;
}

MAIN:
{
	$| = 1;

	my $period = 10;
	my %opts;

	getopts 'Cc:hi:Mn:tV', \%opts or usage(1);
	version if $opts{V};
	usage 0 if $opts{h};
	exit 0 if $opts{V} || $opts{h};

	my $addtime = $opts{t};
	if (defined $opts{n}) {
		if ($opts{n} !~ /^([1-9]\d*)$/) {
			warn "The period must be a positive integer\n";
			usage 1;
		}
		$period = $1;
	}
	my $count;
	if (defined $opts{c}) {
		if ($opts{c} !~ /^([1-9]\d*)$/) {
			warn "The count must be a positive integer\n";
			usage 1;
		}
		$count = $1;
	}
	my $iface = $opts{i}; # also works if it isn't defined

	my $use_color;
	if ($opts{C}) {
		if ($opts{M}) {
			die "The -C and -M options are mutually exclusive\n";
		} elsif (!$have_ansi_color) {
			die "The Term::ANSIColor Perl module is not available\n";
		}
		$use_color = 1;
	} elsif ($opts{M}) {
		$use_color = 0;
	} else {
		$use_color = $have_ansi_color && -t \*STDOUT;
	}
	my %c = map { $_ => $use_color? Term::ANSIColor::color($_): '' }
	    qw(yellow reset);

	my $total = {};
	convert $iface, 1, $total;
	sleep 1;

	while(1) {
		my $devs = convert $iface, $period, $total;
		print time.' ' if $addtime;
		if (scalar keys %{$devs} > 1) {
			say $c{yellow}.format_line('total', $total).$c{reset};
		}
		foreach my $dev (sort { $a->{name} cmp $b->{name} } values %{$devs}) {
			say format_line($dev->{name}, $dev->{data});
		}
		if (defined $count) {
			$count--;
			exit 0 if $count < 1;
		}
		sleep $period;
	}
}
