Spekulanten-Cockpit (Linux-Magazin, März 2009)

Das ``Perlpanel'' ersetzt die Bodenpanels von Gnome und KDE, ist in Perl geschrieben, und erlaubt auch selbstgeschriebene Perl-Widgets.

Jeder ausgewachsene Desktop, ob Gnome oder KDE, bietet sogenannte Panels an. Sie schmiegen sich entweder oben oder unten an den Desktop an, beherbergen Menüs und Icons zum Starten von Programmen, zeigen laufende Applikationen mit Task-Bars an und schalten zwischen virtuellen Desktops hin und her. Ziel des alternativen Pakets perlpanel ist es, ein plattformunabhängiges Panel bereitzustellen, das der Anwender mit ein paar schnell zusammengeklopften Perlskripts um eigene Widgets erweitern kann. Unter Ubuntu installiert der User das Panel mit apt-get install perlpanel und startet es probeweise mit dem Aufruf perlpanel, einem Perlskript in /usr/bin. Abbildung 1 zeigt die GUI am unteren Rand des Desktops.

Abbildung 1: Das PerlPanel bietet die üblichen Funktionen wie virtuelle Desktops und Task-Bars, erlaubt aber auch selbstgeschriebene Widgets wie einen Aktienticker.

Ein eventuell schon an dieser Stelle liegendes Panel des Window-Managers kann man entweder zur Seite ziehen und es den Window-Manager vertikal am rechten oder linken Rand darstellen lassen, oder gleich ganz löschen, falls man die Brücken abbrennen möchte.

Kurse stets im Blick

Wer gerne mit Aktien spekuliert, möchte vielleicht die Kurse interessanter Werte stets im Blick behalten und keine Applikation, deren Fenster eventuell durch andere verdeckt werden, ist dazu geeignet. Was liegt also näher, als ein Applet für das PerlPanel zu entwickeln, das alle 5 Minuten alle in der Konfigurationsdatei ~/.ticker-rc im Homeverzeichnis abgelegten Aktienwerte von Yahoo Finance abholt und anzeigt? Wie in Abbildung 2 gezeigt, ignoriert das Applet Kommentar- und Leerzeilen und erwartet pro Zeile ein Aktienkürzel. Abbildung 1 zeigt, wie es die vier ausgewählten Kurse im Panel auf den Desktop bringt.

Abbildung 2: Die Kurse der in ~/.ticker-rc eingetragenen Aktien holt das Applet in regelmäßigen Abständen vom Yahoo-Server.

Der eigentliche Applet-Code ist in Listing Ticker.pm zu sehen, das Einholen der Kurse vom Yahoo-Server erledigt das Skript in Listing getquote, das auf der Kommandozeile eine Reihe von Aktienkürzeln entgegennimmt und deren letzte Kurswerte in einem Spaltenformat zeilenweise ausgibt. Das Applet liest einfach die Konfigurationsdatei ein, ruft im 5-Minuten-Rhythmus das Skript getquote auf, und frischt mit den zurückkommenden Werten seine Anzeige im PerlPanel auf. Falls der User mit der Maus auf das Applet klickt, ist er offensichtlich ungeduldig und möchte die neuesten Kurse, also führt das Applet sofort einen weiteren Request an den Yahoo-Server durch und frischt die Anzeige im Panel gleich wieder auf.

Schnapp die Kurse

Die an getquote auf der Kommandozeile übergebenen Aktienkürzel liegen im Skript wie in Perl üblich im Array @ARGV vor. Das Modul Finance::YahooQuote holt mit der Funktion getcustomquote die Kurse vom Yahoo-Server und gibt an, dass es nur an den Spalten "Symbol" (das vorher bei der Anfrage angegebene Aktienkürzel) und "Last Trade (Price Only)" (dem letzten Börsenkurs) interessiert ist. Yahoo liefert sonst mit der Funktion getquote() einen ganzen Rattenschwanz an Daten zurück, den das Applet aber nicht braucht und deswegen von vorneherein ablehnt. Passiert ein Fehler bei der Übertragung, kommt ein einzelner Wert mit einer Fehlermeldung zurück, während Yahoo im Erfolgsfall einen Array zurückliefert, dessen Einträge wiederum Referenzen auf Arrays mit den Einträgen Aktiensymbol und Kurs sind. Zeile 13 prüft deswegen, ob tatsächlich ein zweispaltiger Eintrag zurückkam und bricht andernfalls sofort ab. Zeile 5 setzt noch einen Timeout von 60 Sekunden, der bei Netzwerkstörungen dafür sorgt, dass das Skript nicht ewig herumhängt. Über die Leitung eingetrudelte Kurswerte gibt die anschließene for-Schleife ab Zeile 17 im Format ``Kürzel Kurs'' zeilenweise auf der Standardausgabe aus.

