Zurück nach damals (Linux-Magazin, Januar 2000)

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 - what is it good for?

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.

Die Curses Wirbelwind-Tour

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

Der billige Monitor

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.

Warten auf Ereignisse

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

Installation

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!

Listing ProgressBar.pm

    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;

Listing monitor.pl

    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 }

Referenzen

[1]
Programming with Curses, John Strang, O'Reilly, 1986

[2]
Tom Christiansen and Nathan Torkington, The Perl Cookbook, S.532, O'Reilly, 1998

Michael Schilli

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