| 1 |
=head1 NAME |
| 2 |
|
| 3 |
Debuggen mit Coroutinen |
| 4 |
|
| 5 |
=head1 Einführung |
| 6 |
|
| 7 |
Im Laufe der Jahre habe ich eine ganz eigene Methode zum Debuggen |
| 8 |
meiner Programme entwickelt. Diese Methode kombiniert einige Konzepte |
| 9 |
(interkative Shell in jedem Programm, Coroutinen), die mehr als reines |
| 10 |
Debugging ermöglichen. |
| 11 |
|
| 12 |
Diese Methode möchte ich hiermit vorstellen, vielleicht bringt sie |
| 13 |
den einen oder andreen auf Gedanken oder stellt sich gar als nützlich |
| 14 |
heraus... |
| 15 |
|
| 16 |
=head1 "Traditionelle" Debugging-Methoden |
| 17 |
|
| 18 |
Nun, es gibt den Perl-Debugger; über diesen wird tatsächlich viel |
| 19 |
erzählt und geschrieben, und ich nehme an, er wird wirklich häufig |
| 20 |
benutzt. Aber aus welchen Gründen auch immer, ich konnte mich mit ihm |
| 21 |
(anders als mit gdb) nie wirklich anfreunden: die einzige Funktion, die |
| 22 |
ich mit einiger Regelmäßigkeit benutze, ist die Trace-Funktion. Das kann |
| 23 |
ich sogar als "Fest im Gehirn eingebautes Makro" sofort tippen: "perl -d |
| 24 |
xxx, dann t, dann c", andere Debugger-Befehle kenne ich nicht. |
| 25 |
|
| 26 |
Diese Trace-Funktion hat aber viele Nachteile: sie macht das Programm sehr |
| 27 |
langsam, sie funktioniert nur, während man im Debugger ist, und häufig |
| 28 |
hängt mein Programm aus nicht nachvollziehbaren Gründen, und die Ausgabe |
| 29 |
ist zu umfangreich. Die Hauptnachteile sind aber, daß man den Debugger |
| 30 |
nicht ein- oder ausschalten kann und daß er interaktiv arbeitet. |
| 31 |
|
| 32 |
Die Mehrheit meiner Programme sind langlebige Hintergrundprogramme. Diese |
| 33 |
sind immerhin so gut getestet, daß sie in Produktion selten Probleme |
| 34 |
entwickeln, aber manchmal kommt das natürlich vor. Das erklärt |
| 35 |
wahrscheinlich, weshalb ich den Perl-Debugger nicht benutze: man kann den |
| 36 |
Perl-Debugger (meines Wissens) nicht an bestehende Prozesse attachen, man |
| 37 |
kann die Programme nicht dauerhaft unter dem Debugger starten und man hat |
| 38 |
im Allgemeinen auch keinen interkativen Zugang. |
| 39 |
|
| 40 |
Startet man das Programm neu (z.B. nach Einbau einiger warn-statements |
| 41 |
oder um es unter dem Debugger laufen zu lassen) tritt das Problem häufig |
| 42 |
nicht mehr auf. |
| 43 |
|
| 44 |
Hinzu kommt, daß viele meiner Programme stark Ereignisgesteuert |
| 45 |
arbeiten: Hält man das Programm an, gibt es unerwünschte Timeouts; |
| 46 |
erstellt man einen Trace, springt dieser wild zwischen Programmteilen hin- |
| 47 |
und her, die nichts miteinander zu tun haben. |
| 48 |
|
| 49 |
Als einzige Möglichkeit (die auch häufig implementiert wird), bleibt |
| 50 |
häufig nur, extrem viel mitzuloggen, so daß man im Fehlerfall zumindest |
| 51 |
auf eine Art Trace zurückgreifen kann. Natürlich sind die schweren |
| 52 |
Fehler selten dort, wo man gerade viel mitloggt. |
| 53 |
|
| 54 |
=head1 Die Entwicklung eines anderen Ansatzes |
| 55 |
|
| 56 |
=head2 Die Anfänge: ein Webserver |
| 57 |
|
| 58 |
Um das Jahr 2000 herum schrieb ich einen Web-Server, der vollkommen |
| 59 |
Event-gesteuert war (buchstäblich: er benutzte das Event-Modul dazu). |
| 60 |
|
| 61 |
Weil es so einfach war, bekam er bald eine interaktive Shell verpasst: |
| 62 |
|
| 63 |
sub shell { |
| 64 |
my $fh = shift; |
| 65 |
|
| 66 |
while (defined (print $fh "cmd> "), $_ = <$fh>) { |
| 67 |
s/\015?\012$//; |
| 68 |
|
| 69 |
# bearbeite Kommandos |
| 70 |
} |
| 71 |
} |
| 72 |
|
| 73 |
my $port = new Coro::Socket |
| 74 |
LocalPort => $CMDSHELL_PORT, |
| 75 |
ReuseAddr => 1, |
| 76 |
Listen => 1, |
| 77 |
or die "unable to bind cmdshell port: $!"; |
| 78 |
|
| 79 |
push @listen_sockets, $port; |
| 80 |
|
| 81 |
async { |
| 82 |
while () { |
| 83 |
async \&shell, scalar $port->accept; |
| 84 |
} |
| 85 |
}; |
| 86 |
|
| 87 |
Der Code benutzt Coroutinen, sollte aber einfach verständlich |
| 88 |
sein: C<Coro::Socket> ist das Pendant zu C<IO::Socket>, bei dem Aufrufe |
| 89 |
wie C<accept> die anderen coroutinen nicht blockieren; die Funktion |
| 90 |
C<async> startet eine neue ("asynchrone") Coroutine für jedes neue |
| 91 |
Verbindung, und die C<shell>-Funktion schließlich liest in einer Schleife |
| 92 |
Befehle von der Socket und führt sie aus. |
| 93 |
|
| 94 |
Ursprünglich dazu gedacht, eine Liste von aktiven Verbindungen zu |
| 95 |
erhalten bzw. bisweilen IP-Adressen zu blockieren, hatte ich irgendwann |
| 96 |
eine offensichtliche aber doch nicht so offensichtliche idee: |
| 97 |
|
| 98 |
} elsif ($cmd eq "print") { |
| 99 |
my @res = eval $_; |
| 100 |
print $fh "eval: $@\n" if $@; |
| 101 |
print $fh "RES = ", (join " : ", @res), "\n"; |
| 102 |
|
| 103 |
Mit dem "print"-Kommando (eigentlich wäre "eval" ein besserer Name) |
| 104 |
lassen sich erstaunlich viele Dinge erledigen: |
| 105 |
|
| 106 |
Erlaube 200 gleichzeitige Verbindungen mehr: |
| 107 |
|
| 108 |
print $conn::connections->adjust (200) |
| 109 |
|
| 110 |
Setze die Downloadrate auf 1MB/s: |
| 111 |
|
| 112 |
print $conn::tbf_top->{rate} = 1e6 |
| 113 |
|
| 114 |
Erlaube 20 gleichzeitige Downloads mehr: |
| 115 |
|
| 116 |
print $conn::queue_file->{slots} += 20 |
| 117 |
|
| 118 |
Das erste Beispiel benutzt die korrekte "API" zum anpassen einer |
| 119 |
Semaphore, dei beiden letzteren Beispiele sind eigentlich "böse |
| 120 |
Hacks" weil der Code diese Möglichkeit der Anpassung nicht explizit |
| 121 |
unterstützt, sie funktionieren aber trotzdem. |
| 122 |
|
| 123 |
Auf diese Weise läßt sich bisweilen auch debuggen: wenn z.B. eine |
| 124 |
Verbindung hängt, kann man versuchen, sie zu finden und dann versuchen, |
| 125 |
herauszufinden, woran es liegt, indem man sich verschiedene globale |
| 126 |
Variablen anschaut oder kleine "Suchprogramme" mit C<print do "dateiname"> |
| 127 |
ausführt. |
| 128 |
|
| 129 |
Das kann eine extreme Hilfe sein, ist aber immer noch recht umständlich. |
| 130 |
|
| 131 |
=head2 Perl ist besser als jede Shell: Deliantra |
| 132 |
|
| 133 |
Der Deliantra-Server (ein MORPG) ist ebenfalls vollkommen |
| 134 |
Ereignisgesteuert und besitzt ebenfalls eine Shell, die (fast) ohne |
| 135 |
Coroutinen auskommt: |
| 136 |
|
| 137 |
sub tcp_serve($) { |
| 138 |
my ($fh) = @_; |
| 139 |
|
| 140 |
binmode $fh, ":raw:perlio:utf8"; |
| 141 |
print $fh "\n> "; |
| 142 |
|
| 143 |
my $iow; $iow = EV::io $fh, EV::READ, sub { |
| 144 |
if (defined (my $cmd = <$fh>)) { |
| 145 |
$cmd =~ s/\s+$//; |
| 146 |
|
| 147 |
if ($cmd =~ /^\s*exit\b/i) { |
| 148 |
print $fh "will not exit() server.\n"; |
| 149 |
|
| 150 |
# andere befehle |
| 151 |
} |
| 152 |
}; |
| 153 |
} |
| 154 |
|
| 155 |
our $LISTENER; |
| 156 |
|
| 157 |
# now a shell listening on a tcp-port - let the firewall decide access rights |
| 158 |
if ($cf::CFG{perl_shell}) { |
| 159 |
if (my $listen = new IO::Socket::INET LocalAddr => $cf::CFG{perl_shell}, Listen => 1, ReuseAddr => 1, Blocking => 0) { |
| 160 |
$LISTENER = EV::io $listen, EV::READ, sub { tcp_serve $listen->accept }; |
| 161 |
} |
| 162 |
} |
| 163 |
|
| 164 |
Deliantra benutzt EV als Event-Bibliothek, ansonsteb habe ich aus meinen |
| 165 |
früheren Versuchen gelernt und implementiere keine Kommandos mehr direkt, |
| 166 |
sondern erlaube direkt die Eingabe von Perl-Ausdrücken: |
| 167 |
|
| 168 |
... |
| 169 |
} else { |
| 170 |
my $sub = sub { |
| 171 |
package cf; |
| 172 |
select $fh; |
| 173 |
|
| 174 |
# compile first, then execute, as Coro does not support switching in eval string |
| 175 |
my $cb = eval "sub { $cmd \n}"; |
| 176 |
|
| 177 |
my $t1 = Time::HiRes::time; |
| 178 |
my @res = $@ ? () : eval { $cb->() }; |
| 179 |
my $t2 = Time::HiRes::time; |
| 180 |
|
| 181 |
print "\n", |
| 182 |
"command: '$cmd'\n", |
| 183 |
"execution time: ", $t2 - $t1, "\n"; |
| 184 |
warn "evaluation error: $@" if $@; |
| 185 |
print "evaluation error: $@\n" if $@; |
| 186 |
print "result:\n", cf::dumpval @res > 1 ? \@res : $res[0] if @res; |
| 187 |
print "\n> "; |
| 188 |
|
| 189 |
select STDOUT; |
| 190 |
}; |
| 191 |
|
| 192 |
if ($cmd =~ s/\s*&$//) { |
| 193 |
cf::async { |
| 194 |
$Coro::current->desc ($cmd); |
| 195 |
$sub->() |
| 196 |
}; |
| 197 |
} else { |
| 198 |
$sub->(); |
| 199 |
} |
| 200 |
|
| 201 |
Die Befehlsausführung ist weitaus komplexer: Zuerst wird die eingegebene |
| 202 |
Zeile kompilziert und dann evaluiert. Danach wird das Ergebnis, etwaige |
| 203 |
Laufzeitfehler und die Ausführungszeit ausgegeben. |
| 204 |
|
| 205 |
Da man als Administrator manchmal Befehle im "Hauptprogramm" (die |
| 206 |
Coroutine, die die Event-Schleife ausführt) ausführen muss, der Server |
| 207 |
aber all 120ms ein update generieren muss, muss man lang dauernde Befehle |
| 208 |
in den "Hintergrund" (eine weitere Coroutine) schieben, was mit einem |
| 209 |
angehängten "&" geschieht. |
| 210 |
|
| 211 |
Eine Beispielsession sieht so aus: |
| 212 |
|
| 213 |
# dmshell |
| 214 |
Welcome! |
| 215 |
|
| 216 |
ext::help::reload & |
| 217 |
ext::books::reload & |
| 218 |
ext::map_tags::reload & |
| 219 |
ext::map_world::reload & |
| 220 |
print JSON::XS->new->pretty->encode({cf::mallinfo}) |
| 221 |
|
| 222 |
> ext::map_world::reload & |
| 223 |
|
| 224 |
> |
| 225 |
command: 'ext::map_world::reload' |
| 226 |
execution time: 0.0744819641113281 |
| 227 |
|
| 228 |
> ext::map_tags::reload & |
| 229 |
|
| 230 |
> |
| 231 |
command: 'ext::map_tags::reload' |
| 232 |
execution time: 1.14403510093689 |
| 233 |
|
| 234 |
> $cf::PLAYER-{schmorp} |
| 235 |
|
| 236 |
command: '$cf::PLAYER-{schmorp}' |
| 237 |
execution time: 5.00679016113281e-06 |
| 238 |
evaluation error: Bareword "schmorp" not allowed while "strict subs" in use at (eval 180) line 1, <GEN63> line 6. |
| 239 |
|
| 240 |
> $cf::PLAYER{schmorp} |
| 241 |
|
| 242 |
command: '$cf::PLAYER{schmorp}' |
| 243 |
execution time: 1.09672546386719e-05 |
| 244 |
result: |
| 245 |
bless( { |
| 246 |
log_told => {}, |
| 247 |
last_save => "32009728.5217925", |
| 248 |
rent => { |
| 249 |
last_online_check => "1200189659", |
| 250 |
last_offline_check => "1200189659", |
| 251 |
balance => "-0.737118053715676", |
| 252 |
apartment => { |
| 253 |
"/brest/apartments/brest_town_house" => undef, |
| 254 |
"/scorn/apartment/apartments" => undef |
| 255 |
} |
| 256 |
}, |
| 257 |
hintmode => 0, |
| 258 |
npc_dialog_active => {} |
| 259 |
}, 'cf::player::wrap' ) |
| 260 |
|
| 261 |
> |
| 262 |
> "schmorp"->cf::player::find->ob->stats->hp |
| 263 |
|
| 264 |
command: '"schmorp"->cf::player::find->ob->stats->hp' |
| 265 |
execution time: 4.79221343994141e-05 |
| 266 |
result: |
| 267 |
520 |
| 268 |
|
| 269 |
und so weiter... Das ist natürlich eine große Hilfe beim Administrieren |
| 270 |
oder Debuggen, weil man sich die aktuellen Daten die im server geladen |
| 271 |
sind direkt ansehen kann. |
| 272 |
|
| 273 |
Sehr angenehm ist es auch, direkt im Spielbetrieb Bugs zu fixen, indem man |
| 274 |
einzelne Routinen direkt überschreibt: |
| 275 |
|
| 276 |
# cat /tmp/bugfix |
| 277 |
package cf; |
| 278 |
sub _can_merge { |
| 279 |
# neue merge-logik, vielleicht mit printf-style-debugging |
| 280 |
} |
| 281 |
1 |
| 282 |
|
| 283 |
# dmshell |
| 284 |
> do "/tmp/bugfix" |
| 285 |
|
| 286 |
So kann man im laufenden Betrieb schon sehr angenehm am Server arbeiten, |
| 287 |
aber man muss sich jedesmal eine Shell ausdenken, sie implementieren, und |
| 288 |
kann dennoch nur herumstochern. |
| 289 |
|
| 290 |
Doch mit Coroutinen kann mehr wesentlich mehr... |
| 291 |
|
| 292 |
|
| 293 |
=head1 Coroutinen |
| 294 |
|
| 295 |
Mit Perl hat man prinzipiell zwei Methoden der |
| 296 |
"parallelverarbeitung": Coroutinen (teilen sich einen gemeinsamen |
| 297 |
Adressraum, laufen aber nicht wirklich parallel) und Prozesse (haben |
| 298 |
getrennte Adressräume, laufen aber parallel (mit entsprechender |
| 299 |
Hardware)). Threads (gemeinsamer Adressraum und echte Parallelität) |
| 300 |
werden von Perl nicht angeboten. |
| 301 |
|
| 302 |
Gegenüber Prozessen hat man den Vorteil extrem einfacher Kommunikation zwischen den einzelnen Instanzen, beispielsweise |
| 303 |
implementiert der schon genannte Webserver einen Schutz gegen segmentierte Downloads, indem er die gerade heruntergeladenen URLs |
| 304 |
pro Klient in einem globale Hash speichert: |
| 305 |
|
| 306 |
if ($DOWNLOADS{$url}{$clientid} >= 4) { |
| 307 |
# abort, zu viele Verbindungen |
| 308 |
return; |
| 309 |
} |
| 310 |
|
| 311 |
++$DOWNLOADS{$url}{$clientid}; |
| 312 |
|
| 313 |
=head1 Coro::Debug |
| 314 |
|
| 315 |
=head1 Autor |
| 316 |
|
| 317 |
Marc Lehmann <schmorp@schmorp.de>. |