Listing 1: getquote

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Finance::YahooQuote;
    04 
    05 $Finance::YahooQuote::TIMEOUT = 60;
    06 
    07 exit 0 unless @ARGV;
    08 
    09 my @quotes = getcustomquote(
    10     [@ARGV], 
    11     ["Symbol", "Last Trade (Price Only)"]);
    12 
    13 if(! exists $quotes[0][1]) {
    14     die "Fetching quote failed\n";
    15 }
    16 
    17 for my $quote (@quotes) {
    18   print "@$quote\n";
    19 }

Streng nach Vorschrift

Listing Ticker.pm enthält den Applet-Code und muss den Richtlinien des PerlPanels genügen. Dazu gehört ein Modul im Namensraum PerlPanel::Applet::Name, ein Konstruktor new() sowie eine vom Panel anfänglich angesprungene Initial-Funktion configure(). expand() und fill() geben an, wie sich das Widget verändert, wenn der verfügbare Platz im Panel größer wird. widget() muss eine Referenz auf oberste Gtk-Widget des Applets zurückgeben und get_default_config() liefert normalerweise eine Datenstruktur, die das Applet konfiguriert. Da aber im vorliegenden Fall die Konfiguration extern in einer Datei liegt, weil Änderungen seitens des Benutzers keinen Neustart des Applets erfordern sollen, gibt die Funktion hier nur undef zurück.

Damit ist der API des PerlPanels Genüge getan und es fehlt nur noch der eigentliche Applikationscode. In configure() baut das Applet seine GUI auf, die aus einem Label mit den Kursangaben besteht und einem damit verbundenen Button, der Klicks des Benutzers verarbeitet. Zeile 33 definiert dies mit der Methode signal_connect() des Widgets, das dem Event ``clicked'' eine anonyme Subroutine zuordnet, die die Applet-Methode stocks_update() aufruft. Sind alle Widgets definiert, zeichnet show_all() sie in das Panel ein. Zeile 42 ruft das erste Mal stocks_update() auf, bevor das Programm mit add_timeout(). ein alle 5 Minuten (5 * 60 * 1000 Millisekunden) wiederkehrendes Ereignis definiert, das ebenfalls stocks_update() aufruft.

Die Funktion ruft ihrerseits jeweils symbols() auf, die die Datei ~/.ticker-rc ausliest, also kurzfristige Kürzelupdates seitens des Users dort sofort mitbekommt. Kommentarzeilen, Leerzeilen, und alles, was nicht nach Aktienkürzel aussieht, verwirft sie, denn später werden diese Kürzel auf der Kommandozeile an getquotes übergeben und dann sollen keine bösen Überraschungen passieren.

Schnapper im Hintergrund

stocks_update() erledigt dies mit dem Aufruf von open() mit einer Pipe, damit getquote im Hintergrund startet. Nun heißt es Aufpassen: Verbisse sich der Code gleich in die Ausgabe des extern aufgerufenen Prozesses, müsste er eventuell ein paar Sekunden warten, bis die Ergebnisse über das Internet eintreffen, und das ist bei einer GUI, die stets flink auf Usereingaben reagieren muss, keine gute Idee. Stattdessen nutzt stocks_update() die Gtk2::Helper-Funktion add_watch(), die einen Filedeskriptor entgegennimmt (fileno() generiert aus einem Perl-Filehandle einen Deskriptor) und springt bei eintreffenden Daten eine anschließend angegebene Callback-Funktion an. Dies bedeutet, dass des Programm in der Zwischenzeit weiterläuft, stocks_update() beendet und in die Haupteventschleife des Applets springt, wo Usereingaben und andere externe Ereignisse verzugslos abgearbeitet werden.

