Linux-Magazin-Logo Die Zeitschrift für Linux-Professionals

Kooperatives Multitasking in Eigenregie

Kursmakler

Michael Schilli

Das Perl Object Environment versetzt ein Skript in die Lage, intern kooperatives Multitasking ohne Zutun des Betriebssystem-Schedulers zu betreiben. Das Beispiel für eine POE-Anwendung ist ein simpler, aber ruckfrei operierender Aktienticker. Der grafischen Oberfläche verleiht GTK ein Gesicht.

Applikationen mit grafischer Oberfläche arbeiten typischerweise Event-basiert: In einer Hauptschleife wartet das Programm auf Ereignisse wie Mausklicks und Tastatureingaben. Es ist wichtig, dass das Programm diese Events verzögerungsfrei abarbeitet und sofort wieder in die Haupt-Eventschleife eintritt, damit die Oberfläche nur unmerklich kurz nicht benutzbar ist.

Das hier vorgestellte Aktienticker-Programm kontaktiert periodisch die Finanz-Webseite von Yahoo, um die neuesten Börsenkurse einzuholen, Abbildung 1 zeigt es. Ein Request dauert, je nach Netzanbindung, inklusive der DNS-Auflösung des Servernamens schon mal ein paar Sekunden. Sehr gut wäre, wenn während dieser Zeit die Oberfläche der Applikation weiterackern kann.

Abbildung 1: Der GTK-basierte Aktienticker kontaktiert periodisch die Finance-Site von Yahoo.

Das Ziel könnte der Entwickler mit Multiprocessing oder Multithreading erreichen. Beides erhöht allerdings die Komplexität eines Programms beträchtlich: Kritische Sektionen wären vor Parallelzugriffen zu schützen, um die Integrität der Daten zu gewährleisten, und schwer zu analysierende Fehler schleichen sich ein. Wer mal einen Core Dump mit 200 laufenden Threads untersuchen musste, der weiß, wovon die Rede ist.

Eine alternative Möglichkeit, die ohne beide Nachteile auskommt, ist kooperatives Multitasking mit POE, dem Perl Object Environment[2] um Hauptentwickler Honocho Rocco Caputo. In der als State Machine implementierten Umgebung läuft zu jedem Zeitpunkt genau ein Prozess mit nur einem Thread, aber ein auf Benutzerebene realisierter "Kernel" sorgt dafür, dass mehrere Aufgaben quasi zeitgleich abgearbeitet werden.

Kurs halten

Fürs Einholen von Aktienpreisen nimmt man in Perl üblicherweise das CPAN-Modul Yahoo::FinanceQuote:

use Finance::YahooQuote;
my @quote = getonequote($symbol);

Das Modul arbeitet jedoch synchron, was dem Wunsch nach ruckfreier Funktionsweise abträglich ist. Die Funktion »getonequote« setzt einen HTTP-Request an den Yahoo-Server ab, wartet auf die Antwort und kehrt erst dann mit einem Ergebnis zurück.

Während der Wartezeit sollte allerdings die Oberfläche besser in Schuss gehalten werden - wie's der Teufel will, zieht womöglich gerade jemand ein anderes Fenster über den Ticker. Das Fenster muss den eben verdeckten Teil seines Zuständigkeitsbereichs neu zeichnen (der so genannte Refresh). Der laufende Thread gönnt sich aber eine Phase der Untätigkeit, was ein gräuliches Loch auf dem Desktop erzeugt - so nicht.

Asynchron glücklich

Es wäre geschickter, einen Web-Request auszuschicken und, ohne auf das Ergebnis zu warten, die Aufmerksamkeit gleich wieder der grafische Oberfläche zu schenken. Ist irgendwann die Antwort des Yahoo-Servers eingetroffen, sollte das eine Art Alarm auslösen. Also schnell das Fenster des Aktientickers aktualisieren - und wieder zurück in die GUI-Hauptschleife.

Genau dies leistet das POE-Framework. Es besteht aus einem Kern, in dem einzelne Applikationen Sessions registrieren. Dort springen State-Maschinen von Zustand zu Zustand und schicken einander Nachrichten. I/O-Aktivitäten erfolgen asynchron: Statt blockierend über ein File-Handle Daten einzulesen, sagt man: Hallo Kernel, ich will die Datei einlesen, wecke mich, wenn die Daten verfügbar sind.

