ethstats.p6 6.3 KB
Newer Older
1
#!/usr/bin/env perl6
2 3 4 5
#
# Similarly to the Perl 5 version ethstats.pl, I am hereby
# releasing ethstats.p6 into the public domain.
#   - Peter Pentchev <roam@ringlet.net>
6 7 8 9 10 11 12

use v6;
use strict;

use Getopt::Tiny;
use Terminal::ANSIColor;

13 14
constant VERSION = '1.2.0';

15 16
sub version()
{
17 18 19 20 21 22
	say 'ethstats ' ~ VERSION;
}

sub features()
{
	say 'Features: ethstats=' ~ VERSION;
23 24
}

25
sub usage(Bool:D $err = True)
26 27 28
{
	my $s = q:to/EOUSAGE/;
Usage:	ethstats [-t] [-C | -M] [-c count] [-i iface] [-n period]
29
	ethstats -V | --version | -h | --help
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

	-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 {
		$*ERR.print($s);
		exit 1;
	} else {
		print $s;
	}
}

49 50 51
class Stats {
	has Real $.in = 0;
	has Real $.out = 0;
52

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
	method !set(Real:D $in, Real:D $out)
	{
		$!in = $in;
		$!out = $out;
	}

	method reset()
	{
		self!set(0, 0);
	}

	method update_traffic(Stats:D $old, UInt:D $period)
	{
		my $diff-in = $!in - $old.in;
		$diff-in += 4294967296 if $diff-in < 0;
		my $diff-out = $!out - $old.out;
		$diff-out += 4294967296 if $diff-out < 0;

		$old!set($!in, $!out);
		self!set($diff-in / $period, $diff-out / $period);
	}

	method add(Stats:D $more)
	{
		$!in += $more.in;
		$!out += $more.out;
	}

	method kb_from(Stats:D $bytes)
	{
		$!in = $bytes.in / 1000000 * 8;
		$!out = $bytes.out / 1000000 * 8;
85 86 87
	}
}

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
class InterfaceStats {
	has Str $.name is required;
	has Stats $.bytes = Stats.new;
	has Stats $.packets = Stats.new;
	has Stats $.kb = Stats.new;

	method reset()
	{
		$!bytes.reset();
		$!packets.reset();
		$!kb.reset();
	}

	method update_traffic(InterfaceStats:D $odev, UInt:D $period)
	{
		$!bytes.update_traffic($odev.bytes, $period);
		$!packets.update_traffic($odev.packets, $period);
	}

	method add(InterfaceStats:D $more)
	{
		$!bytes.add($more.bytes);
		$!packets.add($more.packets);
		$!kb.add($more.kb);
	}
	method format()
	{
		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',
		    $!kb.in, $!kb.out,
		    $!packets.in, $!packets.out);
122 123 124
	}
}

125 126
my InterfaceStats %prevstat;

127 128 129 130 131 132 133 134 135 136 137 138 139 140
grammar Interfaces {
	token TOP {
		<headerline>
		<headerline>
		<ifaceline>*
	}

	rule headerline { <nonnl>\n }
	token nonnl { <- [\n] >* }

	rule ifaceline { <iface>':' <in> <out>\n }
	rule in {<bytes> <packets> <errs> <drop> <fifo> <frame> <compressed> <multicast>}
	rule out {<bytes> <packets> <errs> <drop> <fifo> <colls> <carrier> <compressed>}

141
	token iface { <[ \w . / - ]>+ }
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

	token bytes { <uint> }
	token packets { <uint> }
	token errs { <uint> }
	token drop { <uint> }
	token fifo { <uint> }
	token frame { <uint> }
	token compressed { <uint> }
	token multicast { <uint> }
	token colls { <uint> }
	token carrier { <uint> }

	token uint { \d+ }
}

class InterfaceActions {
158
	method TOP($/) { $/.make: $<ifaceline>.map(*.made) }
159 160

	method ifaceline($/) {
161
		$/.make: InterfaceStats.new(
162
			name => $<iface>.made,
163 164 165 166 167 168 169 170 171
			bytes => Stats.new(
				in => $<in>.made<bytes>,
				out => $<out>.made<bytes>,
			),
			packets => Stats.new(
				in => $<in>.made<packets>,
				out => $<out>.made<packets>,
			),
		)
172 173 174 175 176 177 178 179 180 181 182 183
	}
	method in($/) { $/.make: { :bytes($<bytes>.made), :packets($<packets>.made) } }
	method out($/) { $/.make: { :bytes($<bytes>.made), :packets($<packets>.made) } }

