Das Millennium rückt heran, Zeit ein wenig sentimental zu werden.
Mit ``Damals, als ich mit Konrad Zuse im Cafe saß'', pflegte ein
Informatik-Dozent an der Uni immer seine Geschichten einzuleiten.
Heute sage ich mal: Damals, als Computer noch keine Mäuse hatten,
und ich um die Gunst der langbärtigen
Unix-Gurus im Rechnerraum buhlen mußte, um
Zugang zu den heiligen Maschinen zu erlangen, damals, es war wohl
Ende der Achtziger, stieß ich auf Curses
, ein Programm-Paket,
mit dem man unter C einfache graphische Oberflächen auf die Text-Terminals
zaubern konnte: Kleine Textformulare, und auch nette Menüs, aus denen
man Einträge auswählen konnte, indem man mit den Cursortasten der Tastatur
auf- und abfuhr. Nachdem ich auch damals schon keine Apple-Computer
mochte und das X-Window-System und natürlich Microsoft Windows noch
unbekannt waren, begann ich begeistert, einigen meiner C-Programme
eine Oberfläche zu verpassen.
Auch heute noch, wo Window-Systeme kaum noch wegzudenken sind, hat
Curses
seinen Platz: Wo immer eine vollständige Window-Oberfläche
zu speicheraufwendig ist, zu lange zum hochfahren braucht,
oder die graphische Datenübermittlung wegen langsamer Netzverbindung
ätzend lange dauert, leistet Curses
gute Dienste.
Auch für Perl gibt es natürlich
ein Curses
-Paket auf dem CPAN, und darum soll's heute gehen.
Eine ausführliche Behandlung der Curses-Bibliothek (allerdings aus Sicht der C-Schnittstelle) findet sich in [1], einem der ersten O'Reilly-Nutshell-Bücher. 1986 erschienen, ist es fünf Jahre älter als die erste Auflage von ``Programmieren mit Perl'' -- das lila Camel, das kostbarste Devotionalium in meinem Bücherregal.
Curses
spricht Unix-Terminals an, malt Zeichen an bestimmten Stellen
eines Text-Fensters und nimmt Eingaben von der Tastatur entgegen.
Curses
löst sich aber bewußt von den Eigenheiten tausenderlei
verschiedener Unix-Terminaltypen.
Es bietet eine abstrakte Schnittstelle an, die auf allen Maschinen gleich
aussieht, egal was das darunterliegende Terminal treibt. Unter Linux
stützt es sich auf die in /etc/termcap
definierten Strukturen, die
für jeden Terminal-Typ festlegen, welche Kontrollsequenzen (z.B. CTRL-A)
welche Kommandos auslösen (z.B. Screen löschen).
Curses
optimiert Ausgaben an ein Terminal dadurch, dass es intern
mit logischen Screens arbeitet: Zeichenbefehle in Curses
(wie z. B. addstr
) schreiben immer in logische Fenster (z. B.
das Hauptfenster stdscreen
).
Wenn die Applikation dann mit dem refresh
-Befehl das Kommando gibt,
die Änderungen tatsächlich anzuzeigen, zeichnet Curses
nur
die Bereiche des Terminals nach, die sich seit dem letzten refresh
geändert haben. Da es sehr viel Zeit in Anspruch nähme, das Terminal
selbst
nach seinem momentanen Zustand zu befragen, um den Abgleich durchzuführen,
hält sich Curses
ein weiteres logisches Fenster, curscreen
, vor,
in dem es festhält, wie das Terminal seiner Vorstellung nach gerade
aussieht.
Der refresh
-Befehl stellt die Unterschiede zwischen stdscreen
und curscreen
fest, sendet notwendige Änderungen ans Terminal
und zieht diese gleichzeitig in curscreen
nach, damit curscreen
und das tatsächliche Terminal sich in Einklang befinden.
Um mit Curses
loszulegen, muß das Perl-Skript zunächst
die Initialisierung mit initscr
einleiten. endwin
beendet
Curses
und sollte auf jeden Fall vor Abschluß des Skripts
aufgerufen werden. Unterbleibt dies, befindet sich das Terminal
in einem traurigen Zustand und nimmt unter Umständen nicht einmal
mehr Eingaben an. Das einzige was dann im allgemeinen noch bleibt, ist,
unter Umständen blind das Kommando tset
mit einem anschließenden CTRL-J
einzugeben und abzuwarten, bis das Terminal sich wieder beruhigt.
endwin
vermeidet derartigen Unbill. Um ein sauberes Ende auch bei
Programmabbruch mit CTRL-C
oder ähnlichem zu gewährleisten,
finden Signal-Handler Anwendung, die vor dem eigentlichen Ende
noch schnell ein endwin
absetzen.
Curses
arbeitet normalerweise mit dem
Hauptfenster stdscr
, das in der Steinzeit der Datenverarbeitung
den gesamten Bildschirm ausfüllte und im Linux-Zeitalter ein
X-Fenster belegt, oder falls X nicht läuft, eine virtuelle Konsole.
Für ausgefuchstere Oberflächen kann man außerdem Sub-Windows
innerhalb des Hauptfensters definieren.
Curses
Wirbelwind-TourFür die Jüngeren unter Euch, habe ich einen Curses
-Schnellkurs
vorbereitet, bitte anschnallen:
initscr
inititalisiert Curses
und muß
vor irgendwelchen anderen Funktionen abgesetzt werden.
endwin
beendet Curses
.
move($y, $x)
bewegt den Terminal-Cursor auf die angegebene
Position, $y
ist die Zeile, $x
die Spalte.
addstr($string)
schreibt einen String, an der aktuellen
Cursorposition beginnend.
standout
schaltet in einen Modus, in dem alle nachfolgend ausgegebenen
Zeichenketten hervorgehoben dargestellt werden (je nach Terminal
farbig oder revers oder beides). standend
beendet den Modus,
den standout
einleitete.
keypad($flag)
fasst, mit einem wahren Wert für $flag
, die
Sequenzen, die beim Drücken einer Sondertaste beim Terminal
hereinpurzeln, zu einem einzigen Code zusammen.
getch
wartet darauf, dass eine Taste gedrückt wird.
Es blockiert normalerweise solange, bis etwas passiert
und liefert dann den Wert der gedrückten Taste zurück.
nodelay($flag)
veranlaßt getch
dazu, nicht zu blockieren,
falls $flag
auf einen wahren Wert gesetzt wurde.
noecho
unterdrückt das Schreiben der Werte gedrückter Tasten auf dem
Bildschirm. echo
schaltet das Terminal-Echo wieder ein.
Und schließlich der wichtigste Befehl: refresh
,
der die durchgeführten Änderungen auf dem Bildschirm erscheinen
läßt, unterbleibt er, passiert nichts.
Dieses Wissen sollte ausreichen, um ein kleines Monitorprogramm zu schreiben,
das laufend den Zustand der Festplatten eines Rechners anzeigt und
außerdem überprüft, ob eine Reihe ausgewählter Webserver laufen oder
den Geist aufgegeben haben. Das schöne an Curses
ist, dass solche
Utilities auch dann sehr schön zu bedienen sind, wenn man sich über
ein Modem mit telnet
irgendwo am Ende der Welt einloggt.
In Listing ProgressBar.pm
ist ein kleines Hilfsmodul definiert,
das langweilige
Prozentzahlen mit den bescheidenen Mitteln eines Text-Terminals
grafisch darstellt. Wenn man mit ProgressBar->new($maxlen)
ein neues Objekt erstellt, dessen Fortschrittsanzeige
maximal $maxlen
Zeichen
lang wird, liefert die status
-Methode anschliessend zu übermittelten
Prozentzahlen passende Progessanzeigen, aus 71%
wird so flugs
der String
[======= ] (71%)
ProgressBar.pm
zeigt die Implementierung des Konstruktors
new
, der nur den Wert des hereingegebenen Parameters in der
Instanzvariablen maxlen
abspeichert. Die status
-Methode hingegen
nimmt einen Prozentwert entgegen, kalkuliert, wie lange dann der
dargestellte Balken wird und nutzt die sprintf
-Funktion, um einen
mit "=" x $barlen
erzeugten Balken linksbündig in ein
$maxlen
großes Feld einzupassen. Die status
-Methode
erzeugt die hierzu notwendige Formatieranweisung im Format
"%-10s"
dynamisch.
Der eigentliche Monitor steht in Listing monitor.pl
, dessen
Ausgabe Abbildung 1 zeigt. Es zieht das Curses
-Modul, unser
gerade geschriebenes ProgressBar
und schließlich
LWP::Simple
, das später beim Überprüfen von Webseiten
helfen wird.
Nach der Curses
-Initialisierung in den Zeilen 10 bis 14 definiert
Zeile 16 einen Signal-Handler: Die Funktion quit
, die ab
Zeile 119 definiert ist und fatalen Fehlern oder
erzwungenem Programmabbruch schnell die übergebene
Fehlermeldunng ausgibt, Curses
abräumt und das Programm mit
exit
beendet.
Die Zeilen 18 und 19 schreiben die Überschrift, ab Zeile 21 steht
die while
-Schleife, die alle 10 Minuten einen Durchgang
steuert, und solange im Kreis läuft, bis der Benutzer die
Tasten 'q' oder 'BACKSPACE' drückt, um den Monitor zu beenden.
Die Funktion getch()
wartet, wie gesagt,
bis der Anwender eine Taste drückt,
kehrt daraufhin zurück und liefert den Wert der Taste. Ein Programm,
das aber in regelmäßigen Zeitabständen Ausgaben auf dem Bildschirm
aktualisiert und nur für den Fall, dass der Anwender irgendwann
auf eine Taste drückt,
etwas anderes tut (z. B. das Programm beenden), kann nicht ständig
in getch()
herumhängen. Der Aufruf von nodelay(1)
bewirkt, dass
ein nachfolgend aufgerufenes
getch()
nicht blockiert, sondern entweder den Wert einer gedrückten
Taste zurückliefert oder einen Fehler, falls keine Eingabe vorlag.
getch()
liefert eine Zahl ungleich ERR
(ein in Curses
definiertes
Macro) zurück, falls eine Taste
gedrückt wurde. Es wäre aber eine Verschwendung von Rechenpower, aktiv
immer wieder getch()
aufzurufen, bis etwas passiert. Abhilfe schafft
hier select
, der Unix-System-Call, der auf Ereignisse wartet.
Perl bietet ja zwei select
-Funktionen, die sich in der Zahl der
übergebenen Parameter unterscheiden: Während die einparametrige
die Ausgabe der print
-Funktionen auf einen File-Deskriptor umleitet,
ist die zweite, vierparametrige, dazu da, eine Schnittstelle
zum Unix-System-Aufruf select
anzubieten.
Aus Unix-Tradition erwartet der select
-Aufruf ein etwas ungewöhnliches
Parameterformat. select
wartet auf Ereignisse, die in
dreierlei Filedeskriptoren auftreten:
Eine erste Reihe von Filedeskriptoren wird daraufhin
überwacht, ob dort Daten zum Lesen anliegen, eine zweite Reihe schlägt
Alarm, falls es OK ist, dorthin zu schreiben und eine dritte Reihe
wird daraufhin untersucht, ob dort irgendwelche Fehler (Exceptions)
passierten. Außerdem
läßt sich ein Timeout angeben, nachdem die Funktion zurückkehrt,
auch wenn nichts passiert ist.
Die Deskriptorensammlungen werden als Bit-Arrays an select
übergeben. Um
nur den Deskriptor der Standardeingabe zu überwachen, muss zunächst
die Nummer des Filedeskriptors aus den in Perl üblichen File-Handles
ermittelt werden: Die Funktion fileno(STDIN)
liefert die
Filedeskriptornummer
der Standardeingabe zurück (im allgemeinen 0
) und das entsprechende
Bit im Vektor, den select
versteht, setzt die selten verwendete
Perl-Funktion vec
:
$in = ""; vec($in, fileno(STDIN), 1) = 1;
Mit dem in $in
gesetzten Wert wartet nun
select($in, undef, undef, 10);
darauf, dass in der Standardeingabe etwas passiert. Nach den
eingestellten 10 Sekunden Timeout wird abgebrochen. Um auch
Sondertasten zu erkennen, hilft ein
Vorab-Aufruf von keypad(1)
. Während diese ``Funny Keys'' normalerweise
mehrere Werte zurückliefern, fasst Curses
nach keypad(1)
diese
Werte zu einer Konstante zusammen -- die wichtigsten als Macro verfügbaren
sind die Cursortasten KEY_UP
, KEY_DOWN
, KEY_LEFT
, KEY_RIGHT
,
KEY_BACKSPACE
und KEY_ENTER
.
monitor.pl
implementiert diese Logik in den Zeilen 24 bis 27 und
39 bis 41. Die einzelnen Aktionen, die der Monitor ausführt, finden
in den Zeilen 30 bis 36 statt. Zunächst sorgt die Funktion
print_update
dafür, dass rechts unten im Fenster hell erleuchtet
der Text ``Updating ...'' zu lesen ist. print_update
, die ab Zeile
105 definiert ist, nimmt einen Parameter entgegen, der, je nachdem
ob er einen wahren oder falschen Wert aufweist, den Text erscheinen
oder verschwinden läßt. Zeile 111 schaltet in den
Modus, der den Text farbig hervorhebt, Zeile 113 gibt ihn unter
Zuhilfenahme der von Curses
exportierten Konstanten
$COLS
(Anzahl der Spalten des Terminals)
und $LINES
(Anzahl der Zeilen) rechts unten im Fenster aus.
Zeile 114 schaltet wieder zurück in den normalen Textmodus, der
anschließende refresh
-Befehl macht den Text auf dem Bildschirm
sichtbar.
Weiter in Zeile 31: Die Funktion plot_df
ermittelt die Auslastung
einer angegebenen Festplattenpartition und gibt eine grafische
Darstellung an den angegebenen Bildschirmpositionen aus. plot_df
ist ab Zeile 50 definiert, ruft die getdf
-Funktion für
die angegebene Partition auf, verwandelt den gewonnenen Prozentwert
mit dem ProgressBar
-Modul in die Textgrafik und gibt sie zusammen
mit dem Partitionsnamen auf dem Terminal aus.
getdf
steht ab Zeile 82 und
selbst zapft nur das df
-Kommando an, sucht nach der
angegebenen Partition und dem %
-Zeichen und gibt den gefundenen
Zahlenwert zurück.
Ähnlich arbeitet die Funktion plot_webhost
, die ab Zeile 66 definiert ist,
dort den Namen des Rechners
aus dem übergebenen URL extrahiert und zusammen mit dem Ergebnis
der Funktion pingurl
anzeigt. pingurl
ab Zeile 98 nutzt
die get
-Funktion aus LWP::Simple
, um das gewünschte Web-Dokument
zu holen und liefert einen wahren Wert zurück, falls die Seite
erfolgreich eingeholt werden konnte.
Abb.1: Der Monitor in Aktion |
Wer Curses
noch nicht auf dem heimischen Rechner verfügbar hat,
kann es und das außerdem benötigte LWP::Simple
mit der komfortablen
CPAN-Shell folgendermaßen vom CPAN holen und installieren:
perl -MCPAN -eshell cpan> install Curses cpan> install LWP::Simple
Das neue Modul ProgressBar
aus Listing ProgressBar.pm
muß irgendwo
stehen, wo monitor.pl
es auch findet,
am einfachsten im gleichen Verzeichnis, in dem auch monitor.pl
haust.
Die Zeilen 31 bis 35 müssen anschließend noch an die lokalen Gegebenheiten
angepasst werden, die Namen der zu überwachenden Partitionen und die
URLs von Webseiten, die es zu überwachen gilt, müssen hier hinein -- und
wer noch weitere Ideen hat, kann hier seiner Fantasie freien Lauf
lassen. Prozesse, die nicht runterfallen dürfen mit ps
überwachen?
Anzahl der aktiven Benutzer mit who
ausfiltern und anzeigen?
The sky's the limit, wie immer ... a guat's nei's Jahrdausn'd, Leidln!
01 ################################################## 02 # Progress Bar - 1999, mschilli@perlmeister.com 03 ################################################## 04 package ProgressBar; 05 06 ################################################## 07 sub new { 08 ################################################## 09 my ($class, $maxlen) = @_; 10 my $self = { maxlen => $maxlen}; 11 bless $self, $class; 12 return $self; 13 } 14 15 ################################################## 16 sub status { 17 ################################################## 18 my ($self, $percent) = @_; 19 20 my $barlen = $percent/100.0 * $self->{maxlen}; 21 sprintf "[%-$self->{maxlen}s] (%d%%) ", 22 "=" x $barlen, $percent; 23 } 24 25 1;
001 #!/usr/bin/perl -w 002 ################################################## 003 # monitor.pl - 1999, mschilli@perlmeister.com 004 ################################################## 005 006 use Curses; 007 use ProgressBar; # Unser eigenes ProgressBar.pm 008 use LWP::Simple; 009 010 initscr; # Curses initialisieren 011 keypad(1); # Funny keys mappen 012 clear; # Bildschirm löschen 013 noecho(); # Kein Tastenecho 014 nodelay(1); # getch() soll nicht blocken 015 $SIG{__DIE__} = 016 $SIG{INT} = \&quit; # Aktion auf CTRL-C 017 018 move(0,0); 019 addstr("My Cute Little Monitor v1.0"); 020 021 while(1) { 022 023 # Eventuell mehrere Tasten abfragen 024 while ((my $key = getch()) ne ERR) { 025 quit() if ($key eq 'q' || 026 $key eq KEY_BACKSPACE); 027 } 028 029 # Abfragen starten 030 print_update(1); 031 plot_df("/", 2, 0); 032 plot_df("/dos", 3, 0); 033 plot_webhost("http://www.yahoo.com", 4, 0); 034 plot_webhost("http://www.br-online.de", 5, 0); 035 plot_webhost("http://www.amazon.com", 6, 0); 036 print_update(0); 037 038 # 10 Minuten auf Tastendruck warten 039 $bitmap = ''; 040 vec($bitmap, fileno(STDIN), 1) = 1; 041 select($bitmap, undef, undef, 600); 042 } 043 044 endwin; 045 046 ################################################## 047 # Auslastung von Plattenpartition $partition auf 048 # Bildschirmkoordinaten $y/$x anzeigen 049 ################################################## 050 sub plot_df { 051 my ($partition, $y, $x) = @_; 052 053 my $progress = ProgressBar->new(10); 054 055 move($y, $x); 056 addstr($partition); 057 move($y, $x+30); 058 addstr($progress->status(getdf($partition))); 059 refresh; 060 } 061 062 ################################################## 063 # Host $host pingen und auf Terminalkoordinaten 064 # $y/$x anzeigen 065 ################################################## 066 sub plot_webhost { 067 my ($url, $y, $x) = @_; 068 069 my ($dispurl) = ($url =~ m#//([^/]+)#); 070 071 move($y, $x); 072 addstr($dispurl); 073 refresh; 074 move($y, $x+30); 075 addstr(pingurl($url) ? "Up " : "Down"); 076 refresh; 077 } 078 079 ################################################## 080 # Plattenauslasten für Partition $part ermitteln 081 ################################################## 082 sub getdf { 083 my $part = shift; 084 my $percentage; 085 086 open PIPE, "/bin/df -k $part |" or 087 quit("Cannot run /bin/df"); 088 while(<PIPE>) { 089 ($percentage) = /(\d+)%/; 090 } 091 close PIPE or quit("/bin/df failed"); 092 $percentage; 093 } 094 095 ################################################## 096 # Bei $host anklopfen, 0=host down, 1=host up 097 ################################################## 098 sub pingurl { 099 my $url = shift; 100 101 return(defined(get($url))); 102 } 103 104 ################################################## 105 sub print_update { 106 ################################################## 107 my $on = shift; 108 109 $msg = "Updating ..."; 110 111 standout() if $on; 112 $msg = " " x length($msg) unless $on; 113 addstr($LINES-1, $COLS - length($msg), $msg); 114 standend() if $on; 115 refresh(); 116 } 117 118 ################################################## 119 sub quit { 120 ################################################## 121 print "@_\n"; 122 endwin(); 123 exit(0); 124 }
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. |