Mit Volldampf auf der Daten-Überholspur

Zwar findet kein echtes asynchrones Schreiben oder Lesen statt (POE nutzt unter der Haube lediglich nicht-blockierende Syswrite- beziehungsweise Sysread-Funktionen), aber die bereitstehenden Daten werden mit Volldampf rausgepustet oder eingesogen. Der kooperative Aspekt bei POE ist, dass die Sessions sich darauf verlassen, dass keine ihrer Mitbewerberinnen rumtrödelt. Wenn eine Aufgabe den Prozessor nicht voll auslastet, muss die Session die Kontrolle freiwillig an den Kernel zurückgeben. Eine einzige unkooperative Stelle im Programm zöge das ganze System in Mitleidenschaft.

Das Multitasking mit einem einzigen Thread erleichtert die Programmentwicklung erheblich - kein Lock muss her, keine Überraschungen mit Race Conditions passieren und wenn doch mal ein Fehler auftritt, ist er meist leicht ausgemacht. POE arbeitet auch schön mit den Haupt-Eventschleifen einiger grafischer Umgebungen zusammen: Perl/Tk und Gtkperl erkennt POE automatisch und bindet sie nahtlos in den kooperativen Reigen ein. So sorgt der Kernel dafür, dass Ereignisse aus dem GUI ebenso ihre Zeitscheibchen bekommen wie die anderer explizit definierter Sessions. Damit ist auch das Refresh-Problem gelöst: Das GUI-Toolkit kann sich rechtzeitig darum kümmern.

Alarmierender Kernel

Der Aktienticker benötigt den Zustandsautomaten POE::Session nach Abbildung 2. Die Initialisierung »_start« baut unter anderem die GTK-Oberfläche auf und setzt den Aliasnamen »ticker«, damit die Session später leicht identifizierbar ist. Von dem Ausgangszustand geht die Kontrolle an den Kernel über. Alle 60 Sekunden (per Alarm) oder sobald jemand den »Update«-Knopf der Oberfläche drückt, kommt der »wake_ up«-Zustand an die Reihe. Er stößt einen weiteren Zustandsautomaten vom Typ POE::Component::Client::HTTP an und gibt dann sofort die Kontrolle an den Kernel zurück.

Abbildung 2: Der Zustandsautomat von Gtkticker: Nach dem Initialisierungszustand »_start« geht die Kontrolle an den Kernel. Alle 60 Sekunden kommt der »wake_up«-Zustand an die Reihe, der einen weiteren Zustandsautomaten anstößt und die Kontrolle an den Kernel zurückgibt.

»PoCoCli::HTTP« ist eine so genannte Komponente (Component) aus dem POE-Framework: ein Zustandsautomat, der seine eigene Session (im Listing 2, Zeile 61, »useragent« genannt) definiert, im »request«-Zustand Webanfragen entgegennimmt und dann im POE-Framework mitspielt, bis er eine HTTP-Antwort vollständig erhalten hat. Dann teilt er dem Kernel mit, dass die »ticker«-Session, die ihn aufgerufen hat, einen ihm vorher mitgeteilten Zustand (»yhoo_response«) anspringen soll.

Veranlasst der Kernel die »ticker«-Session dazu, nimmt sie die bereitliegende HTTP-Antwort entgegen, frischt die Aktien-Widgets in der Anzeige auf und gibt die Kontrolle umgehend an den Kernel zurück. Ab Zeile 59 startet die Komponente »POE::Component::Client::HTTP« mit »spawn()« und legt fest, dass im Server-»UserAgent«-String »gtkticker/0.01« auftaucht und dass hängende Anfragen nach 60 Sekunden abgebrochen werden.

Yahoo von Hand

Im Listing 2 definiert Zeile 9 die URL von Yahoos Aktienkursservice. Dessen CGI-Schnittstelle nimmt zwei Parameter entgegen:

Der Yahoo-Server antwortet darauf in der Art:

"YHOO",45.38,+0.35
"MSFT",27.56,+0.19
"TWX",18.21,+0.75

Gtkticker nimmt die Antwort einfach zeilenweise und an den Kommata auseinander und schleust alles auf die grafische Oberfläche.

Konfiguration im Heimatverzeichnis

