Das Kommando netstat
zeigt Aktivität des lokalen Rechners
im Netzwerk an und hat schon
manchem Admin den Tag gerettet. Mit ein paar Perl-Modulen vom CPAN lässt
sich daraus ein dynamisches Terminal-Tool bauen, das netstat
-Daten
ähnlich komfortabel wie die Unix-Utility top
anzeigt.
Wer wissen will, welche Ports auf einer Maschine gerade mit
Netzwerkaufgaben beschäftigt sind, ruft netstat
auf. Diese praktische
Linux-Utility operiert in mehreren Modi, die der Benutzer durch
Kommandozeilenoptionen einstellt.
Mit -s
aufgerufen gibt
das Tool
eine Statistik des abgewickelten Netzwerkverkehrs aus (Abbildung 2).
Mit -put
hingegen druckt es die Ports aller Applikationen, die über das
TCP-Protokoll mit dem Netzwerk kommunizieren (Abbildung 1).
Beide Ausgaben sind nützlich,
und eigentlich interessiert auch die zeitliche Abfolge der Geschehnisse
und nicht nur ein Sekundenschnappschuss.
Um Prozesse zu beobachten, liegt Linux die Utilty top
bei, die
die Belastung des Rechners, sowie den Speicherverbrauch, die CPU-Auslastung
und viele weitere Eckdaten der laufenden Prozesse anzeigt und dynamisch
auffrischt. Aus der statischen Ausgabe von netstat
eine dynamisch laufende
Terminalapplikation a la top
zu erzeugen, ist dank CPAN auch nicht
weiter schwer.
Abbildung 1: Die Ausgabe des Kommandos netstat -put zeigt eine Liste aktiver TCP-Ports. |
Das Modul Curses::UI, das im Perl-Snapshot in [2] schon einmal für einen Video-Selektor zum Einsatz kam, liefert auch heute das notwendige Framework, um Daten dynamisch im Terminal aufzufrischen und 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.
Allerdings stellt sich wie bei allen GUI-Applikationen, die externe Programme aufrufen, die Frage nach dem Einfrieren der Applikation. Ist ein Prozess nämlich mit anderen Dingen beschäftigt, bekommt er Benutzereingaben und Mausklicks nicht mehr mit und hängt unbeweglich im Window-Manager herum. Das Gefühl der 'eingefrorenen' Applikation macht sich beim Anwender breit.
Das Kommando netstat
läuft zwar im allgemeinen recht zügig durch,
mit der Option -put
löst es allerdings die einem Socket zugehörigen
IP-Adressen mittels eines Reverse-DNS-Aufrufs in Hostnamen auf.
Bei einem langsamen DNS-Server oder recht vielen
dargestellten Sockets kann dies jedoch zu beachtlichen Verzögerungen
führen, manchmal dauert es einige Sekunden, bis netstat
endlich
fertig ist. Dies könnte man durch die Option -n
verhindern und sich
mit den IP-Adressen statt der aufgelösten Hostnamen begnügen, aber wer
den vollen Luxus möchte, bindet den Aufruf von netstat
einfach in
den POE-Reigen des GUI-Moduls ein.
Abbildung 2: Das Kommando netstat -s liefert statistische Daten über den von Linux bewältigten Netzwerkverkehr. |
Die GUI-Applikation in Curses::UI tickt im Zusammenspiel mit dem POE-Kernel. Tastendrücke und Mausklicks verarbeitet dieser als Events, gleichberechtigt mit Events von anderen POE-Sessions.
Folglich liegt es nahe, auch den Aufruf von netstat
darin
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 endlich ein,
bekommt der Kernel dies wiederum als Event mit und ruft die Funktion
auf, die die Daten filtert und in Variablen zur Weiterverarbeitung auf
der Displayebene ablegt.
Das Modul POE::Wheel::Run ist Bestandteil der POE-Distribution vom CPAN. Es betreut einen externen Prozess und springt Zustände eines Automaten an, falls sich etwas auf der Standardausgabe des Prozesses tut. Andere Events treten auf, falls sich der Prozess erfolgreich oder auch wegen eines Fehlers beendet.
Das den Prozess betreuende ``Rädchen'' wiederum findet in einer POE::Session Platz, einem Zustandsautomaten, der sich in den POE-Kernel einklinkt. Der Kernel sorgt dann dafür, dass der Automat hin und wieder eine Zeitscheibe zugeteilt bekommt, muss aber sicherstellen, dass alle eingetragenen Sessions irgendwann einmal drankommen. Anders als im preemptiven Linux-Kernel, der schon mal einem Prozess den Teppich unter den Füßen wegreißt, verlässt sich das POE-Framework darauf, dass alle Sessions sich rücksichtsvoll verhalten.
Jede Session muss die Kontrolle sofort wieder 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 angefallener Session-Daten.
Abbildung 3: Der in Perl geschriebene Monitor zeigt laufend die aktuellen TCP-Verbindungen an. |
Autonome Zustandsautomaten kapselt man in der POE-Welt gerne in
sogenannten ``Components''. Diese Klassen bindet eine Applikation dann nur
ein, erzeugt ein neues Objekt und hinter den Kulissen klinkt sich eine
eigenständige Applikation in den POE-Kernel ein. Listing
PoCoRunner.pm
(PoCo ist die gängige Abkürzung für POE-Component)
zeigt eine Komponente, die den Namen eines Programms (z.B. netstat
)
mit Parametern entgegennimmt und ein ``Wheel'' erzeugt, das einen
externen Prozess mit dem angegebenen Programm abfeuert.
Danach gibt das Rad die Kontrolle sofort wieder an den Kernel zurück, ohne
auf das Ergebnis zu warten.
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, springt der Automat im Erfolgs- wie im Fehlerfall
die Methode finish
an. Dieser kopiert die gesammelten STDOUT-Daten
in einem Rutsch an die per Referenz an den Konstruktor new
hereingereichte Variable data
.
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 Heapvariable data
zurücksetzt
und das Rädchen POE::Wheel::Run erneut mit dem eingestellten
externen Program netstat
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, ruft die Komponente nicht nur das externe Kommando wieder und wieder auf, sondern sorgt auch dafür, dass in dem Skalar stets dessen neueste und komplette Ausgabe liegt.
Den Zustandsautomaten von PoCoRunner.pm
definiert der Aufruf der
Methode create()
in Zeile 18. 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 19 auf die gleichnamigen Funktionen innerhalb des Moduls PoCoRunner.pm
ab.
Es fällt auf, dass POE Session-Parameter auf eigenwillige Art und Weise übergibt. Steht in einem Event-Handler zum Beispiel
my($kernel, $heap) = @_[KERNEL, HEAP];
überreicht die Session dem Handler eine Reihe von Argumenten
im Perl-typischen Array @_
. Der Handler angelt sich nur
zwei davon über die Macros KERNEL und HEAP. Diese von POE
in den Namensraum importierten konstanten Funktionen liefern
Integerwerte zurück, sodass das Konstrukt oben ein sogenanntes
Array-Slice darstellt, das eine Untermenge der im Array liegenden
Parameter als Liste zurückliefert.
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} .= "$input\n"; 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;
Wer nutzt nun diese Komponente? Listing nettop
bindet in den
Zeilen 12 und 18 gleich zwei
Instanzen von PoCoRunner
ein, einen für netstat -s
und einen weiteren für
netstat -put
. Die Ausgaben landen in den Skalaren $stats_data
und $conns_data
.
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
hingegen analysiert die Ausgabe von netstat -s
nach Abbildung 2 und legt die Ausgabe in einem Hash von Hashes ab. Aus
Zwischenüberschriften (z.B. ``Ip:'') werden so Einträge im übergeordneten
Hash und die Beschriftungen der Einzelwerte (z.B. ``incoming packets deliverd'')
wandern als Schlüssel in den untergeordneten Hash. Die darunter
gespeicherten Werte entsprechen den in 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.
Von allen Verbindungen sind diejenigen mit einem Status von ``ESTABLISHED''
oft die interessantesten, deswegen sortiert sie die Sort-Routine
conn_sort()
in Zeile 136 ganz nach oben. Wie in Perl üblich, erhält
die in Zeile 79 aufgerufene Sortierfunktion sort
die Sort-Routine
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
dann -1 zurück, ist $a
``kleiner'' als $b
,
wandert also nach
oben in die Anzeige. Kommt +1 zurück, soll hingegen $b
nach oben.
Ist keiner der beiden Kandidaten im Status ``ESTABLISHED'', liefert
conn_sort
den Wert 0 zurück. Somit kommen in diesem Fall beide Kandidaten
in der Darstellung irgendwo unterhalb der ESTABLISHED
-Sektion zu liegen.
Die in der 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. Der als Code-Referenz
zurückgegebene Formatierer nimmt anschließend wiederum
die Zeilen einer Spalte entgegen und
stutzt stutzt sie mit substr()
auf die Maximalbreite zurecht.
Sind sie zu kurz, füllt er sie mit sprintf()
notfalls mit Leerzeichen auf.
Jede Spalte erhält so ihren eigenen Formatierer, insgesamt tun
vier von ihnen ihren Dienst. Die Werte für die maximale Breite ergeben
aufaddiert den Wert 80, da dies oft die Breite des Textfensters ist.
Ist eine Spalte deutlich kleiner als der maximal für sie reservierte
Platz, gibt sie diesen für andere Spalten auf. So kann es zwar sein, dass
die Anzeige bei variierendem Netzwerkverkehr manchmal etwas hin- und
herschnackelt, aber die Platzaufteilung ist immer optimal.
Die graphische Ausgabe übernimmt wie schon in [2] das Modul
Curses::UI::POE. Die Anzeige besteht aus drei Teilen, einer blauen
Kopfzeile $TOP mit statistischen Daten aus netstat -s
,
einer Listbox $LBOX mit bestehenden Netzwerkverbindungen (notfalls
versteckt sie überschüssige Einträge) und einer ebenfalls
blauen Fußzeile $BOTTOM, die nur die Version des Programmes anzeigt.
Der Parameter paddingspaces
füllt die blauen 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 der Automat 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 implizt auf gleichlautende Funktionsnamen im
gleichen 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
Anzahl von Sekunden ausführt.
So entsteht eine Endlosschleife, die unermüdlich im Sekundentakt
das Terminal auf den neuesten Stand bringt.
In wake_up
holt zunächst der Aufruf von data_refresh()
die
neuesten Daten der über PoCoRunner.pm laufend aufgerufenen
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 auch auf dem Bildschirm erscheint,
ist anschließend der Aufruf der Methode draw()
erforderlich.
Ähnliches gilt für die Listbox, deren Einträge haben Werte
(-values
), die jedoch im vorliegenden Fall nicht interessieren,
da der Benutzer keine Listenelemente auswählt. Der Eintrag -labels
hingegen definiert, was zu jedem Listenelement im Curses-Fenster
angezeigt wird und setzt diese 'Labels' einfach ebenfalls auf die
bereits definierten Werte der Listboxeinträge.
Zeile 52 startet die mainloop
der graphischen Oberfläche und
damit den POE-Kernel mit allen bis dato eingebunden Komponenten.
Der ruckelfreie Reigen beginnt, und jede Sekunde (eingestellt in
$REFRESH_RATE) wird die Anzeige mit den neuesten verfügbaren Daten
aufgefrischt. Dies muss nicht unbedingt heißen, dass schon neue
netstat
-Daten vorliegen. Aber da in einer 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
und $conns_data
immer der vollständige
Datensatz des letzten erfolgreichen netstat
-Aufrufs liegt.
Erweitern könnte man das Skript mit zusätzlichen Tastatureingaben,
die den Bildschirm unterteilen und weitere Details zu einem in der
Listbox ausgewählten Prozess anzeigen, als dies in der beschränkten
Platz, den ein einzeiliger Eintrag bietet, möglich wäre. Und statt
netstat
lassen sich natürlich
auch die Ausgaben völlig anderer Utilities in einem
top
-ähnlichen dynamisch aufgefrischten Fenster darstellen.
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 chld => sub { waitpid $_[ARG1], 0; }, 033 }); 034 035 my $WIN = $CUI->add(qw( win_id Window )); 036 037 my $TOP = $WIN->add(qw( top Label 038 -y 0 -width -1 -paddingspaces 1 039 -fg white -bg blue 040 ), -text => top_text()); 041 042 my $LBOX = $WIN->add(qw( lb Listbox 043 -padtop 1 -padbottom 1 -border 1 ), 044 ); 045 046 my $BOTTOM = $WIN->add(qw( bottom Label 047 -y -1 -width -1 -paddingspaces 1 048 -fg white -bg blue 049 ), -text => "TCP Watcher v1.0" 050 ); 051 052 $CUI->set_binding(sub { exit 0; }, "q"); 053 $poe_kernel->sig("CHLD", "chld"); 054 $CUI->mainloop; 055 056 ########################################### 057 sub wake_up_handler { 058 ########################################### 059 # Re-enable timer 060 $poe_kernel->delay('wake_up', 061 $REFRESH_RATE); 062 data_refresh(); 063 $TOP->text(top_text()); 064 $TOP->draw(); 065 066 my $state_fmt = col_fmt([map $_->{state}, 067 @$CONNS], 8); 068 my $prog_fmt = col_fmt([map $_->{prog}, 069 @$CONNS], 20); 070 my $rem_fmt = col_fmt([map $_->{remote}, 071 @$CONNS], 32); 072 my $loc_fmt = col_fmt([map $_->{local}, 073 @$CONNS], 20); 074 075 my @lines = map { 076 $state_fmt->($_->{state}) . " " . 077 $prog_fmt->($_->{prog}) . " " . 078 $rem_fmt->($_->{remote}) . " " . 079 $loc_fmt->($_->{local}) . " " . 080 ""; 081 } sort conn_sort @$CONNS; 082 083 $LBOX->{-values} = [@lines]; 084 $LBOX->{-labels} = { map { $_ => $_ } 085 @lines }; 086 087 $LBOX->draw(1); 088 } 089 090 ########################################### 091 sub top_text { 092 ########################################### 093 my $ip = $STATS->{Ip}; 094 my $tcp = $STATS->{Tcp}; 095 096 return sprintf 097 "Packets rcvd:%s sent:%s TCPopen " . 098 "active:%s passive:%s", 099 $ip->{'total packets received'}, 100 $ip->{'requests sent out'}, 101 $tcp->{'active connections openings'}, 102 $tcp->{'passive connection openings'}; 103 } 104 105 ########################################### 106 sub data_refresh { 107 ########################################### 108 $STATS = stats_parse($stats_data); 109 $CONNS = conns_parse($conns_data); 110 } 111 112 ########################################### 113 sub stats_parse { 114 ########################################### 115 my($output) = @_; 116 117 my $section; 118 my $data = {}; 119 my $key = qr/\w[\w\s]+/; 120 121 for (split /\n/, $output) { 122 if( /($key):$/ ) { 123 $section = $1; 124 next; 125 } elsif( /($key): (\d+)/ ) { 126 $data->{$section}->{$1} = $2; 127 } elsif( /(\d+)\s+($key)/ ) { 128 $data->{$section}->{$2} = $1; 129 } else { 130 die "Cannot parse stats line '$_'"; 131 } 132 } 133 134 return $data; 135 } 136 137 ########################################### 138 sub conn_sort { 139 ########################################### 140 return -1 if $a->{state} eq "ESTABLISHED"; 141 return 1 if $b->{state} eq "ESTABLISHED"; 142 return 0; 143 } 144 145 ########################################### 146 sub col_fmt { 147 ########################################### 148 my($cols, $max_space) = @_; 149 150 my $max_len = max map { 151 length $_ } @$cols; 152 $max_len = $max_space if 153 $max_len > $max_space; 154 155 return sub { 156 return sprintf("%${max_len}s", 157 substr(shift, 0, $max_len)); 158 }; 159 } 160 161 ########################################### 162 sub conns_parse { 163 ########################################### 164 my($output) = @_; 165 166 my $data = []; 167 168 for (split /\n/, $output) { 169 my($proto, $rec, $snd, $local, $remote, 170 $state, $prog) = split ' ', $_; 171 172 next if $proto ne "tcp"; 173 push @$data, 174 { local => $local, 175 remote => $remote, 176 state => $state, 177 prog => $prog }; 178 } 179 180 return $data; 181 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |