Was der Rechner im eigenen Netz treibt, offenbart ein Aufruf von »netstat«. Mit ein paar Perl-Modulen lässt sich daraus ein Tool entwickeln, das die Daten dynamisch anzeigt, ganz nach dem Vorbild von Top.
Wer wissen will, welche Ports gerade für Netzwerkaufgaben in Verwendung sind, ruft »netstat« auf. Das praktische Linux-Utility kennt mehrere Modi, die der Benutzer über Kommandozeilenoptionen ansteuert. Die Option »-s« produziert beispielsweise eine Statistik des Netzwerkverkehrs (Abbildung 2), »-put« hingegen gibt die Ports aller Applikationen aus, die gerade über TCP kommunizieren (Abbildung 1). Beide Ausgaben sind nützlich, doch eigentlich interessiert auch die zeitliche Abfolge, nicht nur ein Sekundenschnappschuss.

Abbildung 2: Das Kommando »netstat -s« liefert statistische Daten über den von Linux bewältigten Netzwerkverkehr.
Top fürs Netzwerk
Das Vorbild für eine solche dynamische Ausgabe liefert das Utility »top«, das Anzeigen für die Auslastung des Rechners, den Speicherverbrauch und weitere Eckdaten der laufenden Prozesse ständig auffrischt. Aus der statischen Ausgabe von »netstat« eine entsprechende dynamische Terminalapplikation zu erzeugen ist dank CPAN aber nicht weiter schwer.
Das Modul Curses::UI, das im Perl-Snapshot [2] schon einmal für einen Video-Selektor zum Einsatz kam, liefert auch hier das passende Framework. Es sorgt für die dynamischen Ausgaben und ermöglicht es, auf Tastendrücke des Users zu reagieren. Seine Eventschleife lässt sich leicht in den Kernel des Perl-Objekt-Environments (POE) einbinden, das den Ablauf vieler verschiedener Tasks in einem Prozess und einem Thread möglich macht.
Nicht einfrieren!
Hier ergibt sich bei allen GUI-Applikationen, die andere Programme aufrufen, ein Problem: Während die externe Anwendung läuft, reagiert die aufrufende nicht mehr auf Benutzereingaben und Mausklicks. Beim Anwender entsteht das Gefühl, sie sei eingefroren.
Das Kommando »netstat« läuft im Allgemeinen zwar recht zügig durch, mit der Option »-put« löst es jedoch auch IP-Adressen durch einen Reverse-DNS-Aufruf in Hostnamen auf. Bei einem langsamen DNS-Server oder vielen Sockets führt dies zu beachtlichen Verzögerungen. Dies ließe sich durch die Option »-n« verhindern, der Anwender müsste sich dann nur mit IP-Adressen begnügen. Das Beispiel besteht aber auf dem Luxus der Namensauflösung.
Renner-Komponente
Die GUI-Applikation auf der Grundlage von Curses::UI arbeitet mit dem POE-Kernel zusammen, der Tastendrücke und Mausklicks als Events verarbeitet. Folglich liegt es nahe, auch den Aufruf von »netstat« in diese Mechanik einzubinden. POE soll den Prozess starten, aber nicht auf das Ergebnis warten, sondern die Kontrolle sofort wieder an den Kernel zurückgeben, damit dieser weiterhin die Anzeige auffrischen und auf Tastendrücke reagieren kann. Trudelt die Ausgabe von »netstat« dann ein, bekommt der Kernel dies wiederum durch einen Event mit und ruft die Funktion auf, die die Daten filtert und in Variablen zur Weiterverarbeitung ablegt.
Das Modul POE::Wheel::Run ist Bestandteil der POE-Distribution vom CPAN. Es betreut einen externen Prozess und wechselt Zustände eines Automaten, falls sich etwas auf der Standardausgabe des Prozesses tut. Andere Events treten auf, falls sich der Prozess erfolgreich oder wegen eines Fehlers beendet.
Räder im Kernel
Der Betreuer des externen Prozesses wiederum – ein Zustandsautomat, der sich in den POE-Kernel einklinkt – findet in einer POE::Session Platz. Der Kernel sorgt dann dafür, dass der Automat hin und wieder eine Zeitscheibe zugeteilt bekommt, muss aber sicherstellen, dass die anderen eingetragenen Sessions auch irgendwann einmal drankommen.
Anders als im präemptiven Linux-Kernel, der schon mal einem Prozess den Teppich unter den Füßen wegreißt, verlässt sich das POE-Framework darauf, dass sich alle Sessions rücksichtsvoll verhalten. Jede Session muss die Kontrolle sofort an den Kernel zurückgeben, sobald sie nicht mehr mit voller Geschwindigkeit laufen kann.
Bei diesem kooperativen Multitasking ist es wichtig, dass alle Tasks, die auf etwas warten (Festplatte, ein Netzwerkereignis oder die Ausgabe eines externen Prozesses), mitspielen. Ein rücksichtsloser Teil legt das gesamte System lahm. Jede Session verfügt über einen privaten Datenbereich, den Heap. Er ist als Hash implementiert und speichert Key-Value-Paare mit Session-Daten.
Autonome Automaten
Autonome Zustandsautomaten kapselt die POE-Welt gerne in so genannten Components. Diese Klassen bindet eine Applikation dann nur ein und erzeugt ein neues Objekt, hinter dessen Kulissen sich wiederum der Zustandsautomat in den POE-Kernel einklinkt.
Listing 1 (»PoCoRunner.pm«, PoCo ist die gebräuchliche Abkürzung für POE Component) zeigt eine Komponente, die den Namen eines Programms (hier »netstat«) mit Parametern entgegennimmt und ein Wheel erzeugt, das einen externen Prozess mit diesem Programm abfeuert. Danach gibt das Rad die Kontrolle sofort wieder an den Kernel zurück, ohne auf das Ergebnis zu warten.
|
Listing 1: |
|---|
01 ###########################################
02 package PoCoRunner;
03 # Mike Schilli, 2007 (m@perlmeister.com)
04 ###########################################
05 use strict;
06 use warnings;
07 use POE::Wheel::Run;
08 use POE;
09
10 our $PKG = __PACKAGE__;
11
12 ###########################################
13 sub new {
14 ###########################################
15 my($class, %options) = @_;
16
17 my $self = { %options };
18
19 POE::Session->create(
20 package_states => [
21 $PKG => [ qw(_start stdout
22 finish run) ]
23 ],
24 heap => { self => $self },
25 );
26
27 bless $self, $class;
28 }
29
30 ###########################################
31 sub _start {
32 ##########################################
33 my ($kernel, $session) =
34 @_[KERNEL, SESSION];
35 $kernel->post($session, "run");
36 }
37
38 ###########################################
39 sub run {
40 ###########################################
41 my ($kernel, $heap, $session) =
42 @_[KERNEL, HEAP, SESSION];
43
44 my $wheel = POE::Wheel::Run->new(
45 Program => $heap->{self}->{command},
46 ProgramArgs => [$heap->{self}->{args}],
47 StdoutEvent => "stdout",
48 ErrorEvent => "finish",
49 CloseEvent => "finish",
50 );
51
52 $heap->{"wheel"} = $wheel;
53 $heap->{"stdout"} = "";
54 }
55
56 ###########################################
57 sub stdout {
58 ###########################################
59 my ($input, $heap) = @_[ARG0, HEAP];
60
61 $heap->{stdout} .= "$inputn";
62 }
63
64 ###########################################
65 sub finish {
66 ###########################################
67 my ($kernel, $heap) = @_[KERNEL, HEAP];
68
69 ${ $heap->{self}->{data} } =
70 $heap->{stdout};
71
72 $kernel->delay("run",
73 $heap->{self}->{interval});
74 }
75
76 1;
|
Bei jeder Zeile, die in der Standardausgabe des Prozesses auftaucht, springt POE den Zustand »stdout« und damit die Methode »stdout()« von »PoCoRunner.pm« an. Dort sammelt die Session-eigene Heap-Variable »data« (ein Skalar) die Daten als Text ein. Ist »netstat« beendet, wechselt der Automat im Erfolgs- wie im Fehlerfall zur Methode »finish()«. Die kopiert die gesammelten Stdout-Daten in einem Rutsch in die Variable »data«, die der Konstruktor »new()« per Referenz überreicht bekam.
Anschließend ruft »finish()« in Zeile 71 die Methode »delay()« des Kernels auf und veranlasst ihn, nach einer eingestellten Verzögerung die Methode »run()« aufzurufen, die die Heap-Variable »data« zurücksetzt und das Rädchen POE::Wheel::Run erneut mit dem eingestellten externen Programm aufruft.
Bindet also jemand diese Komponente in ein POE-Programm ein und ruft den Konstruktor mit einem Kommando, einer Option, einer Intervalldauer und einer Referenz auf einen Skalar auf, startet die Komponente nicht nur das externe Kommando wieder und wieder, sondern sorgt auch dafür, dass in dem Skalar stets dessen neueste und vollständige Ausgabe liegt.
Den Zustandsautomaten von »PoCoRunner.pm« definiert der Aufruf der Methode »create()« in Zeile 19. Die Zustände sind »_start« (Ausgangszustand), »run« (feuert den Prozess ab), »stdout« (Prozess sendet eine Ladung Daten nach Stdout) und »finish« (Prozess beendet). Diese Zustände bildet POE wegen des Parameters »package_states« in Zeile 20 auf gleichnamige Funktionen innerhalb des Moduls »PoCoRunner.pm« ab.
Parameter POE-Style
Es fällt auf, dass POE die Session-Parameter auf etwas eigenwillige Art und Weise übergibt. Steht in einem Eventhandler zum Beispiel
my($kernel, $heap) = @_[KERNEL, HEAP];
dann überreicht die Session dem Handler eine Reihe von Argumenten im Perl-typischen Array »@_«. Der Handler angelt sich nur zwei davon über die Makros »KERNEL« und »HEAP«. Diese von POE in den Namensraum importierten konstanten Funktionen liefern dann Integerwerte zurück, sodass das Konstrukt ein so genanntes Array-Slice darstellt, das eine Untermenge der Parameter im Array als Liste liefert.
Daten filtern
Wer aber nutzt diese Komponente? Listing 2 (»nettop«) bindet in den Zeilen 12 und 18 gleich zwei Instanzen von »PoCoRunner« ein, eine für »netstat -s« und eine für »netstat -put«. Deren Ausgaben wiederum landen jeweils in den Skalaren »$stats_data« beziehungsweise in »$conns_data«.
|
Listing 2: |
|---|
001 #!/usr/bin/perl -w
002 use strict;
003 use Curses::UI::POE;
004 use List::Util qw(max);
005
006 my ($STATS, $CONNS);
007 my $netstat = "netstat";
008 my $REFRESH_RATE = 1;
009
010 use PoCoRunner;
011
012 PoCoRunner->new(
013 command => $netstat,
014 args => "-s",
015 data => my $stats_data,
016 interval => 1,
017 );
018 PoCoRunner->new(
019 command => $netstat,
020 args => "-put",
021 data => my $conns_data,
022 interval => 1,
023 );
024
025 my $CUI = Curses::UI::POE->new(
026 -color_support => 1,
027 inline_states => {
028 _start => sub {
029 $poe_kernel->delay('wake_up',
030 $REFRESH_RATE)},
031 wake_up => &wake_up_handler,
032 });
033
034 my $WIN = $CUI->add(qw( win_id Window ));
035
036 my $TOP = $WIN->add(qw( top Label
037 -y 0 -width -1 -paddingspaces 1
038 -fg white -bg blue
039 ), -text => top_text());
040
041 my $LBOX = $WIN->add(qw( lb Listbox
042 -padtop 1 -padbottom 1 -border 1 ),
043 );
044
045 my $BOTTOM = $WIN->add(qw( bottom Label
046 -y -1 -width -1 -paddingspaces 1
047 -fg white -bg blue
048 ), -text => "TCP Watcher v1.0"
049 );
050
051 $CUI->set_binding(sub { exit 0; }, "q");
052 $CUI->mainloop;
053
054 ###########################################
055 sub wake_up_handler {
056 ###########################################
057 # Re-enable timer
058 $poe_kernel->delay('wake_up',
059 $REFRESH_RATE);
060 data_refresh();
061 $TOP->text(top_text());
062 $TOP->draw();
063
064 my $state_fmt = col_fmt([map $_->{state},
065 @$CONNS], 8);
066 my $prog_fmt = col_fmt([map $_->{prog},
067 @$CONNS], 20);
068 my $rem_fmt = col_fmt([map $_->{remote},
069 @$CONNS], 32);
070 my $loc_fmt = col_fmt([map $_->{local},
071 @$CONNS], 20);
072
073 my @lines = map {
074 $state_fmt->($_->{state}) . " " .
075 $prog_fmt->($_->{prog}) . " " .
076 $rem_fmt->($_->{remote}) . " " .
077 $loc_fmt->($_->{local}) . " " .
078 "";
079 } sort conn_sort @$CONNS;
080
081 $LBOX->{-values} = [@lines];
082 $LBOX->{-labels} = { map { $_ => $_ }
083 @lines };
084
085 $LBOX->draw(1);
086 }
087
088 ###########################################
089 sub top_text {
090 ###########################################
091 my $ip = $STATS->{Ip};
092 my $tcp = $STATS->{Tcp};
093
094 return sprintf
095 "Packets rcvd:%s sent:%s TCPopen " .
096 "active:%s passive:%s",
097 $ip->{'total packets received'},
098 $ip->{'requests sent out'},
099 $tcp->{'active connections openings'},
100 $tcp->{'passive connection openings'};
101 }
102
103 ###########################################
104 sub data_refresh {
105 ###########################################
106 $STATS = stats_parse($stats_data);
107 $CONNS = conns_parse($conns_data);
108 }
109
110 ###########################################
111 sub stats_parse {
112 ###########################################
113 my($output) = @_;
114
115 my $section;
116 my $data = {};
117 my $key = qr/w[ws]+/;
118
119 for (split /n/, $output) {
120 if( /($key):$/ ) {
121 $section = $1;
122 next;
123 } elsif( /($key): (d+)/ ) {
124 $data->{$section}->{$1} = $2;
125 } elsif( /(d+)s+($key)/ ) {
126 $data->{$section}->{$2} = $1;
127 } else {
128 die "Cannot parse stats line '$_'";
129 }
130 }
131
132 return $data;
133 }
134
135 ###########################################
136 sub conn_sort {
137 ###########################################
138 return -1 if $a->{state} eq "ESTABLISHED";
139 return 1 if $b->{state} eq "ESTABLISHED";
140 return 0;
141 }
142
143 ###########################################
144 sub col_fmt {
145 ###########################################
146 my($cols, $max_space) = @_;
147
148 my $max_len = max map {
149 length $_ } @$cols;
150 $max_len = $max_space if
151 $max_len > $max_space;
152
153 return sub {
154 return sprintf("%${max_len}s",
155 substr(shift, 0, $max_len));
156 };
157 }
158
159 ###########################################
160 sub conns_parse {
161 ###########################################
162 my($output) = @_;
163
164 my $data = [];
165
166 for (split /n/, $output) {
167 my($proto, $rec, $snd, $local, $remote,
168 $state, $prog) = split ' ', $_;
169
170 next if $proto ne "tcp";
171 push @$data,
172 { local => $local,
173 remote => $remote,
174 state => $state,
175 prog => $prog };
176 }
177
178 return $data;
179 }
|
Die Funktion »conns_parse()« in Zeile 160 arbeitet sich durch die Netstat-Ausgabe nach Abbildung 1, extrahiert die wichtigen Kolumnen (lokale IP, Netzwerk-IP, Status, Programm), macht aus dem Tabellenformat einen Array von Arrays und gibt eine Referenz darauf zurück. »stats_parse()« in Zeile 111 dagegen analysiert die Ausgabe von »netstat -s« nach Abbildung 2 und legt die Ausgabe in einem Hash von Hashes ab.
Aus Zwischenüberschriften werden so Einträge im übergeordneten Hash und die Beschriftungen der Einzelwerte (etwa »incoming packets delivered«) wandern als Schlüssel in den untergeordneten Hash. Die darunter gespeicherten Werte entsprechen den in der Netstat-Ausgabe angezeigten Zahlenkolonnen. Insgesamt nutzt »stats_parse()« drei verschiedene reguläre Ausdrücke, um Zwischenüberschriften sowie zwei verschiedene Ausgabeformate von »netstat« zu erfassen.
Etablierte Aufsteiger
Von allen Verbindungen sind jene mit dem Status »Established« die interessantesten, aus diesem Grund sortiert sie die Sort-Routine »conn_sort()« in Zeile 136 ganz nach oben. Wie in Perl üblich, erhält die Sortierfunktion »sort« (Zeile 79) die Vergleichsfunktion als Parameter überreicht. Bei jedem Vergleich im Sortiervorgang ruft »sort« dann »conn_sort()« auf und besetzt die Spezialvariablen »$a« und »$b« mit den Werten der zu sortierenden Einträge.
Liefert »conn_sort()« daraufhin »-1« zurück, ist »$a« kleiner als »$b«, wandert also in der Anzeige nach oben. Kommt aber »+1« zurück, soll »$b« nach oben. Ist keiner der beiden Kandidaten im Status »Established«, liefert »conn_sort« den Wert »0«. Dadurch kommen in diesem Fall beide Kandidaten in der Darstellung irgendwo unterhalb der »ESTABLISHED«-Sektion zu liegen.
Die im GUI tabellenartig angezeigten Werte muss das Skript manchmal zurechtstutzen, damit ein schönes Spaltenlayout entsteht. Die Funktion »col_fmt()« nimmt zwei Parameter entgegen: eine Referenz auf einen Array aller Zeilen einer Tabellenspalte und eine maximal verfügbare Breite »$max_space« für diese Spalte. Mit der Funktion »max()« aus dem CPAN-Modul List::Util bestimmt es anschließend die längste Zeile. Ist diese kürzer als »$max_space«, ist dies die festgesetzte Breite der Spalte. Andernfalls ist »$max_space« maßgebend.
Formatierer als Rückgabe
Der als Codereferenz zurückgegebene Formatierer nimmt wiederum die Zeilen einer Spalte entgegen und stutzt sie mit »substr()« auf die Maximalbreite zurecht. Sind sie zu kurz, füllt er sie per »sprintf()« mit Leerzeichen auf. Jede Spalte erhält so ihren eigenen Formatierer, insgesamt gibt es vier. Die Werte für die maximale Breite ergeben addiert 80, da dies oft die Breite des Textfensters ist. Fällt eine Spalte deutlich kleiner aus als der maximal für sie reservierte Platz, gibt sie Platz an andere Spalten ab. So ist die Platzaufteilung immer optimal.
Die grafische Ausgabe übernimmt wie schon in [2] das Modul Curses::UI::POE. Die Anzeige besteht aus drei Teilen, einer blauen Kopfzeile »$TOP«, die statistische Daten aus »netstat -s« enthält, einer Listbox »$LBOX« mit den bestehenden Netzwerkverbindungen sowie einer ebenfalls blauen Fußzeile »$BOTTOM«, die nur die Version des Programms anzeigt (Abbildung 3).
Der Parameter »paddingspaces« füllt die Kopf- und Fußzeilen rechts auf, damit die blauen Balken sich über die gesamten Bildschirmbreite erstrecken und nicht mit der tatsächlichen Länge des enthaltenen Textes variieren. Die Methode »set_binding()« definiert in Zeile 51, dass die Taste [Q] einen Programmabbruch auslöst, denn sie ruft im Ereignisfall einfach eine Funktion auf, die »exit 0« ausführt.
Der finite Automat der Darstellungsschicht kennt zwei Zustände: den Startzustand »_start« und den Aufwachzustand »wake_up«, in dem er den Bildschirm mit den neuesten Daten auffrischt. Statt »package_states« kommt in »nettop« der Parameter »inline_states« zum Zuge, denn der Konstruktor der POE-Session weist den Zustandsnamen direkt anonyme Subroutinen zu und verweist nicht implizit auf gleichlautende Funktionsnamen im selben Modul. Während »wake_up()« noch läuft, setzt es mit »delay()« schon einen Event an den Kernel ab, den dieser nach der in »$REFRESH_RATE« gespeicherten Sekundenzahl ausführt. So entsteht eine Endlosschleife, die im Sekundentakt das Terminal auf den neuesten Stand bringt.
In »wake_up« holt zunächst der Aufruf von »data_refresh()« die neuesten Daten der »netstat«-Prozesse ab, presst sie in übersichtliche Datenstrukturen und legt diese in globalen Variablen ab. Den dynamisch aufgefrischte Text im Kopfbalken formatiert die Funktion »top_text()« und liefert ihn an die Methode »text()« des Kopfbalkenobjekts. Damit das Ganze auf dem Bildschirm erscheint, ist der Aufruf von »draw()« erforderlich.
Ähnliches gilt auch für die Listbox, deren Einträge haben Werte, die allerdings im vorliegenden Fall gar nicht interessant sind, weil der Benutzer keine Listenelemente auswählt. Der Eintrag »-labels« hingegen definiert, was zu jedem Listenelement im Curses-Fenster anzuzeigen ist, und setzt diese Labels einfach ebenfalls auf die bereits definierten Werte der Listbox-Einträge.
Ruckelfreier Reigen
In Zeile 52 startet die »mainloop« der grafischen Oberfläche den POE-Kernel mit allen Komponenten. Der ruckelfreie Reigen beginnt und jede Sekunde erhält die Anzeige ein Update der Daten. Dies muss nicht unbedingt heißen, dass schon neue »netstat«-Daten vorliegen. Weil aber in einer solchen Umgebung, in der immer nur ein Thread aktiv ist, keine Race-Conditions auftreten, ist sichergestellt, dass in den beiden von den POE-Komponenten gefüllten Skalaren »$stats_data« sowie »$conns_data« in jedem Fall der vollständige Datensatz des letzten erfolgreichen »netstat«-Aufrufs enthalten ist.
Wer mag, erweitert das Skript mit zusätzlichen Tastatureingaben, die den Bildschirm unterteilen und weitere Details zu einem in der Listbox ausgewählten Prozess anzeigen. Und statt »netstat« lassen sich natürlich auch die Ausgaben völlig anderer Utilities in einem »top«-ähnlichen, dynamisch aufgefrischten Fenster darstellen. (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel:[ftp://www.linux-magazin.de/pub/listings/magazin/2008/02/Perl] [2] Michael Schilli, “Ich glotz\’ TV”: [https://www.linux-magazin.de/heft_abo/ausgaben/2006/08/ich_glotz_tv] |
|
Der Autor |
|---|
|
|








