Wir brauchen Bass (Linux-Magazin, November 2008)

Statt die Ergebnisse eingehender Webrequests nur in der Logdatei des Webservers zu verfolgen, hilft ein Soundserver dem Systemadministrator, die AktivitÀten akustisch zu untermalen.

Immer wenn ich eine neue Ausgabe unseres Blogs ``USA-Rundbrief'' hochlade, eine AnkĂŒndigungs-Email abschicke und das RSS-Feed auffrische, sehe ich anschließend gerne im access.log des Webservers zu, wie die ersten Infohungrigen mit ihren Browsern die Berichte lesen und die darin eingebauten Bilder vergrĂ¶ĂŸern. Allerdings ist das Ansehen der Hits im Serverlog recht anstrengend und eigentlich sollte dies im Hintergrund passieren, sodass ich mich gleich neuen Aufgaben widmen kann.

Wie sonst könnte man den Webnutzern ĂŒber die Schulter schauen? In dem Buch ``Netscape Time'' von Jim Clark, das ich vor vielen Jahren einmal gelesen habe, stand, dass die frĂŒhen Netscapeler nach dem Fertigstellen einer neuen Softwareversion die eintrudelnden Hits fĂŒr den Download akustisch ĂŒber den PC-Lautsprecher ausgaben. Ein Browser-Download fĂŒr Windows löste ein Froschquaken aus, fĂŒr Macs wurde ein GerĂ€usch zerbrechenden Glases gespielt und fĂŒr die Unix-Plattform löste sich ein Kanonenschuss [4]. So erfreuten sich die Internetpioniere in ihren Cubicles gemeinsam ĂŒber den sicht- und letztlich auch hörbaren Erfolg ihrer Arbeit.

Ein Ă€hnliches Verfahren lĂ€sst sich leicht in Perl implementieren, allerdings ist die Lage bei mir verzwickter: Der Webserver lĂ€uft bei einem Hosting-Service, der zwar den Shell-Zugang ĂŒber ssh erlaubt, aber keinerlei VerstĂ€ndnis fĂŒr meine akustischen BedĂŒrfnisse zeigt.

Firewalltunnel

Aber das Skript boom-sender auf dem Hosting-Rechner, das die access.log-Datei des Webservers ĂŒberwacht und bei ausgewĂ€hlten URLs Nachrichten ĂŒber einen Reverse-ssh-Tunnel an einen Soundserver boom-receiver auf meinem Rechner daheim schickt, schafft da schnell Abhilfe.

Abbildung 1: Das Skript, das die Accesslog-Datei des Webservers auf dem Hostingrechner ĂŒberwacht, kommuniziert ĂŒber den ssh-Tunnel mit dem sounderzeugenden Desktop.

Abbildung 1 zeigt den Aufbau: Der Soundserver ist ein mit dem CPAN-Modul POE geschriebenes Skript, das auf Port 8080 des heimischen Rechners auf Soundbefehle wartet. Auf der Seite des Hosting-Providers lĂ€uft ein weiteres POE-Skript, das bei Änderungen im access.log anspringt und als TCP-Client Nachrichten abschickt. Da mein Heimrechner hinter einer Firewall steckt, kann der Logchecker boom-sender ihn nicht direkt kontaktieren aber ein per

    home$ ssh -R 8080:localhost:8080 host.xyz-hosting.com

vom Heimrechner aus angelegter Reverse-Tunnel verbindet die beiden GesprÀchsparter und das Logskript muss seine Nachrichten nur an den Port 8080 des Hostingrechners schicken, damit sie durch den Tunnel wie durch Zauberei auf Port 8080 des Heimrechners ankommen, ganz so, als gÀbe es die Firewall gar nicht.

Auf Befehl Sound

Die Soundmaschine ist ein TCP-Server, der auf Port 8080 des heimischen Rechners von TCP-Clients die Namen von Sounddateien entgegennimmt und diese dann mit der play-Utility aus dem Fundus des Programmpakets sox auf dem Linux-Soundsystem abspielt. Das Programm play kommt standardmĂ€ĂŸig mit Ubuntu und kann nicht nur .wav-Dateien, sondern auch .mp3s ausgeben, falls Ubuntu dafĂŒr konfiguriert ist.