Schickt get_quote endlich Daten, hat aber noch mehr auf Lager, ist eof(COMMAND) falsch und der else-Zweig ab Zeile 106 schnappt die bisher vorliegenden Ergebnisse auf. Ist get_quote hingegen fertigt, kommt der if-Zweig an die Reihe, das File-Handle wird mit close() abgeräumt und der Watch auf den zugehörigen Deskriptor mit remove_watch() beendet. Zeile 103 zieht das Spaltenformat dann einfach zu einer Zeile im Format ``Kürzel Kurs Kürzel Kurs ...'' zusammen und schickt es mit der Methode set_markup() an das Label-Widget, das den Text auf dem Panel anzeigt.

Abbildung 3: In der Datei /tmp/ticker.log protokolliert das Perl-Applet mit, woran es gerade arbeitet.

Damit der Entwickler weiß, was das Applet treibt, initialisiert Ticker.pm anfangs Log4perl, um die in den Code eingebetteten Log-Statements in die Datei /tmp/ticker.log umzuleiten. In Abbildung 3 sind einige Zeilen daraus zu sehen. Wer auf die zusätzlichen Daten verzichten kann, kommentiert einfach den Aufruf von easy_init() im Code aus.

Listing 2: Ticker.pm

    001 package PerlPanel::Applet::Ticker;
    002 use strict;
    003 use Log::Log4perl qw(:easy);
    004 
    005 my $REFRESH  = 5 * 30_000;
    006 my($CFG_FILE)= glob "~/.ticker-rc";
    007 my $GETQUOTE = "/usr/bin/getquote";
    008 
    009 Log::Log4perl->easy_init({ 
    010     level => $DEBUG,
    011     file  => ">>/tmp/ticker.log",
    012 });
    013 
    014 ###########################################
    015 sub new {
    016 ###########################################
    017   my($package) = @_;
    018 
    019   my $self = {};
    020   bless($self, $package);
    021   return $self;
    022 }
    023 
    024 ###########################################
    025 sub configure {
    026 ###########################################
    027   my($self) = @_;
    028 
    029   $self->{label} = 
    030               Gtk2::Label->new("Ticker");
    031 
    032   $self->{widget} = Gtk2::Button->new();
    033   $self->{widget}->signal_connect(
    034        'clicked', sub { 
    035                $self->stocks_update() });
    036 
    037   $self->{widget}->set_relief('none');
    038   $self->{widget}->add($self->{label});
    039 
    040   $self->{widget}->show_all;
    041 
    042   $self->stocks_update();
    043 
    044   PerlPanel::add_timeout(5 * 60_000, 
    045       sub { $self->stocks_update() ; 
    046             return 1 });
    047 
    048   return 1;
    049 }
    050 
    051 ###########################################
    052 sub symbols {
    053 ###########################################
    054   my @symbols = ();
    055 
    056   if(! open(FILE, "<$CFG_FILE")) {
    057       ERROR "Cannot open $CFG_FILE";
    058       return ();
    059   }
    060 
    061   while(<FILE>) {
    062       s/#.*//g;
    063       s/[^\w\.]//g;
    064       next if /^\s*$/;
    065       chomp;
    066       push @symbols, $_;
    067   }
    068 
    069   return @symbols;
    070 }
    071 
    072 ###########################################
    073 sub stocks_update {
    074 ###########################################
    075   my ($self) = @_;
    076 
    077   my($tag, $buffer);
    078   my $symbols = join " ", symbols();
    079 
    080   DEBUG "Updating '$symbols'";
    081 
    082   if($symbols eq "") {
    083       $self->{label}->set_markup(
    084                    "No symbols defined");
    085       return undef;
    086   }
    087 
    088   if (!open(COMMAND, 
    089             "$GETQUOTE $symbols |")) {
    090       $self->{label}->set_markup(
    091                    "Fetch failed ($!)");
    092       ERROR "Fetch failed ($!)";
    093       return undef;
    094   }
    095 
    096   $tag = Gtk2::Helper->add_watch(
    097     fileno(COMMAND), 'in', 
    098     sub {
    099       if (eof(COMMAND)) {
    100         DEBUG "Received data: $buffer";
    101         close(COMMAND);
    102         Gtk2::Helper->remove_watch($tag);
    103         $buffer =~ s/\n/ /g;
    104         $self->{label}->set_markup(
    105                                   $buffer);
    106       } else {
    107         $buffer .= <COMMAND>;
    108       }
    109     });
    110 
    111   return 1;
    112 }
    113 
    114 ###########################################
    115 sub expand { return 0; }
    116 sub fill { return 0; }
    117 sub widget { return $_[0]->{widget} }
    118 sub get_default_config { return undef; }
    119 
    120 1;