	method iface($/) { $/.make: ~$/; }

	method bytes($/) { $/.make: $<uint>.made }
	method packets($/) { $/.make: $<uint>.made }

	method uint($/) { $/.make: $/.UInt }
}

184
sub convert(Str $iface, UInt:D $period, InterfaceStats:D $total)
185
{
186
	try my $f = open :r, '/proc/net/dev', :chomp(False), :enc('latin1');
187
	die "Could not open the interface info pseudo-file: $!" if $!;
188
	my Str $contents = '';
189 190 191
	for $f.lines -> $line {
		# Ah, the joys of reading from pseudo-files...
		last if $line eq '';
192
		$contents ~= $line;
193 194 195
	}
	$f.close;

196 197 198
	my InterfaceStats %devs =
	    Interfaces.parse($contents, :actions(InterfaceActions))
	    .made.map: { $_.name => $_ };
199 200 201 202
	if defined $iface {
		%devs = %devs{$iface}:kv;
	}

203
	$total.reset;
204
	%devs<lo>:delete;
205 206 207 208 209 210
	for %devs.values -> $dev {
		my $name = $dev.name;
		%prevstat{$name} //= InterfaceStats.new(:name($name));
		$dev.update_traffic(%prevstat{$name}, $period);
		$dev.kb.kb_from($dev.bytes);
		$total.add($dev);
211 212 213 214 215
	}
	return %devs;
}

{
216 217
	my Bool %flags;
	my Getopt::Tiny $opts .= new;
218 219 220 221

	$opts.bool('h', Nil, -> $v { %flags<h> = $v });
	$opts.bool('V', Nil, -> $v { %flags<V> = $v });

222 223 224 225
	$opts.str('-', Nil, -> $v {
		given $v {
			when 'help'	{ %flags<h> = True }
			when 'version'	{ %flags<V> = True }
226
			when 'features'	{ %flags<features> = True }
227 228 229 230 231 232 233
			default		{
				note "Invalid long option '$v'";
				usage;
			}
		}
	});

234 235 236 237 238 239
	my Bool $addtime = False;
	$opts.bool('t', Nil, -> $v { $addtime = $v; });

	my UInt $period = 10;
	$opts.int('n', Nil, -> $v {
		if $v < 1 {
240
			note 'The period must be a positive integer';
241
			usage;
242 243 244 245 246 247 248
		}
		$period = $v;
	});

	my UInt $count = 0;
	$opts.int('c', Nil, -> $v {
		if $v < 1 {
249
			note 'The count must be a positive integer';
250
			usage;
251 252 253 254 255 256 257
		}
		$count = $v;
	});

	my Str $iface = Nil;
	$opts.str('i', Nil, -> $v { $iface = $v; });

258
	my Bool $use_color = $*OUT.t;
259 260
	$opts.bool('C', Nil, -> $v {
		if %flags<M> {
261
			note 'The -C and -M options are mutually exclusive';
262
			usage;
263 264 265 266 267 268
		}
		%flags<C> = True;
		$use_color = True;
	});
	$opts.bool('M', Nil, -> $v {
		if %flags<C> {
269
			note 'The -C and -M options are mutually exclusive';
270
			usage;
271 272 273 274 275 276 277 278 279 280
		}
		%flags<M> = True;
		$use_color = False;
	});

	$opts.parse([@*ARGS]);

	if %flags<V> {
		version;
	}
281 282 283
	if %flags<features> {
		features;
	}
284 285 286
	if %flags<h> {
		usage False;
	}
287
	if %flags<V> || %flags<features> || %flags<h> {
288 289 290
		exit 0;
	}

291 292 293
	my Str %c = <yellow reset>.map: { $_ => $use_color?? color($_)!! '' };
	my InterfaceStats $total .= new(:name('total'));
	convert $iface, 1, $total;
294 295
	sleep 1;
	loop {
296
		my InterfaceStats %devs = convert $iface, $period, $total;
297 298
		print time ~ ' ' if $addtime;
		if %devs.elems > 1 {
299
			say %c<yellow> ~ $total.format ~ %c<reset>;
300
		}
301
		.format.say for %devs.values.sort: *.name;
302 303 304 305 306 307 308
		if $count > 0 {
			$count--;
			exit 0 if $count < 1;
		}
		sleep $period;
	}
}