Abbildung 2 zeigt die Interaktion eines Testclients mit dem laufenden Soundserver: Der Befehl telnet auf den eingestellten Port des Rechners localhost zeigt zunĂ€chst eine Grußnachricht des Servers und alle Sounddateien, die dieser anbietet. Schickt der Client den Namen einer dieser .wav-Files als Befehl an den Server, spielt dieser sie ab. Das Skript in Listing boom-receiver legt in Zeile 09 in $SOUND_DIR das Verzeichnis fest, in der die Sounddateien liegen mĂŒssen. Aus SicherheitsgrĂŒnden sind nur Dateinamen und keine Pfade erlaubt. Voreingestellt ist das aktuelle Verzeichnis (.).

POE bietet sich hier als Technologie sowohl fĂŒr die Server wie auch die Clientseite an, denn man muss die standardmĂ€ĂŸig bereitgestellten POE-Komponenten nur noch wie mit einem Lego-Baukasten zusammenstecken. Eine gute EinfĂŒhrung in POE gibt die Website poe.perl.org oder das POE-Kapitel des Buchs [5]. Der Server in boom_receiver definiert Callbacks fĂŒr die ZustĂ€nde ClientConnected (Client hat angedockt), ClientInput (Client hat eine Textzeile ĂŒbermittelt), sowie in InlineStates einen weiteren Zustand, der fĂŒr die weiter unten erklĂ€rten AbrĂ€umarbeiten nach dem Abspielen eines GerĂ€usches verantwortlich zeichnet.

Der Server bearbeitet so viele Clientverbindungen quasi gleichzeitig. Die in der POE-Komponente versteckte Logik sorgt fĂŒr den reibungslosen Ablauf hinter den Kulissen. Wie bei jedem POE-Skript legt der Programmcode zunĂ€chst das Verhalten bei allen möglicherweise eintretenden Events fest, um dann den POE-Kernel mit POE::Kernel->run() zu starten. Dieser lĂ€uft dann, bis das Programmende eintritt, entweder durch einen fatalen Fehler oder wenn der Benutzer es mit CTRL-C abbricht.

Abbildung 2: Der Test-Client telnet verbindet sich mit dem EmpfÀnger-Server, der auf Kommando eine Sounddatei abspielt.

Nicht Trödeln

Die Funktion sound_play() ab Zeile 60 in boom_receiver spielt eine ihr ĂŒberreichte Sounddatei ab. Hierzu erzeugt sie ein POE::Wheel, ein RĂ€dchen im Getriebe des POE-Systems, mit dem der POE-Kernel mit der Außenwelt kommuniziert. Damit das System keine Zeit verplempert, darf Perl-Code in POE nur solange ohne Unterbrechnung vor sich hinwerkeln, wie er mit voller Geschwindigkeit lĂ€uft. Bei der Interaktion mit Dateien, Sockets oder anderen Prozessen kommt es naturgemĂ€ĂŸ zu Verzögerungen. Da der Zugriff auf die Festplatte oder das Netzwerk um GrĂ¶ĂŸenordungen langsamer ist als das Abarbeiten von CPU-Instruktionen oder der Zugriff auf den Hauptspeicher, wĂ€re es Ă€ußerst uneffizient, auf die Erledigung derartiger Aufgaben blockierend zu warten. Statt dessen sorgen Wheels dafĂŒr, dass der POE-Kernel diese Tasks eventbasiert und asynchron abarbeitet. Einen neuen Prozess mit dem Programm play zu starten, ihm eine kurze Sounddatei zu ĂŒbergeben und auf das Abspielen zu warten, kostet gut und gerne eine Sekunde. WĂ€re das Skript in dieser Zeit blockiert, wĂŒrde es die Antwort an den aufgabenstellenden Client solange verzögern und könnte auch keine neuen Clientanfragen beantworten. Das Wheel hingegen sorgt dafĂŒr, dass der Callback sofort zurĂŒckkehrt, der POE-Kernel wieder die Kontrolle ĂŒbernimmt und der Rest einfach im Hintergrund ablĂ€uft.

