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. |