Download: Programowanie_perl
Transkrypt
Download: Programowanie_perl
PROGRAMOWANIE Perl GUI Gtk i zapytania WWW z Perl Object Environment Gracz zespołowy Perl Object Environment (POE) dostarcza platformy dla tworzenia skryptów, które maja wykonywać tzw. cooperative multitasking bez jakiejkolwiek pomocy ze strony schedulera systemu operacyjnego. Aplikacja, którą stworzymy w tym miesiącu, umożliwi wykonywanie z interfejsu Gtk czasochłonnych zapytań WWW. MICHAEL SCHILLI A plikacje GUI są zwykle sterowane wydarzeniami. Typowy program będzie posiadał główną pętlę, której używa do oczekiwania na wydarzenia, takie jak kliknięcia myszy czy znaki wpisywane z klawiatury. Istotne jest przy tym, aby program przetwarzał te zdarzenia bez opóźnień i szybko powracał do głównej pętli. To sprawia, że użytkownik nie zauważa chwilowego barku dostęp do interfejsu. Ticker wyświetlający ceny akcji, któremu przyjrzymy się w tym miesiącu, okresowo łączy się z serwisem finansowym portalu Yahoo, żeby pobrać ceny akcji (Rysunek 1). W zależności od jakości połączenia sieciowego, żądanie rozwiązania nazwy DNS może zająć nawet kilka sekund. Mimo to chciałbym, żeby mój program w tym czasie nadal wyświetlał swój interfejs normalnie pracując. Programiści używają w takich wypadkach wielowątkowości i wielozadaniowości. Jednak obydwie techniki są zbyt skomplikowane do naszego zastosowania. Krytyczne części danych muszą być chronione przed równoczesnym dostępem, co sprawia też, że wyszukiwanie błędów jest bardzo 78 Maj 2004 trudne. Jeśli kiedykolwiek musieliście analizować zrzut pamięci z 200 aktywnymi wątkami, na pewno wiesz co mam na myśli. Jest metoda na uniknięcie tych problemów – tzw. cooperative multitasking z POE – Perl Object Environment [2], którego głównym twórcą jest Rocco Caputo. Środowisko zostało zaimplementowane jako maszyna stanu, które i tak wykonuje dokładnie jeden proces w jednym wątku, ale dla aplikacji użytkownika „jądro” umożliwia uruchamianie wielu zadań w trybie semi-równoległym. Ręka na pulsie Programiści, którzy chcą zadawać zapytania na temat cen akcji z poziomu skryptu Perl, zazwyczaj używają modułu CPAN o nazwie Yahoo::FinanceQuote: use Finance::YahooQuote; my @quote = U getonequote($symbol); Niestety, moduł pracuje synchronicznie i to bardzo utrudnia osiągnięcie płynności przy przewijaniu. Funkcja getonequote wysyła www.linux-magazine.pl zapytanie HTTP do serwera Yahoo server, oczekuje na odpowiedź i zwraca rezultaty. Jednak my chcemy, żeby wyświetlanie w naszym programie było płynne i nie było blokowane przez oczekiwanie – zgodnie z prawem Murphy'ego, ktoś na pewno przeciągnie inne okno ponad naszym tickerem. W takim przypadku aplikacja powinna przerysować swój interfejs, żeby odświeżyć widok okna, jednak ponieważ program oczekuje na wyniki zapytania HTTP, nie zrobi tego, a jedynie pozostawi paskdny, pusty i szary prostokąt na pulpicie. Słowem kompromitacja dla autora. Podejście asynchroniczne Byłoby bardziej elegancko przekazać żądanie WWW i powrócić do odświeżania GUI bez oczekiwania na rezultaty. Kiedy nadejdzie wreszcie odpowiedź z serwera Yahoo, powinien zostać wygenerowany swoisty alarm. To oznacza szybką aktualizację okna tickera i „wskoczenie” z powrotem do głównej pętli GUI. Dokładnie tak działa POE. Dostarcza jądro, gdzie poszczególne aplikacje rejestrują swoje sesje i maszyna stanu przełącza się Perl zwracana do jądra. Maszyna stanu przechodzi w stan wake_up co każde 60 sekund (poprzez alarm) lub gdy ktoś kliknie przycisk Update w GUI. Uruchamia to kolejną maszynę stanu POE::Component::Client::HTTP i natychmiast przekazuje kontrolę z powrotem do jądra. Rysunek 1: PoCoCli::HTTP to tak zwany Dane Oparty na GTK w ostatniej kolejności ticker pokazują- komponent POE: maszyna stanu, która definiuje swoją własną sesję Mimo że operacje odczytu i zapisu cy ceny akcji łą(nazywaną useragent, patrz Listing nie są na razie asynchroniczne czy się co pewien 2, linia 73), przyjmuje zapytania (POE po prostu używa nieblokujączas ze serwisem WWW w stanie request i przełącza cych funkcji syswrite lub sysread), finansowym porsię do POE dopóki nie otrzyma pełdowolna informacja jest przekazytalu Yahoo. nej odpowiedzi HTTP. W tym mowana dalej bardzo szybko. Aspekt mencie useragent sprawia, że kernel powoduje, kooperatywny POE wynika stąd, że sesje poleiż sesja wywołująca ticker przełącza się w stan gają na konkurujących sesjach. Jeśli zadanie yhoo_response, który był przekazany poprzednie zajmie permanentnie procesora, sesja munio do useragent. si przekazać kontrolę z powrotem do kernela. Kernel przełącza stan sesji ticker i sesja akMultitasking w ramach pojedynczego ceptuje odpowiedzi HTTP, odświeża widget wątku znacząco ułatwia rozwój aplikacji, cen akcji w GUI i przekazuje kontrolę z ponie trzeba martwić się o blokowanie dostęwrotem do kernela. In line 69, the POE::Compu, a jeśli pojawi się błąd, to jest on łatwy ponent::Client::HTTP uruchamia spawn() do zlokalizowania. POE będzie współpracoi powoduje, że łańcuch gtkticker/0.01 pojawia wać z główną pętlą zdarzeń. POE rozpoznasię po stronie serwera jako UserAgent i wreszje Perl/Tk i gtkperl automatycznie oraz incie ustawia timeout na 60 sekund. tegruje się z nimi bez problemu. Dzięki temu kernel może przypisać poszczególnym zdarzeniom szczeliny czasowe, tak jak jawPodręcznik Yahoo nie zdefiniowanym sesjom. Jest to zatem Linie 10-21 z Listingu 2 określają URL do odpowiedź dotycząca odświeżania. serwisu z kursami prowadzonego przez portal Yahoo. Interfejs CGI tego serwisu oczekuje dwóch parametrów: Alarmowanie jądra Ticker używa maszyny stanu POE::Session, ■ parametru określającego format (f=), jak pokazano na Rysunku 2. Faza inicjalizaktóry formatuje poszczególne pola użycji, _start generuje interfejs GTK i ustawia wając następujących nazw: s (symbol), alias sesji na ticker, co umożliwi nam łatwe l1 (cena akcji) i c1 (procentowa zmiana zidentyfikowanie sesji później. Kontrola jest do ceny z poprzedniego dnia) między stanami i wymienia wiadomości. Operacje I/O są asynchroniczne. Zamiast otwierania pliku lub gniazda, proces może po prostu powiedzieć „Hej kernel, chcę odczytać pewne dane. Czy możesz mnie obudzić, kiedy już będą dostępne?” PROGRAMOWANIE Listing 1: Plik konfiguracyjny 01 02 03 04 05 06 # ~/.gtkticker TWX MSFT YHOO AMZN RHAT DODGX JNJ COKE IBM SUN parametru określającego symbole, który zawiera listę skrótów reprezentujących akcje poszczególnych firm. Skróty są rozdzielane przecinkami np. YHOO, MSFT, TWX. Serwer Yahoo odpowiada w następujący sposób: ■ „YHOO”,45.38,+0.35 „MSFT”,27.56,+0.19 „TWX”,18.21,+0.75 Gtkticker przyjmuje odpowiedź – linia po linii, używa przecinków do rozdzielania łańcuchów znaków i łączy dane przygotowując je do wyświetlania w GUI. Konfiguracja katalogu domowego Wiersze 13 i 14 określają gtkicker w katalogu domowym użytkownika jako plik symbolu wyświetlany przez ticker. Wiersze od 30 do 37 przetwarzają plik, wiersz po wierszu, pomijając wiersze zaczynające się od #, co jest traktowane jako komentarz (wiersz 33). Jawna pętla for znajduje się w wierszu 35 ... for /(\S+)/g; wykonuje się ona na wszystkich słowach Prenumerata Linux Magazine Nie przegap takiej okazji ■ Zamawiając prenumeratęoszczędzasz! ■ Płacisz jak za 9 numerów, a otrzymujesz 12! ■ Z każdym numerem DVD lub płyta CD-ROM. Najszybszy sposób zamówienia prenumeraty: http://www.linux-magazine.pl Infolinia: 0801-800-105 PROGRAMOWANIE Perl znajdujących się w linii (począwszy od lewej) i symbol jest umieszczany w zmiennej $_. Funkcja push układa skróty nazw akcji w tablicy @SYMBOLS – umożliwia to zapis wielu symboli w jednej linii, oddzielonych spacjami. Listing 1 pokazuje przykładowy plik. Pomimo użycia framework-u POE, gtkticker wykorzystuje standardowe funkcje synchroniczne I/O do odczytu pliku konfiguracyjnego. Taniec zacząć czas... Linia 39 definiuje stan maszyny ticker. Parametr inline_states używa referencji przez hasz, aby mapować funkcje na poszczególne stany. Kernel będzie skakał do tych funkcji, jeśli maszyna wejdzie w odpowiedni stan. Linia 53 przełącza stan wake_up dla sesji ticker jądra $poe_kernel->post('ticker', U 'wake_up'); poprzez zmienną $poe_kernel eksportowaną przez POE. Wiersz 55 uruchamia główną pętlę krenela $poe_kernel->run(); w której program pozostaje do czasu jego wyłączenia. I o to nam chodzi! Konstrukcja obiektu POE::Session, pokazana poprzednio, ma pewien efekt uboczny. Uruchamia procedurę start() zdefiniowaną w linii 58, która mapuje stan _start. Powoduje to ustawienie aliasa dla sesji ticker i wykonuje skok do my_gtk_init(). Ta funkcja z kolei (uruchamiana w linii 98) tworzy GUI GTK. Budowanie GUI z użyciem GTK Gtk jest modułem CPAN autorstwa Marc-a Listing 2: Program Gtkticker c.d. 001 #!/usr/bin/perl 002 ############################# 003 # gtkticker 004 # Mike Schilli, 2004 005 # ([email protected]) 006 ############################# 007 use warnings; 008 use strict; 009 010 my $YHOO_URL = 011 "http://quote.yahoo.com/d?". 012 "f=sl1c1&s="; 013 my $RCFILE = 014 "$ENV{HOME}/.gtkticker"; 015 my @LABELS = (); 016 my $UPD_INTERVAL = 60; 017 my @SYMBOLS; 018 019 use Gtk; 020 use POE qw( 021 Component::Client::HTTP); 022 use HTTP::Request; 023 use Log::Log4perl qw(:easy); 024 use Data::Dumper; 025 026 Log::Log4perl->easy_init( 027 $DEBUG); 028 029 # Odczytujemy plik konfiguracyjny 030 open FILE, "<$RCFILE" or 031 die "Nie mogę otworzyć pliku $RCFILE"; 032 while(<FILE>) { 033 next if /^\s*#/; 034 push @SYMBOLS, $_ 035 for /(\S+)/g; 036 } 037 close FILE; 038 039 POE::Session->create( 80 Maj 2004 040 041 042 043 }, 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 www.linux-magazine.pl inline_states => { _start => \&start, _stop => sub { INFO "Zamykanie" yhoo_response => \&resp_handler, wake_up => \&wake_up_handler, } ); my $STATUS; $poe_kernel->post( "ticker", "wake_up"); $poe_kernel->run(); ############################# sub start { ############################# DEBUG "Startujemy"; $poe_kernel->alias_set( 'ticker'); my_gtk_init(); $STATUS->set("Startujemy"); POE::Component::Client::HTTP ->spawn( Agent => 'gtkticker/0.01', Alias => 'useragent', Timeout => 60, ); } ############################# sub upd_quotes { 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 ############################# my $request = HTTP::Request->new( GET => $YHOO_URL . join ",", @SYMBOLS); $STATUS->set( "Pobieram kursy"); $poe_kernel->post( 'useragent', 'request', 'yhoo_response', $request); } ############################# sub my_gtk_init { ############################# my $w = Gtk::Window->new(); $w->set_default_size( 150,200); # Tworzymy menu my $accel = Gtk::AccelGroup->new(); $accel->attach($w); my $factory = Gtk::ItemFactory->new( 'Gtk::MenuBar', "<main>", $accel); $factory->create_items( { path => '/_File', type => '<Branch>', }, { path => '/_File/_Quit', accelerator => Perl Lehmann-a, który był również uprzejmy przejrzeć niniejszy artykuł. Obecnie moduł został zastąpiony przez Gtk2, jednak nowa wersja ma kilka problemów z POE. Tak czy inaczej, moduł GTK i tak wykonuje wspaniałą robotę. Obiekt klasy Gtk::Window reprezentuje główne okno aplikacji. Posiada ono typowy pasek menu na górze dający dostęp do rozwijalnego menu File, które ma jedna opcję – Quit. Opcja ta używa procedury Gtk->exit(0) do kończenia pracy programu. Jest także obiekt Gtk::AccelGroup, który umożliwia zamknięcie programu przez wciśnięcie kombinacji klawiszy [Ctrl]+[Q]. Obiekt tek dodaje tzw. Akceleratory (kombinacje klawiszy). Domyślne menu Menu jest tworzone przez użycie klasy Gtk::ItemFactory, która jest najpierw używana to utworzenia paska menu Gtk::MenuBar. Pozycje menu i ewentualne podmenu są tworzone przy użyciu metody create_items(). Parametr path określa pozycję elementu menu – przykładowo, /_File/_Quit definiuje pozycję Quit poniżej File na pasku menu. Znak podkreślenia _ powoduje, że Gtk podkreśla daną literę, umoż- PROGRAMOWANIE liwiając użytkownikowi wciśnięcie skrótu klawiszowego (takiego jak [Alt]+[F]) do przemieszczania się po menu. Parametr callback określa, że Gtk uruchomi akcję niezależnie od tego, czy użytkownik wybierze pozycję w menu myszą, czy wciśnie kombinację klawiszy zdefiniowaną przez parametr accelerator. Menedżer wyglądu Można stosować dwa sposoby do geometrycznego rozmieszczania widgetów: Gtk::VBox i Gtk::Table. Kontener Gtk::VBox wyrównuje zawarte w nim widgety pionowo. Metoda Listing 2: Program Gtkticker c.d. 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 '<control>Q', callback => [sub { Gtk->exit(0) }], }); my $vb = Gtk::VBox->new( 0,0); my $upd = Gtk::Button->new( 'Update'); $vb->pack_start( $factory->get_widget( '<main>'), 0, 0, 0); # Button at bottom $vb->pack_end($upd, 0, 0, 0); # Status line on top # of buttons $STATUS= Gtk::Label->new(); $STATUS->set_alignment( 0.5, 0.5); $vb->pack_end($STATUS, 0, 0, 0); my $table = Gtk::Table->new( scalar @SYMBOLS, 3); $vb->pack_start($table, 1, 1, 0); for my $row (0.. @SYMBOLS-1) { for my $col (0..2) { my $label = Gtk::Label->new(); $label->set_alignment( 0.0, 0.5); push @{$LABELS[$row]}, $label; 162 163 $table->attach_defaults( 164 $label, $col, $col+1, 165 $row, $row+1); 166 } 167 } 168 169 $w->add($vb); 170 171 # Niszczymy okno 172 $w->signal_connect( 173 'destroy', sub { 174 Gtk->exit(0)}); 175 176 # Wciśnięcie przycisku update 177 $upd->signal_connect( 178 'clicked', sub { 179 DEBUG "Wysyłam wakeup"; 180 $poe_kernel->post( 181 'ticker', 'wake_up')} 182 ); 183 $w->show_all(); 184 } 185 186 ############################# 187 sub resp_handler { 188 ############################# 189 my ($req, $resp) = 190 map { $_->[0] } 191 @_[ARG0, ARG1]; 192 193 if($resp->is_error()) { 194 ERROR $resp->message(); 195 $STATUS->set( 196 $resp->message()); 197 return 1; 198 } 199 200 DEBUG "Odpowiedź: ", 201 $resp->content(); 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 my $count = 0; for(split /\n/, $resp->content()) { my($symbol, $price, $change) = split /,/, $_; chop $change; $change = "" if $change =~ /^0/; $symbol =~ s/"//g; $LABELS[$count][0]-> set($symbol); $LABELS[$count][1]-> set($price); $LABELS[$count][2]-> set($change); $count++; } $STATUS->set(""); 1; } ############################# sub wake_up_handler { ############################# DEBUG("waking up"); # Initiate update upd_quotes(); # Re-enable timer $poe_kernel->delay( 'wake_up', $UPD_INTERVAL); } www.linux-magazine.pl Maj 2004 81 PROGRAMOWANIE Perl pack_start() umieszcza elementy od góry do dołu, podczas gdy pack_end() czyni to odwrotnie. Definicja $vb->pack_start($menu_bar, U $expand, $fill, $padding); umieszcza pasek menu na górze Vbox-a. W linii 132 gtkticker używa $factory>get_widget('<main>') do pobrania obiektu paska używając nazwy. Parametr $expand określa, czy obszar, który zajmuje widget powinien się rozszerzać, jeśli użytkownik użyje myszy dla rozszerzenia głównego okna. Jeśli tak, to widget będzie również się powiększał – pozwoli to przyciskom zwiększać się do dowolnych rozmiarów. Wreszcie $padding określa minimalną ilość pikseli, która powinna rozdzielać (pionowo) widgety od siebie. Gtkticker wyświetla informacje o statusie poprzez widget Gtk::Label bezpośrednio nad przyciskiem Update. Metoda set_alignment() używa następującej składni $STATUS->set_alignmentU (0.5, 0.5); żeby wykonać wyrównanie tekstu poziome i pionowe. Jeśli chcemy eksperymentować, wartość wyrównania poziomego 0.0 oznacza wyrównanie do lewej, a 1.0 wyrównanie tekstu do prawej. Dla kontrastu kontener Gtk::Table daje programistom Perla narzędzie do wygodnego rozmieszczania widgetów w tabeli. Metoda attach_defaults() oczekuje pięciu parametrów: widgeta wzorcowego, do którego ma być wyrównywany widget oraz dwóch kolumn i współrzędnych, między którymi widget będzie umieszczony. Przykładowo, $table->attach_defaultsU ($label, 0, 1, 1, 2); Akcja! Do widgetów Gtk::Button można przypisywać określone akcje. Gtk wykonuje akcję, gdy użytkownik wciśnie przycisk. Metoda wywoływana w linii 177 – signal_connect(), określa, że Gtk powinno wysłać zdarzenie wake_up do jądra POE, gdy użytkownik kliknie przycisk Update. Do głównego okna również przypisano określoną akcję – użytkownicy Maj 2004 $w->signal_connect('destroy', sub {Gtk->exit(0)}); kończy sesję Gtk i zamyka program. Po zdefiniowaniu wszystkich widgetów, metoda show_all() wyświetla je w oknie głównym (linia 183) na ekranie. Kernel kontratakuje W stanie yahoo_response jądro POE skacze do funkcji pokazanej poniżej linii 187 – resp_handler. Z definicji POE::Component::Client::HTTP będzie przechowywać pakiety zapytania i odpowiedzi w ARG0 i ARG1, gdy dojdzie do takiego skoku. POE wykorzystuje nieco dziwny sposób przekazywania parametrów po wprowadzeniu kilku nowych funkcji reprezentujących stałe liczbowe, takich jak KERNEL, HEAP, ARG0, ARG1. Autorzy POE oczekują, że programiści użyją ich do indeksowania tabeli parametrów funkcji @_. Przykładowo, $_[KERNEL] zawsze zwróci obiekt kernela, którego indeks wskazuje KERNEL. Wspomniane pakiety z pytaniami i odpowiedziami są referencjami do tablic, które przechowują obiekty HTTP::Request lub HTTP::Response. Wreszcie polecenie map z wiersza 190 dokonuje ekstrakcji danych do $req i $resp. Jeśli pojawi się błąd HTTP, linia 195 wygeneruje odpowiednią wiadomość w oknie statusu widgeta i dla wartości zwracanej przez funkcję. W innym przypadku globalna, dwuwymiarowa tablica etykiet przycisków jest odświeżana. Widgety wyświetlają symbole akcji, cenę bieżącą oraz zmianę procentową ceny (przypadek szczególny – zmiana 0% jest ignorowany). Alarmy okresowo opóźnione określa, że dla element $label obiektu Gtk::Label zostanie dodany do pierwszego wiersza („między 0 i 1”) i drugiej kolumny („między 1 i 2”) tabeli wskazywanej przez $table. 82 mogą kliknąć krzyżyk na pasku tytułowym okna, aby zakończyć pracę aplikacji. Zdarzenie wake_up wywołuje w jądrze POE procedurę wake_up_handler(), zdefiniowaną w wierszu 232 i następnych. Wywołuje też funkcję upd_quotes(), która jest zaimplementowana w wierszu 79 i kolejnych. Funkcja definiuje obiekt HTTP::Request i używając zdarzenia przesyła go do komponentu POE::Component::Client::HTTP. Docelowy stan dla ticker jest ustawiony na yahoo_response. Po wykonaniu tych czynności przygotowawczych, wake_up_handler() używa zaimplementowanej w jądrze metody delay() do wysłania alarmu. To z kolei powoduje, że zdarzenie wake_up jest wywoływane w sesji ticker, gdy minie określona liczba sekund www.linux-magazine.pl zdefiniowana w $UPD_INTERVAL (w naszym przypadku to 60 sekund). Od tego momentu ticker aktualizuje ceny akcji co każde 60 sekund, bez potrzeby wciskania przycisku Update przez użytkownika. Instalacja Najlepszym sposobem na zainstalowanie niezbędnych pakietów POE oraz POE::Component::Client::HTTP jest użycie powłoki CPAN. Jeśli zostanie również instalowany moduł POE::Component::Client::DNS, zapytania DNS będą wykonywane asynchronicznie; w przeciwnym wypadku wykonywanie funkcji gethostbyname() powodowałoby opóźnienia. Instalacja Gtk z archiwów CPAN oznacza, że trzeba rozwiązać kilka zależności między pakietami, prowadzi to zazwyczaj do różnych problemów w zależności od używanego systemu. Jednak wykonanie poleceń touch./Gtk/build/U perl-gtk-ref.pod perl Makefile.PL U --without-guessing w katalogu dystrybucyjnym, a następnie uruchomienie make install rozwiązało wszystkie problemy. Skrypt loguje informacje do debuggowania na standardowe wyjście (STDOUT), jeśli jest to problemem, można przekierować logowanie używając pakietu Log::Log4perl (również z CPAN) i zamieniając w wierszu 27 $DEBUG na $ERROR. Niezwykłe, jak szybko to działa. Nawet jeśli aplikacja wykonuje automatyczną aktualizację przez powolną sieć, GUI nie jest blokowane. A wszystko takie łatwe do zaimplementowania! ■ INFO [1] Listingi dla artykułu: http://www.linux-magazine.com/Magazine/ Downloads/42/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”, talk at TPC 2002: http://axkit.org/docs/ presentations/tpc2002 [5] Gtkperl: http://gtkperl.org [6] Tutorial Gtkperl: http://personal.riverusers.com/ ~swilhelm/gtkperl-tutorial/ [7] Eric Harlow, „Developing Linux Applications with GTK+ and GDK”: New Riders, 1999, ISBN 0735700214