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.
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.
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. |
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!
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 }
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.
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.
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.
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.
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 }
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.
Michael Schilliarbeitet 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. |