Zeile 11 benennt mit ».gtkicker« im Heimatverzeichnis des Benutzers die Datei mit den Symbolen, die der Ticker anzeigen soll. Die Zeilen 25 bis 31 lesen die Datei zeilenweise ein und verwerfen mit »#« beginnende Kommentarzeilen (Zeile 28). Die implizite For-Schleife am Ende der Zeile 29

... for /(\S+)/g;

führt den Ausdruck links von ihr für alle Wörter einer Zeile aus und setzt jeweils das Börsensymbol in die Variable »$_«. Der Push-Aufruf stapelt die Werte aus »$_« im Array »@SYMBOLS« - so dürfen auch mehrere Symbole durch Leerzeichen getrennt in einer Zeile stehen. Listing 1 zeigt zeigt eine Beispieldatei.

Listing 1: Konfigurationsdatei

01 # ~/.gtkticker
02 TWX
03 MSFT
04 YHOO AMZN RHAT
05 DODGX
06 JNJ COKE IBM SUN

Trotz POE-Frameworks verwendet Gtkticker beim Lesen der Konfigurationsdatei die regulären synchronen I/O-Funktionen, denn dieses File ist kurz und der POE-Kernel läuft noch gar nicht.

Der Tanz beginnt

Zeile 33 definiert den Zustandsautomaten des Tickers. Der Parameter »inline_ states« weist mit einer Hashreferenz den Zuständen Funktionen zu, die der Kernel anspringt - falls die Zustände erreicht sind. Dann schiebt Zeile 44 mit

$poe_kernel->post("ticker", "wake_up");

über die von »POE« exportierte Variable »$poe_kernel« den Zustand »wake_up« für die »ticker«-Session in den Kernel. Zeile 45 startet mit

$poe_kernel->run();

die Kernel-Hauptschleife, die das Programm bis zum Shutdown nie mehr verlässt. Das war's!

Die zuvor gezeigte Konstruktion des »POE::Session«-Objekts hat einen Seiteneffekt: Die dem »_start«-Zustand zugewiesene und ab Zeile 48 definierte Routine »start()« wurde ausgeführt. Sie setzt den Aliasnamen der Session auf »ticker« und springt sodann in »my_ gtk_init()«. Diese ab Zeile 82 definierte Funktion baut die GTK-Oberfläche zusammen.

GUIs mit GTK

»Gtk« ist ein CPAN-Modul von Marc Lehmann, der auch freundlicherweise das Manuskript für diesen Artikel durchsah. Eigentlich hat »Gtk2« jenes Modul schon abgelöst, doch die neue Version spielt mit POE noch nicht so recht zusammen. Das ehrwürdige GTK erledigte den Job dagegen hervorragend.

Ein Objekt der Klasse »Gtk::Window« ist das Hauptfenster der Applikation. Oben hängt ein typisches Pull-down-Menü, das aus einem Menübalken mit einem Eintrag »File« besteht. Dessen ausziehbares Menü enthält nur den Eintrag »Quit«, der die Applikation über eine Callback-Routine mit »Gtk->exit(0)« beendet. Dafür, dass der Benutzer auch mit der Tastenkombination [Ctrl]+[Q] das Programm verlassen kann, sorgt ein »Gtk::AccelGroup«-Objekt, das die Menüsteuerung mit so genannten Accelerators bestückt.

Menüs aus der Fabrik

Das Menü wird mit Hilfe der »Gtk::ItemFactory« aufgebaut, die zuerst einen Menübalken vom Typ »Gtk::MenuBar« erzeugt. Die Menü-Einträge und ihnen untergeordnete ausklappbare Menüs entstehen per »create_items()«.

Der »path«-Parameter gibt dabei die Lage des Menüpunkts an - so spezifiziert »/_File/_Quit« den Eintrag »Quit« unter dem »File«-Eintrag im Menübalken. Die Underlines »_« sorgen dafür, dass »Gtk« das folgende Zeichen unterstreicht und der Anwender mit den entsprechenden Tasten (etwa [Alt]+[F]) im Menü navigieren kann. Der »callback«-Parameter setzt eine Funktion, die »Gtk« anspringt, falls der Benutzer den Eintrag mit der Maus anwählt oder die über den »accelerator«-Parameter definierte Tastenkombination drückt. Die Callback-Funktion ist hier als anonyme Subroutine ausgelegt: Sie trägt keinen Namen und ist nur an Ort und Stelle zu benutzen.

Layout-Manager

