=head1 NAME Debuggen mit Coroutinen =head1 Einführung Im Laufe der Jahre habe ich eine ganz eigene Methode zum Debuggen meiner Programme entwickelt. Diese Methode kombiniert einige Konzepte (interkative Shell in jedem Programm, Coroutinen), die mehr als reines Debugging ermöglichen. Diese Methode möchte ich hiermit vorstellen, vielleicht bringt sie den einen oder andreen auf Gedanken oder stellt sich gar als nützlich heraus... =head1 "Traditionelle" Debugging-Methoden Nun, es gibt den Perl-Debugger; über diesen wird tatsächlich viel erzählt und geschrieben, und ich nehme an, er wird wirklich häufig benutzt. Aber aus welchen Gründen auch immer, ich konnte mich mit ihm (anders als mit gdb) nie wirklich anfreunden: die einzige Funktion, die ich mit einiger Regelmäßigkeit benutze, ist die Trace-Funktion. Das kann ich sogar als "Fest im Gehirn eingebautes Makro" sofort tippen: "perl -d xxx, dann t, dann c", andere Debugger-Befehle kenne ich nicht. Diese Trace-Funktion hat aber viele Nachteile: sie macht das Programm sehr langsam, sie funktioniert nur, während man im Debugger ist, und häufig hängt mein Programm aus nicht nachvollziehbaren Gründen, und die Ausgabe ist zu umfangreich. Die Hauptnachteile sind aber, daß man den Debugger nicht ein- oder ausschalten kann und daß er interaktiv arbeitet. Die Mehrheit meiner Programme sind langlebige Hintergrundprogramme. Diese sind immerhin so gut getestet, daß sie in Produktion selten Probleme entwickeln, aber manchmal kommt das natürlich vor. Das erklärt wahrscheinlich, weshalb ich den Perl-Debugger nicht benutze: man kann den Perl-Debugger (meines Wissens) nicht an bestehende Prozesse attachen, man kann die Programme nicht dauerhaft unter dem Debugger starten und man hat im Allgemeinen auch keinen interkativen Zugang. Startet man das Programm neu (z.B. nach Einbau einiger warn-statements oder um es unter dem Debugger laufen zu lassen) tritt das Problem häufig nicht mehr auf. Hinzu kommt, daß viele meiner Programme stark Ereignisgesteuert arbeiten: Hält man das Programm an, gibt es unerwünschte Timeouts; erstellt man einen Trace, springt dieser wild zwischen Programmteilen hin- und her, die nichts miteinander zu tun haben. Als einzige Möglichkeit (die auch häufig implementiert wird), bleibt häufig nur, extrem viel mitzuloggen, so daß man im Fehlerfall zumindest auf eine Art Trace zurückgreifen kann. Natürlich sind die schweren Fehler selten dort, wo man gerade viel mitloggt. =head1 Die Entwicklung eines anderen Ansatzes =head2 Die Anfänge: ein Webserver Um das Jahr 2000 herum schrieb ich einen Web-Server, der vollkommen Event-gesteuert war (buchstäblich: er benutzte das Event-Modul dazu). Weil es so einfach war, bekam er bald eine interaktive Shell verpasst: sub shell { my $fh = shift; while (defined (print $fh "cmd> "), $_ = <$fh>) { s/\015?\012$//; # bearbeite Kommandos } } my $port = new Coro::Socket LocalPort => $CMDSHELL_PORT, ReuseAddr => 1, Listen => 1, or die "unable to bind cmdshell port: $!"; push @listen_sockets, $port; async { while () { async \&shell, scalar $port->accept; } }; Der Code benutzt Coroutinen, sollte aber einfach verständlich sein: C ist das Pendant zu C, bei dem Aufrufe wie C die anderen coroutinen nicht blockieren; die Funktion C startet eine neue ("asynchrone") Coroutine für jedes neue Verbindung, und die C-Funktion schließlich liest in einer Schleife Befehle von der Socket und führt sie aus. Ursprünglich dazu gedacht, eine Liste von aktiven Verbindungen zu erhalten bzw. bisweilen IP-Adressen zu blockieren, hatte ich irgendwann eine offensichtliche aber doch nicht so offensichtliche idee: } elsif ($cmd eq "print") { my @res = eval $_; print $fh "eval: $@\n" if $@; print $fh "RES = ", (join " : ", @res), "\n"; Mit dem "print"-Kommando (eigentlich wäre "eval" ein besserer Name) lassen sich erstaunlich viele Dinge erledigen: Erlaube 200 gleichzeitige Verbindungen mehr: print $conn::connections->adjust (200) Setze die Downloadrate auf 1MB/s: print $conn::tbf_top->{rate} = 1e6 Erlaube 20 gleichzeitige Downloads mehr: print $conn::queue_file->{slots} += 20 Das erste Beispiel benutzt die korrekte "API" zum anpassen einer Semaphore, dei beiden letzteren Beispiele sind eigentlich "böse Hacks" weil der Code diese Möglichkeit der Anpassung nicht explizit unterstützt, sie funktionieren aber trotzdem. Auf diese Weise läßt sich bisweilen auch debuggen: wenn z.B. eine Verbindung hängt, kann man versuchen, sie zu finden und dann versuchen, herauszufinden, woran es liegt, indem man sich verschiedene globale Variablen anschaut oder kleine "Suchprogramme" mit C ausführt. Das kann eine extreme Hilfe sein, ist aber immer noch recht umständlich. =head2 Perl ist besser als jede Shell: Deliantra Der Deliantra-Server (ein MORPG) ist ebenfalls vollkommen Ereignisgesteuert und besitzt ebenfalls eine Shell, die (fast) ohne Coroutinen auskommt: sub tcp_serve($) { my ($fh) = @_; binmode $fh, ":raw:perlio:utf8"; print $fh "\n> "; my $iow; $iow = EV::io $fh, EV::READ, sub { if (defined (my $cmd = <$fh>)) { $cmd =~ s/\s+$//; if ($cmd =~ /^\s*exit\b/i) { print $fh "will not exit() server.\n"; # andere befehle } }; } our $LISTENER; # now a shell listening on a tcp-port - let the firewall decide access rights if ($cf::CFG{perl_shell}) { if (my $listen = new IO::Socket::INET LocalAddr => $cf::CFG{perl_shell}, Listen => 1, ReuseAddr => 1, Blocking => 0) { $LISTENER = EV::io $listen, EV::READ, sub { tcp_serve $listen->accept }; } } Deliantra benutzt EV als Event-Bibliothek, ansonsteb habe ich aus meinen früheren Versuchen gelernt und implementiere keine Kommandos mehr direkt, sondern erlaube direkt die Eingabe von Perl-Ausdrücken: ... } else { my $sub = sub { package cf; select $fh; # compile first, then execute, as Coro does not support switching in eval string my $cb = eval "sub { $cmd \n}"; my $t1 = Time::HiRes::time; my @res = $@ ? () : eval { $cb->() }; my $t2 = Time::HiRes::time; print "\n", "command: '$cmd'\n", "execution time: ", $t2 - $t1, "\n"; warn "evaluation error: $@" if $@; print "evaluation error: $@\n" if $@; print "result:\n", cf::dumpval @res > 1 ? \@res : $res[0] if @res; print "\n> "; select STDOUT; }; if ($cmd =~ s/\s*&$//) { cf::async { $Coro::current->desc ($cmd); $sub->() }; } else { $sub->(); } Die Befehlsausführung ist weitaus komplexer: Zuerst wird die eingegebene Zeile kompilziert und dann evaluiert. Danach wird das Ergebnis, etwaige Laufzeitfehler und die Ausführungszeit ausgegeben. Da man als Administrator manchmal Befehle im "Hauptprogramm" (die Coroutine, die die Event-Schleife ausführt) ausführen muss, der Server aber all 120ms ein update generieren muss, muss man lang dauernde Befehle in den "Hintergrund" (eine weitere Coroutine) schieben, was mit einem angehängten "&" geschieht. Eine Beispielsession sieht so aus: # dmshell Welcome! ext::help::reload & ext::books::reload & ext::map_tags::reload & ext::map_world::reload & print JSON::XS->new->pretty->encode({cf::mallinfo}) > ext::map_world::reload & > command: 'ext::map_world::reload' execution time: 0.0744819641113281 > ext::map_tags::reload & > command: 'ext::map_tags::reload' execution time: 1.14403510093689 > $cf::PLAYER-{schmorp} command: '$cf::PLAYER-{schmorp}' execution time: 5.00679016113281e-06 evaluation error: Bareword "schmorp" not allowed while "strict subs" in use at (eval 180) line 1, line 6. > $cf::PLAYER{schmorp} command: '$cf::PLAYER{schmorp}' execution time: 1.09672546386719e-05 result: bless( { log_told => {}, last_save => "32009728.5217925", rent => { last_online_check => "1200189659", last_offline_check => "1200189659", balance => "-0.737118053715676", apartment => { "/brest/apartments/brest_town_house" => undef, "/scorn/apartment/apartments" => undef } }, hintmode => 0, npc_dialog_active => {} }, 'cf::player::wrap' ) > > "schmorp"->cf::player::find->ob->stats->hp command: '"schmorp"->cf::player::find->ob->stats->hp' execution time: 4.79221343994141e-05 result: 520 und so weiter... Das ist natürlich eine große Hilfe beim Administrieren oder Debuggen, weil man sich die aktuellen Daten die im server geladen sind direkt ansehen kann. Sehr angenehm ist es auch, direkt im Spielbetrieb Bugs zu fixen, indem man einzelne Routinen direkt überschreibt: # cat /tmp/bugfix package cf; sub _can_merge { # neue merge-logik, vielleicht mit printf-style-debugging } 1 # dmshell > do "/tmp/bugfix" So kann man im laufenden Betrieb schon sehr angenehm am Server arbeiten, aber man muss sich jedesmal eine Shell ausdenken, sie implementieren, und kann dennoch nur herumstochern. Doch mit Coroutinen kann mehr wesentlich mehr... =head1 Coroutinen Mit Perl hat man prinzipiell zwei Methoden der "parallelverarbeitung": Coroutinen (teilen sich einen gemeinsamen Adressraum, laufen aber nicht wirklich parallel) und Prozesse (haben getrennte Adressräume, laufen aber parallel (mit entsprechender Hardware)). Threads (gemeinsamer Adressraum und echte Parallelität) werden von Perl nicht angeboten. Gegenüber Prozessen hat man den Vorteil extrem einfacher Kommunikation zwischen den einzelnen Instanzen, beispielsweise implementiert der schon genannte Webserver einen Schutz gegen segmentierte Downloads, indem er die gerade heruntergeladenen URLs pro Klient in einem globale Hash speichert: if ($DOWNLOADS{$url}{$clientid} >= 4) { # abort, zu viele Verbindungen return; } ++$DOWNLOADS{$url}{$clientid}; =head1 Coro::Debug =head1 Autor Marc Lehmann .