Installation

Unter Ubuntu installiert der Befehl sudo apt-get install perlpanel das Perl-Panel und bringt gleich alle abhängigen Perl-Module mit. Diese stammen aus dem Ubuntu-Package-Repository und deshalb geht dies ganz ohne CPAN-Shell. Das Skript getquote wird in /usr/bin installiert und benötigt das Modul Finance::YahooQuote, das ebenfalls per Ubuntu-Paket (libfinance-yahooquote-perl) erhältlich ist.

Damit das Perlpanel um das neue Ticker-Applet weiß, sind zwei Schritte erforderlich: Erstens wird die Datei Ticker.pm ins Verzeichnis /usr/share/perlpanel/PerlPanel kopiert, wo auch noch weitere Applets, mit denen PerlPanel ausgeliefert wird, liegen. Zweitens wird die Applet-Registry in der Datei /usr/share/perlpanel/applet.registry um die Zeile

    Ticker:Stock Ticker:Utilities

erweitert. Sie gibt an, dass das Panel das Widget ``Ticker'' im Appletverzeichnis unter dem Namen Ticker.pm findet, dass es sich um einen ``Stock Ticker'' handelt und dass dieser im Bereich ``Utiltities'' zu finden ist.

Nun weiß Perlpanel nach einem Neustart um die Existenz des Applets, stellt es aber noch nicht dar. Hierzu muss es der User erst dem Panel hinzufügen, und das geht, indem er auf den ``Actions''-Button (Abbildung 1) des Panels klickt und den Menüpunkt ``Configure'' auswählt. In der der aufpoppenden Dialogbox (Abbildung 4) wird anschließend der Knopf ``+Add'' gedrückt. Zum Vorschein kommt die Auswahlbox nach Abbildung 5, die eine Vielzahl von bereits fertigen Applets anbietet. Unter ihnen befindet sich auch ``Ticker'', das erst ausgewählt und dann mit +Install Applet installiert wird. Anschließend erscheint es wieder in der Box nach Abbildung 4, wo der User es nach oben oder unten verschieben kann, um seine horizontale Lage auf dem Panel zu verändern.

Abbildung 4: Ein Klick auf den "Action"-Knopf des Panels und anschließende Wahl des Menüpunkts "Configure" bringt diesen Dialog zum Vorschein, mit der der User weitere Applets zum Panel hinzufügen kann.

Abbildung 5: Das neugeschriebene Perl-Applet "Ticker" steht zur Auswahl bereit und kann per Mausklick zum Panel hinzugefügt werden.

Damit das Panel automatisch beim Einloggen in eine Session des Window-Managers startet, wird das Programm /usr/bin/perlpanel in den Session-Start-Dialog eingefügt, unter Gnome geht dies wie in Abbildung 6 gezeigt in der Dialogbox, die hochkommt, wenn der User den Eintrag ``System->Preferences->Sessions'' im Hauptmenü anklickt.

Abbildung 6: Mit dieser Konfiguration startet der Gnome Session Manager das Perl Panel jeweils beim Einloggen.

Weitere Informationen zum Bauen eigener Perl-Applets können experimentierfreudige Panelnutzer der Datei perlpanel-applet-howto.pod entnehmen, die zwar nicht dem Ubuntu-Paket, aber dem in CVS eingecheckten Source-Code des Perlpanel-Projektes auf [2] beiliegt. Alle Funktionen des Panels, inklusive der virtuellen Desktopschalttafel, dem Taskbar und den Dialogfenstern zum Hinzufügen neuer Applets und deren Konfiguration sind in Perl geschrieben und geben einen Vorgeschmack darauf, was mit PerlPanel alles möglich ist.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/03/Perl

[2]

PerlPanel-Projekt: http://savannah.nongnu.org/projects/perlpanel

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.