Um Widgets geometrisch anzuordnen, kommen zwei verschiedene Verfahren zum Einsatz: »Gtk::VBox« und »Gtk::Table«. Das Container-Element »Gtk::VBox« richtet in ihm enthaltene Widgets vertikal aus. Seine »pack_start()«-Methode platziert dabei die Elemente vom oberen Rand nach unten, während »pack_end()« seine Widgets von unten nach oben stapelt. Der Aufruf

$vb->pack_start(Menübalken, Expand, Fill, 
Padding);

packt den Menübalken oben in die VBox. Gtkticker holt den Balken per »$factory->get_widget('<main>')« in Zeile 108 über seinen Namenseintrag aus der Factory.

Der Expand-Parameter gibt an, ob die Fläche, in der das Widget schwimmt, sich vergrößert, falls der Bediener das Hauptfenster mit der Maus aufzieht. Falls ja, gibt Fill an, ob das Widget selbst sich ausdehnt - auf diese Weise können kleine Druckknöpfe riesengroß werden. Padding spezifiziert schließlich die Anzahl der Pixel, die das Widget mindestens vertikal zu seinen Nachbarn hält. Statusmeldungen zeigt Gtkticker in einem unauffälligen »Gtk::Label«-Widget direkt über dem »Update«-Knopf. Die »set_alignment()«-Methode erreicht mit

$STATUS->set_alignment(0.5, 0.5);

dass der Text horizontal und vertikal zentriert wird. Wer experimentieren will: Ein Wert »0.0« wäre links-, »1.0« wäre rechtsbündig.

Das Container-Element »Gtk::Table« hingegen gibt Perl-Programmierern ein Werkzeug in die Hand, um andere Widgets bequem in Tabellenform zu arrangieren: Die »attach_defaults()«-Methode erwartet das anzuordnende Widget und je zwei Spalten- und zwei Reihenkoordinaten, zwischen denen das Widget liegen soll. Zum Beispiel legt

$table->attach_defaults($label, 0, 1, 1, 2);

fest, dass das mit »$label« referenzierte »Gtk::Label«-Objekt in der ersten Reihe ("zwischen 0 und 1") und in der zweiten Spalte ("zwischen 1 und 2") der Tabelle »$table« aufgehängt sein soll.

And Action!

Widgets vom Typ »Gtk::Button« kann man Aktionen zuordnen, die »Gtk« ausführt, sobald der Benutzer den vereinbarten Knopf drückt. Die in Zeile 144 aufgerufene Methode »signal_connect()« legt fest, dass »Gtk« einen »wake_up«-Event an den POE-Kernel schickt, falls der Benutzer auf den »Update«-Knopf klickt. Auch das Hauptfenster verknüpft eine Aktion - im Ereignis löst der Benutzer das Schließen des Fensters aus, wenn er auf das »X« rechts oben klickt.

$w->signal_connect('destroy',
    sub {Gtk->exit(0)});

räumt die »Gtk«-Session auf und bricht das Programm ab. Sind alle Widgets definiert, werden sie von der »show_all()«-Methode des Hauptfensters (Zeile 148) auf den Bildschirm befördert.

Der Kernel schlägt zurück

Im Zustand »yhoo_response« springt der POE-Kernel zur ab Zeile 152 gelisteten Funktion »resp_handler«. Per Definition legt »POE::Component::Client::HTTP« dabei ein Request- und ein Response-Paket in »ARG0« und »ARG1« ab. POE nutzt ja diese seltsam anmutende Parameterübergabe, nachdem es neue numerische Konstanten wie »KERNEL«, »HEAP«, »ARG0«, »ARG1« eingeführt hat. Der POE-Autor erwartet, dass der Programmierer sie nutzt, um das Funktionsparameter-Array »@_« zu indizieren: »$_[KERNEL]« zum Beispiel gibt so immer das Kernelobjekt zurück.

Die erwähnten Request- und Response-Pakete sind Referenzen auf Arrays, deren erste Elemente die »HTTP::Request«- beziehungsweise »HTTP::Response«-Objekte enthalten. Also extrahiert der »map«-Befehl sie in den Zeilen 154 und 155 nach »$req« und »$resp«.

Tritt ein HTTP-Fehler auf, erzeugt Zeile 159 eine entsprechende Meldung im Status-Widget und kehrt sofort zurück. Andernfalls wird das globale zweidimensionale Array der Label-Widgets aufgefrischt, die für jede zu überwachende Aktie das Börsensymbol, den aktuellen Kurs und die prozentuale Veränderung anzeigen (null Prozent wird als Sonderfall einfach unterdrückt).