Das Wheel POE::Wheel::Run verlangt außer dem zu startenden externen Programm und dessen Argumenten auch noch die Definition eines StderrEvent. Dies ist eine Funktion, die das Wheel anspringt, falls Daten an dessen STDERR-Ausgabe ankommen. FĂŒr das Programm play, das normalerweise keine Fehlermeldungen ausgibt und sich nach jeder abgespielten Sounddatei wieder beendet, ist dies jedoch irrelevant und boom-receiver legt einfach einen nicht-existierenden Zustand ignore fest, den POE ignoriert. Stellt das Wheel fest, dass der gestartete play-Prozess sich beendet hat, löst es den in Zeile 75 gesetzten und in Zeile 48 definierten CloseEvent aus.

Idealerweise wĂŒrde das Wheel einen dauerhaft laufenden Prozess wie xmms starten und ihm laufend Sounddateien unterschieben, aber die dafĂŒr vorgesehene POE-Komponenente auf dem CPAN ist etwas in die Jahre gekommen und lĂ€sst sich nicht mehr ohne KlimmzĂŒge mit dem aktuellen <xmms> kompilieren. Schade!

Listing 1: boom-receiver

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use POE;
    04 use POE::Component::Server::TCP;
    05 use POE::Wheel::Run;
    06 use File::Basename;
    07 use Log::Log4perl qw(:easy);
    08 
    09 my $SOUND_DIR = ".";
    10 my @SOUND_FILES = map { basename $_ } 
    11                   <$SOUND_DIR/*.wav>;
    12 
    13 Log::Log4perl->easy_init($DEBUG);
    14 
    15 POE::Component::Server::TCP->new(
    16   Port  => 8080,
    17 
    18   ClientConnected => sub {
    19     $_[HEAP]{client}->put("Soundfiles: [".
    20      join(", ", @SOUND_FILES) . "]" );
    21 
    22     $_[HEAP]{client}->put(
    23      "Ready when you are.");
    24   },
    25 
    26   ClientInput => sub {
    27     my $client_input = $_[ARG0];
    28 
    29     if( $client_input !~ /^[\w.-]+$/ ) {
    30         $_[HEAP]{client}->put(
    31           "Illegal input.");
    32         return;
    33     }
    34 
    35     if( $client_input eq "q" ) {
    36         POE::Kernel->yield("shutdown");
    37         return;
    38     }
    39 
    40     my $msg = sound_play(
    41                 $_[HEAP],
    42                 basename($client_input));
    43 
    44     $_[HEAP]{client}->put( $msg );
    45   },
    46 
    47   InlineStates => {
    48     sound_ended => sub {
    49       my ($heap, $wid) = @_[HEAP, ARG0];
    50       DEBUG "Deleting wheel $wid";
    51       delete $heap->{players}->{$wid};
    52     },
    53   },
    54 );
    55 
    56 POE::Kernel->run();
    57 exit;
    58 
    59 ###########################################
    60 sub sound_play {
    61 ###########################################
    62   my($heap, $file) = @_;
    63 
    64   if(! -f "$SOUND_DIR/$file") {
    65     return "$file doesn't exist";
    66   }        
    67 
    68   POE::Kernel->sig(CHLD => "reaped");
    69 
    70   my $wheel =
    71     POE::Wheel::Run->new(
    72       Program     => "/usr/bin/play",
    73       ProgramArgs => ["$SOUND_DIR/$file"],
    74       StderrEvent => 'ignore',
    75       CloseEvent  => 'sound_ended',
    76   );
    77 
    78   DEBUG "Creating wheel ", $wheel->ID;
    79   $heap->{players}->{ $wheel->ID } = $wheel;
    80 
    81   return "Played $file";
    82 }

Holzauge sei wachsam!

Die gezeigte Implementierung geht zwar verschwenderisch mit den Resourcen des lokalen Rechners um, ist aber durchaus in der Lage, quasi-parallele Requests in GerĂ€usche umzuwandeln. Das Skript muss hierzu eine Referenz auf das sounderzeugende Wheel-Objekt behalten, da POE es sofort abrĂ€umt, sobald sich niemand mehr um es kĂŒmmert. Am Ende von sound_play() ist allerdings die Aufgabe des Wheels noch nicht erledigt, da der POE-Kernel es scheibchenweise nach dem Beenden der Funktion abarbeitet. Um den verfrĂŒhten Tod zu vermeiden und gleichzeitig die Wheels nicht unnötig herumlungern zu lassen, legt Zeile 79 das jeweils erzeugte Wheel-Objekt mit seiner ID im POE-Session-Heap unter dem SchlĂŒssel players ab. Da das Wheel einen CloseEvent mit dem Sprungziel sound_ended definiert, ruft POE beim Abschluss des Soundprozesses die ab Zeile 48 definierte Funktion auf, die die Wheel-Referenz wieder löscht, damit POE den Sensenmann vorbeischickt. Als weiteres Gotcha rĂ€umt POE::Wheel::Run terminierte Kindsprozesse nicht automatisch auf, sodass sie als Zombies im Unix-System herumlungern. Zeile 68 definiert deswegen einen SIGCHLD-Handler, der den Elternprozess dazu veranlasst, ein wait() auf das terminierte Kind abzusetzen und damit dessen Zombie-Werdung aktiv zu verhindern.

Sobald ein Client sich mit Port 8080 der Serverkomponente POE::Component::Server::TCP verbindet, springt deren Zustandsautomat den Zustand ClientConnected an. In $_[HEAP]{client} liegt dort ein Client-Objekt vor, ĂŒber dessen put-Methode der Server Nachrichten an den Client schicken kann. In ClientConnected teilt der Server dem andockenden Client die verfĂŒgbaren Sounddateien mit und begrĂŒĂŸt ihn anschließend mit einem freundlichen ``Ready when you are''.

Wann immer der Client etwas an den Server schickt, springt letzerer die dem Zustand ClientInput zugeordnete Subroutine an. Die eingegangene Nachricht liegt dort in $_[ARG0], einer dieser POE-typischen Felder des Argumenten-Array @_. Damit der Client keinen Schabernack mit dem Server treibt und bösartige Unix-Kommandos statt der erwarteten Sounddatei schickt, prĂŒft Zeile 29, ob außer den in Dateien ĂŒblichen Zeichen auch noch andere vorliegen und verwirft in diesem Fall den Request sofort mit einer Fehlermeldung.

Schickt der Client hingegen das Zeichen ``q'', möchte dieser die Session beenden und der Server springt den Zustand shutdown an, der die aktuelle Client-Verbindung löst, aber den Server weiterlaufen lĂ€sst. Schickt der Cient dagegen tatsĂ€chlich den Namen einer existierenden Sounddatei, spielt ihn die Funktion sound_play ab und gibt einen Statusstring zurĂŒck, den der Server dem Client zur BestĂ€tigung mit put() schickt.

Ende des Tunnels

Auf der anderen Seite des Tunnels ĂŒberwacht das POE-Skript aus Listing boom-sender das access.log-File des Webservers. Es lĂ€uft auf dem Rechner des Hostingservice, host.xyz-hosting.com und nutzt die TCP-Client-Komponente des POE-Frameworks, um mit dem Server Kontakt zu halten.

Diese bietet von Haus aus unter anderem die Events ServerInput und ConnectError an, deren Callbacks sie angespringt, falls der Server Text sendet oder die Verbindung fehlschlĂ€gt. Zum Senden definiert boom-sender ĂŒber InlineStates den Zustand send, der eine ihm ĂŒbergebene Nachricht mit put() an den Server schickt.

Die ab Zeile 29 definierte LogfileĂŒberwachersession bekommt mittels eines FollowTail-Wheels aus der POE-Werkzeugkiste mit, wenn der Webserver eine neue Zeile an die in Zeile 35 definierte Logdatei anhĂ€ngt. Auch hier ist es wichtig, dass sie eine Referenz auf das Wheel beibehĂ€lt, denn sonst wĂŒrde POE letzteres rĂŒcksichtslos abrĂ€umen, sobald der _start-Callback beendet wĂ€re. Die Referenz bleibt im sogenannten Heap der POE-Session unter dem Eintrag tail bestehen, solange die Session lĂ€uft, also bis zum Ende von boom-sender.

Produktionssysteme rotieren ihre Logdateien oft einmal am Tag, und FollowTail ist darauf vorbereitet und springt in diesem Fall den dem ResetEvent zugeordneten got_log_rollover-Callback an. Dieser macht damit nichts Großartiges, sondern schreibt nur eine Debug-Meldung, damit der User Bescheid weiß.

Jedesmal, wenn das Wheel eine neu angehĂ€ngte Logzeile findet, springt es den Status got_log_line an und fĂŒhrt den entsprechenden Callback aus. Dort analysiert es neue Zeilen, die im Format

    67.195.37.108 - - [01/Sep/2008:17:25:20 -0700] "GET /1/p3.html HTTP/1.0" 200 8678 "-" "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.4) Gecko/20080721 BonEcho/2.0.0.4"

vorliegen mit dem Modul ApacheLog::Parser vom CPAN. Dessen exportierte Funktion parse_line_to_hash() liefert einen Hash zurĂŒck, der unter dem Eintrag file die per HTTP angeforderte Datei enthĂ€lt.

Da der TCP-Client in Zeile 12 mit dem Alias-Namen "boom" definiert wurde, kann die in Zeile 29 definierte Session mit dem FollowTail-Wheel dem TCP-Server die zu sendende Sounddatei mit

      POE::Kernel->post("boom", "send", 
              file2sound($file));

mitteilen. Hier kommunizieren zwei verschiedene Sessions miteinander, deswegen wird das Ereignis nicht mit yield() eingeleitet sondern mit post() und dem Namen der empfangenden Session. Der Event "send" der Session boom bekommt dann in ARG0 den Namen der .wav-Datei ĂŒbermittelt, die es in Zeile 21 mit put() an den TCP-Client ĂŒbergibt, der diese wiederum als Kommando an den Soundserver schickt. Nicht direkt zwar, aber an Port 8080 des localhost, der wiederum ĂŒber den Tunnel mit Port 8080 des Soundservers in Verbindung steht.

Keine Kakophonie

Löste jeder neue Eintrag im access.log einen Ton aus, entstĂŒnde bei einer Webseite mit 20 Bildern, die der Browser in kĂŒrzesten AbstĂ€nden einholt, eine nervige Kakophonie von ĂŒberlagerten GerĂ€uschen. Deswegen filtert boom-sender die Ausgabe des Access Log und ĂŒbermittelt nur bei aufgerufenen Hauptseiten, vergrĂ¶ĂŸerten Bildern und ForumsaktivitĂ€t Anfragen an den Soundserver boom-receiver. Die ab Zeile 57 definierte Funktion file2sound() nimmt den vom Browser angeforderten Dateipfad (z.B. /index.html) entgegen und gibt den Namen der abzuspielenden Sounddatei zurĂŒck. Hierzu macht er ein paar Annahmen, die eine Installation eventuell angepassen muss, wie zum Beispiel dass ein auf ``/'' endender Pfad eine index.html-Datei herausgibt.

Installation

Das Skript boom-sender wird auf dem Hostingrechner installiert, die notwendigen Perl-Module gibt's alle auf dem CPAN. AccessLog::Parser erfordert noch die Module Getopt::Helpful, Date::Piece, File::Fu und Class::Accessor::Classy. StrĂ€ubt sich der Hostingprovider, die Installation mit dem angebotenen perl durchzufĂŒhren, geht das auch mittels eines zusĂ€tzlichen Modulverzeichnisses in einem beschreibbaren Bereich auf dem Hostingrechner und der Direktive

    use lib "/home/name/perl-modules";

im Perl-Skript. Alternativ geht auch eine eigene Perlinstallation, ebenfalls in einem fĂŒr den Hostingkunden beschreibbaren Bereich. Details hierzu finden sich in [2]. Und als weitere Alternative kommt das PAR-Toolkit in Betracht, mit dem sich Ă€hnlich wie mit Javas JAR-Dateien Modularchive und sogar ausfĂŒhrbare Executables ohne Installationssorgen packen lassen ([3]).

Die in der Funktion file2sound() in boom-sender vorgenommenen Zuweisungen von URL-Dateien zu Soundfiles sollte noch an die lokalen VerhÀltnisse angepasst werden. Nur Sounddateien, die tatsÀchlich vom Soundserver angeboten werden, sollten hier Verwendung finden.

Auf dem Soundserver werden die Sounddateien im Verzeichnis $SOUND_DIR installiert. Eine gute Auswahl an kurzen GerÀuschen bietet das Verzeichnis /usr/share/sounds/purple. Dort legt der IM-Client Pidgin (ehemals Gaim) die Daten von GerÀuschen ab, die das Program von sich gibt, wenn sich Buddies an- oder abmelden oder Instant Messages eintrudeln oder abgesandt werden.

Nach dem Starten des Soundservers boom-receiver, einem kurzen Test mit dem telnet-Client und dem Einrichten des oben beschriebenen ssh-Tunnels, darf dann der LogdateiĂŒberwacher boom-sender auf dem Hostingrechner gestartet werden und das Konzert beginnt.

Listing 2: boom-sender

    01 #!/home/mschilli/PERL/bin/perl -w
    02 use strict;
    03 use POE;
    04 use POE::Wheel::FollowTail;
    05 use POE::Component::Client::TCP;
    06 use ApacheLog::Parser 
    07                     qw(parse_line_to_hash);
    08 use Log::Log4perl qw(:easy);
    09 Log::Log4perl->easy_init($DEBUG);
    10 
    11 POE::Component::Client::TCP->new(
    12   Alias         => 'boom',
    13   RemoteAddress => 'localhost',
    14   RemotePort    => 8080,
    15   ServerInput   => sub {
    16       DEBUG "Server says: $_[ARG0]";
    17   },
    18   InlineStates => {
    19     send => sub { 
    20       DEBUG "Sending [$_[ARG0]] to server";
    21       $_[HEAP]->{server}->put($_[ARG0]);
    22     },
    23   },
    24   ConnectError => sub {
    25       LOGDIE "Cannot connect to server";
    26   }
    27 ); 
    28 
    29 POE::Session->create(
    30   inline_states => {
    31     _start => sub {
    32       $_[HEAP]->{tail} = 
    33         POE::Wheel::FollowTail->new(
    34           Filename => 
    35             "/var/log/apache2/access.log",
    36           InputEvent => "got_log_line",
    37           ResetEvent => "got_log_rollover",
    38       );
    39     },
    40     got_log_line => sub {
    41       my %fields = 
    42                parse_line_to_hash $_[ARG0];
    43       my $file = $fields{ file };
    44       if(my $sound = file2sound($file)) {
    45         POE::Kernel->post("boom", "send", 
    46              $sound);
    47       }
    48     },
    49     got_log_rollover => sub {
    50       DEBUG "Log rolled over.";
    51     },
    52   }
    53 );
    54 
    55 POE::Kernel->run();
    56 exit;
    57 
    58 ###########################################
    59 sub file2sound {
    60 ###########################################
    61     $_ = $_[0];
    62 
    63     DEBUG "Got $_";
    64 
    65     s#/$#/index.html#;
    66 
    67     m#/index.html$# and
    68         return "article-page.wav";
    69 
    70     m#/posting.php# and 
    71         return "forum-post.wav";
    72 
    73     m#/viewforum.php# and 
    74         return "forum-page.wav";
    75 
    76     m#/images/.*html# and 
    77         return "image.wav";
    78 
    79     return "";
    80 }

Erweiterungen

ZusĂ€tzlich zu den angeforderten URL-Pfaden könnte der Soundserver auch noch bei fehlschlagenden Requests Töne abspielen. Der Webserver legt auch den Returncode jedes Requests in access.log ab und der Logparser stellt ihn mit dem Hasheintrag $fields{code} zur VerfĂŒgung. FlatulenzgerĂ€usche oder eine Explosion wĂ€ren die geeignete Hintergrundmusik fĂŒr derartige Fehlleistungen und zögen die Aufmerksamkeit des Systemadministrators auf sich.

Infos

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

[2]
Michael Schilli, ``Da hab ich dann was eigenes'', Linux-Magazin, November 1999 (http://perlmeister.com/snapshots/199911/index.html) (Auf eurer Webseite leider im Zuge der Neuformatiertung verschlampt).

[3]
Michael Schilli, ``Gepackte Koffer'', Linux-Magazin, April 2004, http://www.linux-magazin.de/heft_abo/ausgaben/2004/09/gepackte_koffer

[4]
Jim Clark, ``Netscape Time'', http://www.amazon.com/gp/reader/0312263619/ref=sib_dp_ptu# (nach ``cannon shot'' suchen).

[5]
``Advanced Perl Programming (2nd edition)'', Simon Cozens, O'Reilly, 2005.

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.