Periodisch verzögerter Alarm

Ein »wake_up«-Event im POE-Kernel löst die ab Zeile 185 definierte Routine »wakeup_handler()« aus. Sie ruft die ab Zeile 67 implementierte Funktion »upd_quotes()« auf, die ein »HTTP::Request«-Objekt definiert und es per Event an die Komponente »POE::Component::Client::HTTP« schickt. Als Zielzustand für den »ticker« gibt sie »yhoo_response« an. Nachdem diese Vorarbeit erledigt ist, setzt »wakeup_handler()« mit der »delay()«-Methode des Kernels einen Weckruf. Der löst nach der in »$UPD_INTERVAL« definierten Sekundenzahl (60) einen »wake_up«-Event der »ticker«-Session aus. Ab jetzt frischt der Ticker alle 60 Sekunden seine Aktiendaten auf, egal ob der Benutzer den »Update«-Knopf drückt oder nicht.

Installation

Die erforderlichen POE-Module POE sowie POE::Component::Client::HTTP installiert man am besten mit einer CPAN-Shell. Falls das Modul POE::Component::Client::DNS ebenfalls installiert ist, werden sogar DNS-Anfragen asynchron bearbeitet, sonst verursacht das eingesetzte »gethostbyname()« gelegentlich eine kleine Verzögerung.

»Gtk« vom CPAN zieht diverse Abhängigkeiten herein - und bereitete jedenfalls dem System des Autors dieses Beitrags einige Probleme. Aber

touch ./Gtk/build/perl-gtk-ref.pod
perl Makefile.PL --without-guessing

im Distributionsverzeichnis mit nachgeschobenem »make install« brach alle Widerstände. Und wie sonst auch lässt sich die Geschwätzigkeit der Logs des Skripts auf dem Terminal mit »Log::Log4perl« (ebenfalls vom CPAN) und in Zeile 22 einstellen.

Es ist faszinierend, wie glatt sich die Oberfläche bedienen lässt. Selbst wenn man während eines automatischen Auffrischvorgangs über ein langsames Netzwerk im Menü rumfuhrwerkt, kommt die Applikation nicht ins Schleudern. Eine ideale Technologie für alle grafischen Client-Applikationen. (jk)

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2004/04/Perl]

[2] POE: [http://poe.perl.org]

[3] Jeffrey Goff, "A Beginner's Introduction to POE", 2001: [http://www.perl.com/pub/a/2001/01/poe.html]

[4] Matt Sergeant, "Programming POE", Vortrag auf TPC 2002: [http://axkit.org/docs/presentations/tpc2002]

[5] Gtkperl: [http://gtkperl.org]

[6] Gtkperl-Tutorial: [http://personal.riverusers.com/~swilhelm/gtkperl-tutorial/]

[7] Eric Harlow, "Developing Linux Applications with GTK+ and GDK": New Riders, 1999, ISBN 0735700214

Der Autor

Michael Schilli arbeitet als Software-Engineer für AOL/Netscape in Mountain View, 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 ist [http://perlmeister.com].

Listing 2: Gtkticker

001 #!/usr/bin/perl
002 ###########################################
003 # gtkticker
004 # Mike Schilli, 2004 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008 
009 my $YHOO_URL = "http://quote.yahoo.com/d?".
010                "f=sl1c1&s=";
011 my $RCFILE   = "$ENV{HOME}/.gtkticker";
012 my @LABELS       = ();
013 my $UPD_INTERVAL = 60;
014 my @SYMBOLS;
015 
016 use Gtk;
017 use POE qw(Component::Client::HTTP);
018 use HTTP::Request;
019 use Log::Log4perl qw(:easy);
020 use Data::Dumper;
021 
022 Log::Log4perl->easy_init($DEBUG);
023 
024     # Read config file
025 open FILE, "<$RCFILE" or
026     die "Cannot open $RCFILE";
027 while(<FILE>) {
028     next if /^\s*#/;
029     push @SYMBOLS, $_ for /(\S+)/g;
030 }
031 close FILE;
032 
033 POE::Session->create(
034    inline_states => {
035        _start         => \&start,
036        _stop    => sub { INFO "Shutdown" },
037        yhoo_response  => \&resp_handler,
038        wake_up        => \&wake_up_handler,
039    }
040 );
041 
042 my $STATUS;
043 
044 $poe_kernel->post("ticker", "wake_up");
045 $poe_kernel->run();
046 
047 ###########################################
048 sub start {
049 ###########################################
050 
051   DEBUG "Starting up";
052 
053   $poe_kernel->alias_set( 'ticker' );
054 
055   my_gtk_init();
056 
057   $STATUS->set("Starting up");
058 
059   POE::Component::Client::HTTP->spawn(
060     Agent     => 'gtkticker/0.01',
061     Alias     => 'useragent',
062     Timeout   => 60,
063   );
064 }
065 
066 ###########################################
067 sub upd_quotes {
068 ###########################################
069 
070     my $request = HTTP::Request->new(
071         GET => $YHOO_URL .
072                join ",", @SYMBOLS);
073 
074     $STATUS->set("Fetching quotes");
075 
076     $poe_kernel->post('useragent',
077       'request', 'yhoo_response',
078       $request);
079 }
080 
081 #########################################
082 sub my_gtk_init {
083 #########################################
084 
085     my $w = Gtk::Window->new();
086     $w->set_default_size(150,200);
087 
088         # Create Menu
089     my $accel = Gtk::AccelGroup->new();
090     $accel->attach($w);
091     my $factory = Gtk::ItemFactory->new(
092        'Gtk::MenuBar', "<main>", $accel);
093 
094     $factory->create_items(
095       { path =>  '/_File',
096         type =>  '<Branch>',
097       },
098       { path        =>  '/_File/_Quit',E
099         accelerator =>  '<control>Q',
100         callback    =>
101                   [sub { Gtk->exit(0) }],
102       });
103 
104     my $vb   = Gtk::VBox->new(0, 0);
105     my $upd  = Gtk::Button->new(
106                               'Update');
107 
108     $vb->pack_start($factory->get_widget(
109                     '<main>'), 0, 0, 0);
110 
111         # Button at bottom
112     $vb->pack_end($upd,  0, 0, 0);
113 
114         # Status line on top of buttons
115     $STATUS = Gtk::Label->new();
116     $STATUS->set_alignment(0.5, 0.5);
117     $vb->pack_end($STATUS, 0, 0, 0);
118 
119     my $table = Gtk::Table->new(
120                      scalar @SYMBOLS, 3);
121     $vb->pack_start($table, 1, 1, 0);
122 
123     for my $row (0..@SYMBOLS-1) {
124 
125       for my $col (0..2) {
126 
127         my $label = Gtk::Label->new();
128         $label->set_alignment(0.0, 0.5);
129         push @{$LABELS[$row]}, $label;
130 
131         $table->attach_defaults($label,
132             $col, $col+1, $row, $row+1);
133       }
134 
135     }
136 
137     $w->add($vb);
138 
139         # Destroying window
140     $w->signal_connect('destroy',
141         sub {Gtk->exit(0)});
142 
143         # Pressing update button
144     $upd->signal_connect('clicked',
145         sub { DEBUG "Sending wake_up";
146               $poe_kernel->post(
147                 'ticker', 'wake_up')} );
148     $w->show_all();
149 }
150 
151 ###########################################
152 sub resp_handler {
153 ###########################################
154     my ($req, $resp) =
155             map { $_->[0] } @_[ARG0, ARG1];
156 
157     if($resp->is_error()) {
158         ERROR $resp->message();
159         $STATUS->set($resp->message());
160         return 1;
161     }
162 
163     DEBUG "Response: ", $resp->content();
164 
165     my $count = 0;
166 
167     for(split /\n/, $resp->content()) {
168         my($symbol, $price, $change) =
169                            split /,/, $_;
170         chop $change;
171         $change = "" if $change =~ /^0/;
172         $symbol =~ s/"//g;
173         $LABELS[$count][0]->set($symbol);
174         $LABELS[$count][1]->set($price);
175         $LABELS[$count][2]->set($change);
176         $count++;
177     }
178 
179     $STATUS->set("");
180 
181     1;
182 }
183 
184 ###########################################
185 sub wake_up_handler {
186 ###########################################
187     DEBUG("waking up");
188 
189         # Initiate update
190     upd_quotes();
191 
192         # Re-enable timer
193     $poe_kernel->delay('wake_up',
194                    $UPD_INTERVAL);
195 }