| 1 |
root |
1.4 |
=head1 Debuggen mit Coroutinen |
| 2 |
root |
1.1 |
|
| 3 |
|
|
=head1 Einführung |
| 4 |
|
|
|
| 5 |
|
|
Im Laufe der Jahre habe ich eine ganz eigene Methode zum Debuggen |
| 6 |
|
|
meiner Programme entwickelt. Diese Methode kombiniert einige Konzepte |
| 7 |
|
|
(interkative Shell in jedem Programm, Coroutinen), die mehr als reines |
| 8 |
|
|
Debugging ermöglichen. |
| 9 |
|
|
|
| 10 |
|
|
Diese Methode möchte ich hiermit vorstellen, vielleicht bringt sie |
| 11 |
root |
1.7 |
den einen oder anderen auf Gedanken oder stellt sich gar als nützlich |
| 12 |
root |
1.1 |
heraus... |
| 13 |
|
|
|
| 14 |
|
|
=head1 "Traditionelle" Debugging-Methoden |
| 15 |
|
|
|
| 16 |
|
|
Nun, es gibt den Perl-Debugger; über diesen wird tatsächlich viel |
| 17 |
|
|
erzählt und geschrieben, und ich nehme an, er wird wirklich häufig |
| 18 |
|
|
benutzt. Aber aus welchen Gründen auch immer, ich konnte mich mit ihm |
| 19 |
|
|
(anders als mit gdb) nie wirklich anfreunden: die einzige Funktion, die |
| 20 |
|
|
ich mit einiger Regelmäßigkeit benutze, ist die Trace-Funktion. Das kann |
| 21 |
|
|
ich sogar als "Fest im Gehirn eingebautes Makro" sofort tippen: "perl -d |
| 22 |
|
|
xxx, dann t, dann c", andere Debugger-Befehle kenne ich nicht. |
| 23 |
|
|
|
| 24 |
root |
1.7 |
Diese Trace-Funktion hat viele Nachteile: sie macht das Programm sehr |
| 25 |
root |
1.1 |
langsam, sie funktioniert nur, während man im Debugger ist, und häufig |
| 26 |
|
|
hängt mein Programm aus nicht nachvollziehbaren Gründen, und die Ausgabe |
| 27 |
|
|
ist zu umfangreich. Die Hauptnachteile sind aber, daß man den Debugger |
| 28 |
|
|
nicht ein- oder ausschalten kann und daß er interaktiv arbeitet. |
| 29 |
|
|
|
| 30 |
|
|
Die Mehrheit meiner Programme sind langlebige Hintergrundprogramme. Diese |
| 31 |
|
|
sind immerhin so gut getestet, daß sie in Produktion selten Probleme |
| 32 |
|
|
entwickeln, aber manchmal kommt das natürlich vor. Das erklärt |
| 33 |
|
|
wahrscheinlich, weshalb ich den Perl-Debugger nicht benutze: man kann den |
| 34 |
|
|
Perl-Debugger (meines Wissens) nicht an bestehende Prozesse attachen, man |
| 35 |
|
|
kann die Programme nicht dauerhaft unter dem Debugger starten und man hat |
| 36 |
root |
1.7 |
im Allgemeinen auch keinen interaktiven Zugang. |
| 37 |
root |
1.1 |
|
| 38 |
root |
1.7 |
Startet man das Programm neu (z.B. nach Einbau einiger C<warn>-statements |
| 39 |
|
|
oder um es unter dem Debugger laufen zu lassen) tritt das Problem |
| 40 |
|
|
natürlich nicht mehr auf, oder man müsste zu lange warten bis der |
| 41 |
|
|
entscheidende "Trigger" kommt. |
| 42 |
root |
1.1 |
|
| 43 |
root |
1.7 |
Hinzu kommt, daß viele meiner Programme stark ereignisgesteuert |
| 44 |
root |
1.1 |
arbeiten: Hält man das Programm an, gibt es unerwünschte Timeouts; |
| 45 |
root |
1.7 |
erstellt man einen Trace, springt dieser wild zwischen Programmteilen hin |
| 46 |
root |
1.1 |
und her, die nichts miteinander zu tun haben. |
| 47 |
|
|
|
| 48 |
root |
1.7 |
Als einzige Möglichkeit (die auch häufig angewendet wird), bleibt meist |
| 49 |
|
|
nur, extrem viel mitzuloggen, so daß man im Fehlerfall zumindest auf eine |
| 50 |
|
|
Art Trace zurückgreifen kann. Natürlich sind die schweren Fehler selten |
| 51 |
|
|
dort, wo man gerade viel mitloggt. |
| 52 |
root |
1.1 |
|
| 53 |
|
|
=head1 Die Entwicklung eines anderen Ansatzes |
| 54 |
|
|
|
| 55 |
|
|
=head2 Die Anfänge: ein Webserver |
| 56 |
|
|
|
| 57 |
|
|
Um das Jahr 2000 herum schrieb ich einen Web-Server, der vollkommen |
| 58 |
|
|
Event-gesteuert war (buchstäblich: er benutzte das Event-Modul dazu). |
| 59 |
|
|
|
| 60 |
|
|
Weil es so einfach war, bekam er bald eine interaktive Shell verpasst: |
| 61 |
|
|
|
| 62 |
|
|
sub shell { |
| 63 |
|
|
my $fh = shift; |
| 64 |
|
|
|
| 65 |
|
|
while (defined (print $fh "cmd> "), $_ = <$fh>) { |
| 66 |
|
|
s/\015?\012$//; |
| 67 |
|
|
|
| 68 |
root |
1.7 |
if (/^info/) { |
| 69 |
|
|
# bearbeite Kommandos |
| 70 |
|
|
... |
| 71 |
root |
1.1 |
} |
| 72 |
|
|
} |
| 73 |
|
|
|
| 74 |
|
|
my $port = new Coro::Socket |
| 75 |
|
|
LocalPort => $CMDSHELL_PORT, |
| 76 |
|
|
ReuseAddr => 1, |
| 77 |
|
|
Listen => 1, |
| 78 |
|
|
or die "unable to bind cmdshell port: $!"; |
| 79 |
|
|
|
| 80 |
|
|
push @listen_sockets, $port; |
| 81 |
|
|
|
| 82 |
|
|
async { |
| 83 |
|
|
while () { |
| 84 |
|
|
async \&shell, scalar $port->accept; |
| 85 |
|
|
} |
| 86 |
|
|
}; |
| 87 |
|
|
|
| 88 |
|
|
Der Code benutzt Coroutinen, sollte aber einfach verständlich |
| 89 |
|
|
sein: C<Coro::Socket> ist das Pendant zu C<IO::Socket>, bei dem Aufrufe |
| 90 |
root |
1.7 |
wie C<accept> die anderen Coroutinen nicht blockieren; die Funktion |
| 91 |
|
|
C<async> startet eine neue ("asynchrone") Coroutine für jede neue |
| 92 |
root |
1.1 |
Verbindung, und die C<shell>-Funktion schließlich liest in einer Schleife |
| 93 |
|
|
Befehle von der Socket und führt sie aus. |
| 94 |
|
|
|
| 95 |
|
|
Ursprünglich dazu gedacht, eine Liste von aktiven Verbindungen zu |
| 96 |
|
|
erhalten bzw. bisweilen IP-Adressen zu blockieren, hatte ich irgendwann |
| 97 |
root |
1.7 |
eine offensichtliche aber doch nicht so offensichtliche idee (*: |
| 98 |
root |
1.1 |
|
| 99 |
root |
1.7 |
} elsif (s/^print//) { |
| 100 |
root |
1.1 |
my @res = eval $_; |
| 101 |
|
|
print $fh "eval: $@\n" if $@; |
| 102 |
|
|
print $fh "RES = ", (join " : ", @res), "\n"; |
| 103 |
|
|
|
| 104 |
|
|
Mit dem "print"-Kommando (eigentlich wäre "eval" ein besserer Name) |
| 105 |
|
|
lassen sich erstaunlich viele Dinge erledigen: |
| 106 |
|
|
|
| 107 |
|
|
Erlaube 200 gleichzeitige Verbindungen mehr: |
| 108 |
|
|
|
| 109 |
|
|
print $conn::connections->adjust (200) |
| 110 |
|
|
|
| 111 |
|
|
Setze die Downloadrate auf 1MB/s: |
| 112 |
|
|
|
| 113 |
|
|
print $conn::tbf_top->{rate} = 1e6 |
| 114 |
|
|
|
| 115 |
|
|
Erlaube 20 gleichzeitige Downloads mehr: |
| 116 |
|
|
|
| 117 |
|
|
print $conn::queue_file->{slots} += 20 |
| 118 |
|
|
|
| 119 |
root |
1.7 |
Das erste Beispiel benutzt die korrekte "API" zum Anpassen einer |
| 120 |
|
|
Semaphore, die beiden letzteren Beispiele sind eigentlich "böse |
| 121 |
root |
1.1 |
Hacks" weil der Code diese Möglichkeit der Anpassung nicht explizit |
| 122 |
|
|
unterstützt, sie funktionieren aber trotzdem. |
| 123 |
|
|
|
| 124 |
root |
1.7 |
Auf diese Weise läßt sich bisweilen auch Debuggen: wenn z.B. eine |
| 125 |
root |
1.1 |
Verbindung hängt, kann man versuchen, sie zu finden und dann versuchen, |
| 126 |
|
|
herauszufinden, woran es liegt, indem man sich verschiedene globale |
| 127 |
|
|
Variablen anschaut oder kleine "Suchprogramme" mit C<print do "dateiname"> |
| 128 |
|
|
ausführt. |
| 129 |
|
|
|
| 130 |
|
|
Das kann eine extreme Hilfe sein, ist aber immer noch recht umständlich. |
| 131 |
|
|
|
| 132 |
|
|
=head2 Perl ist besser als jede Shell: Deliantra |
| 133 |
|
|
|
| 134 |
|
|
Der Deliantra-Server (ein MORPG) ist ebenfalls vollkommen |
| 135 |
root |
1.8 |
ereignisgesteuert und besitzt ebenfalls eine Shell, die (fast) ohne |
| 136 |
root |
1.1 |
Coroutinen auskommt: |
| 137 |
|
|
|
| 138 |
|
|
sub tcp_serve($) { |
| 139 |
|
|
my ($fh) = @_; |
| 140 |
|
|
|
| 141 |
|
|
binmode $fh, ":raw:perlio:utf8"; |
| 142 |
|
|
print $fh "\n> "; |
| 143 |
|
|
|
| 144 |
|
|
my $iow; $iow = EV::io $fh, EV::READ, sub { |
| 145 |
|
|
if (defined (my $cmd = <$fh>)) { |
| 146 |
|
|
$cmd =~ s/\s+$//; |
| 147 |
|
|
|
| 148 |
|
|
if ($cmd =~ /^\s*exit\b/i) { |
| 149 |
|
|
print $fh "will not exit() server.\n"; |
| 150 |
root |
1.7 |
} elsif (... |
| 151 |
|
|
# andere befehle |
| 152 |
|
|
... |
| 153 |
|
|
|
| 154 |
root |
1.1 |
} |
| 155 |
|
|
}; |
| 156 |
|
|
} |
| 157 |
|
|
|
| 158 |
|
|
our $LISTENER; |
| 159 |
|
|
|
| 160 |
|
|
# now a shell listening on a tcp-port - let the firewall decide access rights |
| 161 |
|
|
if ($cf::CFG{perl_shell}) { |
| 162 |
|
|
if (my $listen = new IO::Socket::INET LocalAddr => $cf::CFG{perl_shell}, Listen => 1, ReuseAddr => 1, Blocking => 0) { |
| 163 |
|
|
$LISTENER = EV::io $listen, EV::READ, sub { tcp_serve $listen->accept }; |
| 164 |
|
|
} |
| 165 |
|
|
} |
| 166 |
|
|
|
| 167 |
root |
1.7 |
Deliantra benutzt EV als Event-Bibliothek, ansonsten habe ich aus meinen |
| 168 |
root |
1.1 |
früheren Versuchen gelernt und implementiere keine Kommandos mehr direkt, |
| 169 |
root |
1.7 |
sondern erlaube direkt die Eingabe von Perl-Ausdrücken (und stelle |
| 170 |
|
|
stattdessen einfach ein paar Funktionen im Namensraum zur Verfügung): |
| 171 |
root |
1.1 |
|
| 172 |
|
|
... |
| 173 |
|
|
} else { |
| 174 |
|
|
my $sub = sub { |
| 175 |
|
|
package cf; |
| 176 |
|
|
select $fh; |
| 177 |
|
|
|
| 178 |
|
|
# compile first, then execute, as Coro does not support switching in eval string |
| 179 |
|
|
my $cb = eval "sub { $cmd \n}"; |
| 180 |
|
|
|
| 181 |
|
|
my $t1 = Time::HiRes::time; |
| 182 |
|
|
my @res = $@ ? () : eval { $cb->() }; |
| 183 |
|
|
my $t2 = Time::HiRes::time; |
| 184 |
|
|
|
| 185 |
|
|
print "\n", |
| 186 |
|
|
"command: '$cmd'\n", |
| 187 |
|
|
"execution time: ", $t2 - $t1, "\n"; |
| 188 |
|
|
warn "evaluation error: $@" if $@; |
| 189 |
|
|
print "evaluation error: $@\n" if $@; |
| 190 |
|
|
print "result:\n", cf::dumpval @res > 1 ? \@res : $res[0] if @res; |
| 191 |
|
|
print "\n> "; |
| 192 |
|
|
|
| 193 |
|
|
select STDOUT; |
| 194 |
|
|
}; |
| 195 |
|
|
|
| 196 |
|
|
if ($cmd =~ s/\s*&$//) { |
| 197 |
|
|
cf::async { |
| 198 |
|
|
$Coro::current->desc ($cmd); |
| 199 |
|
|
$sub->() |
| 200 |
|
|
}; |
| 201 |
|
|
} else { |
| 202 |
|
|
$sub->(); |
| 203 |
|
|
} |
| 204 |
|
|
|
| 205 |
|
|
Die Befehlsausführung ist weitaus komplexer: Zuerst wird die eingegebene |
| 206 |
root |
1.7 |
Zeile kompiliert und dann C<eval>uiert. Danach wird das Ergebnis, etwaige |
| 207 |
root |
1.1 |
Laufzeitfehler und die Ausführungszeit ausgegeben. |
| 208 |
|
|
|
| 209 |
|
|
Da man als Administrator manchmal Befehle im "Hauptprogramm" (die |
| 210 |
|
|
Coroutine, die die Event-Schleife ausführt) ausführen muss, der Server |
| 211 |
|
|
aber all 120ms ein update generieren muss, muss man lang dauernde Befehle |
| 212 |
|
|
in den "Hintergrund" (eine weitere Coroutine) schieben, was mit einem |
| 213 |
|
|
angehängten "&" geschieht. |
| 214 |
|
|
|
| 215 |
|
|
Eine Beispielsession sieht so aus: |
| 216 |
|
|
|
| 217 |
|
|
# dmshell |
| 218 |
|
|
Welcome! |
| 219 |
|
|
|
| 220 |
|
|
ext::help::reload & |
| 221 |
|
|
ext::books::reload & |
| 222 |
|
|
ext::map_tags::reload & |
| 223 |
|
|
ext::map_world::reload & |
| 224 |
|
|
print JSON::XS->new->pretty->encode({cf::mallinfo}) |
| 225 |
|
|
|
| 226 |
|
|
> ext::map_world::reload & |
| 227 |
|
|
|
| 228 |
|
|
> |
| 229 |
|
|
command: 'ext::map_world::reload' |
| 230 |
|
|
execution time: 0.0744819641113281 |
| 231 |
|
|
|
| 232 |
|
|
> ext::map_tags::reload & |
| 233 |
|
|
|
| 234 |
|
|
> |
| 235 |
|
|
command: 'ext::map_tags::reload' |
| 236 |
|
|
execution time: 1.14403510093689 |
| 237 |
|
|
|
| 238 |
|
|
> $cf::PLAYER-{schmorp} |
| 239 |
|
|
|
| 240 |
|
|
command: '$cf::PLAYER-{schmorp}' |
| 241 |
|
|
execution time: 5.00679016113281e-06 |
| 242 |
|
|
evaluation error: Bareword "schmorp" not allowed while "strict subs" in use at (eval 180) line 1, <GEN63> line 6. |
| 243 |
|
|
|
| 244 |
|
|
> $cf::PLAYER{schmorp} |
| 245 |
|
|
|
| 246 |
|
|
command: '$cf::PLAYER{schmorp}' |
| 247 |
|
|
execution time: 1.09672546386719e-05 |
| 248 |
|
|
result: |
| 249 |
|
|
bless( { |
| 250 |
|
|
log_told => {}, |
| 251 |
|
|
last_save => "32009728.5217925", |
| 252 |
|
|
rent => { |
| 253 |
|
|
last_online_check => "1200189659", |
| 254 |
|
|
last_offline_check => "1200189659", |
| 255 |
|
|
balance => "-0.737118053715676", |
| 256 |
|
|
apartment => { |
| 257 |
|
|
"/brest/apartments/brest_town_house" => undef, |
| 258 |
|
|
"/scorn/apartment/apartments" => undef |
| 259 |
|
|
} |
| 260 |
|
|
}, |
| 261 |
|
|
hintmode => 0, |
| 262 |
|
|
npc_dialog_active => {} |
| 263 |
|
|
}, 'cf::player::wrap' ) |
| 264 |
|
|
|
| 265 |
|
|
> |
| 266 |
|
|
> "schmorp"->cf::player::find->ob->stats->hp |
| 267 |
|
|
|
| 268 |
|
|
command: '"schmorp"->cf::player::find->ob->stats->hp' |
| 269 |
|
|
execution time: 4.79221343994141e-05 |
| 270 |
|
|
result: |
| 271 |
|
|
520 |
| 272 |
|
|
|
| 273 |
|
|
und so weiter... Das ist natürlich eine große Hilfe beim Administrieren |
| 274 |
root |
1.5 |
oder Debuggen, weil man sich die aktuellen Daten, die im Server geladen |
| 275 |
root |
1.7 |
sind, direkt ansehen kann. |
| 276 |
root |
1.1 |
|
| 277 |
|
|
Sehr angenehm ist es auch, direkt im Spielbetrieb Bugs zu fixen, indem man |
| 278 |
|
|
einzelne Routinen direkt überschreibt: |
| 279 |
|
|
|
| 280 |
|
|
# cat /tmp/bugfix |
| 281 |
|
|
package cf; |
| 282 |
|
|
sub _can_merge { |
| 283 |
|
|
# neue merge-logik, vielleicht mit printf-style-debugging |
| 284 |
|
|
} |
| 285 |
|
|
1 |
| 286 |
|
|
|
| 287 |
|
|
# dmshell |
| 288 |
|
|
> do "/tmp/bugfix" |
| 289 |
|
|
|
| 290 |
|
|
So kann man im laufenden Betrieb schon sehr angenehm am Server arbeiten, |
| 291 |
|
|
aber man muss sich jedesmal eine Shell ausdenken, sie implementieren, und |
| 292 |
|
|
kann dennoch nur herumstochern. |
| 293 |
|
|
|
| 294 |
|
|
Doch mit Coroutinen kann mehr wesentlich mehr... |
| 295 |
|
|
|
| 296 |
|
|
|
| 297 |
|
|
=head1 Coroutinen |
| 298 |
|
|
|
| 299 |
|
|
Mit Perl hat man prinzipiell zwei Methoden der |
| 300 |
root |
1.8 |
"Parallelverarbeitung": Coroutinen (teilen sich einen gemeinsamen |
| 301 |
root |
1.1 |
Adressraum, laufen aber nicht wirklich parallel) und Prozesse (haben |
| 302 |
|
|
getrennte Adressräume, laufen aber parallel (mit entsprechender |
| 303 |
|
|
Hardware)). Threads (gemeinsamer Adressraum und echte Parallelität) |
| 304 |
|
|
werden von Perl nicht angeboten. |
| 305 |
|
|
|
| 306 |
root |
1.2 |
Gegenüber Prozessen hat man den Vorteil extrem einfacher Kommunikation |
| 307 |
root |
1.7 |
zwischen den einzelnen Instanzen. Beispielsweise implementiert der schon |
| 308 |
root |
1.2 |
genannte Webserver einen Schutz gegen segmentierte Downloads, indem er die |
| 309 |
root |
1.7 |
gerade heruntergeladenen URLs pro Klient in einem globalen Hash speichert: |
| 310 |
root |
1.1 |
|
| 311 |
|
|
if ($DOWNLOADS{$url}{$clientid} >= 4) { |
| 312 |
|
|
# abort, zu viele Verbindungen |
| 313 |
|
|
return; |
| 314 |
|
|
} |
| 315 |
|
|
|
| 316 |
|
|
++$DOWNLOADS{$url}{$clientid}; |
| 317 |
|
|
|
| 318 |
root |
1.2 |
Mit mehreren Prozessen ist dies natürlich nicht so einfach. |
| 319 |
|
|
|
| 320 |
root |
1.7 |
Ein weiterer Vorteil von Coroutinen ist das stark vereinfachte Locking: es |
| 321 |
|
|
gibt praktisch keine Race-Conditions, denn solange man nicht absichtlich |
| 322 |
|
|
Rechenzeit abgibt, wird man auch nicht unterbrochen. |
| 323 |
root |
1.2 |
|
| 324 |
|
|
Aber wie hilft das beim Debuggen? |
| 325 |
|
|
|
| 326 |
|
|
In einem ereignisgesteuerten Programm hat man meistens wenige Watcher |
| 327 |
|
|
(z.B. einen pro TCP-Verbindung). Der aktuelle Zustand einer Verbindung |
| 328 |
|
|
steht in irgendwelchen Variablen serialisiert. Das kann entweder eine |
| 329 |
|
|
Zustandsmaschine sein (entweder Ad-Hoc oder z.B. mit POE) oder auch |
| 330 |
|
|
per Continuation-Style durch den auf den Watcher gebundenen Callback, |
| 331 |
|
|
mit einigen lexikalischen Variablen, in die man garnicht so einfach |
| 332 |
|
|
reinschauen kann. |
| 333 |
|
|
|
| 334 |
|
|
Die ausgeführten Programmzeilen sind relativ uninteressant, das Programm |
| 335 |
|
|
verbringt wohl die meiste Zeit in der Hauptschleife (z.B. C<EV::loop> oder |
| 336 |
|
|
C<< Gtk->main >>), und Backtraces helfen garnicht ("steckt das Programm in |
| 337 |
|
|
C<lese_von_socket> weil es noch auf den Header wartet oder ist es schon |
| 338 |
root |
1.7 |
beim Lesen des Request-Bodies? In welcher Variable steht der aktuelle |
| 339 |
|
|
Zustand nochmal?"). |
| 340 |
root |
1.2 |
|
| 341 |
root |
1.7 |
Bei einem "herkömmlichen" - nicht ereignisgesteuertem - Programm ist |
| 342 |
root |
1.2 |
es viel einfacher: "Programm steckt in Zeile 231, da liest er gerade den |
| 343 |
|
|
Header ein". |
| 344 |
|
|
|
| 345 |
root |
1.7 |
Und genau diese einfache Relation "Ort entspricht Zustand" erhält man mit |
| 346 |
|
|
Coroutinen. Natürlich braucht man Unterstützung vom Laufzeitsystem, und diese |
| 347 |
|
|
Hilfe kommt von... |
| 348 |
root |
1.2 |
|
| 349 |
root |
1.1 |
=head1 Coro::Debug |
| 350 |
|
|
|
| 351 |
root |
1.2 |
Seit Version 4.0 gibt es in der Coro-Distribution das |
| 352 |
|
|
C<Coro::Debug>-Modul. |
| 353 |
|
|
|
| 354 |
|
|
Dieses implementiert nicht nur eine interaktive Shell (bzw. auch |
| 355 |
|
|
Einzelteile, mit denen man das selbst tun kann), sondern auch eine |
| 356 |
|
|
Übersicht über die Coroutinen, Backtraces und Aufruftraces. |
| 357 |
|
|
|
| 358 |
|
|
=head2 Benutzung |
| 359 |
|
|
|
| 360 |
|
|
Die einfachste Methode, um an eine Shell zu kommen, ist es, einen "UNIX |
| 361 |
|
|
Socket Server" zu starten: |
| 362 |
|
|
|
| 363 |
|
|
use Coro::Debug; |
| 364 |
|
|
|
| 365 |
|
|
$CORO_DEBUGGER = new_unix_server Coro::Debug "/tmp/debug"; |
| 366 |
|
|
|
| 367 |
|
|
Und schon kann man sich mit F</tmp/debug> verbinden... Oder auch |
| 368 |
root |
1.7 |
nicht: Die bisherigen Beispiele benutzten TCP-Sockets, da konnte man |
| 369 |
root |
1.2 |
C<telnet> benutzen, aber wie macht man das mit UNIX Sockets, und |
| 370 |
|
|
warum? |
| 371 |
|
|
|
| 372 |
|
|
An dieser Stelle möchte ich herzlichst ein feines Werkzeug namens |
| 373 |
root |
1.7 |
C<socat> empfehlen, mit dem man sehr einfach Verbindungen zwischen zwei |
| 374 |
root |
1.2 |
Punkten schaffen kann, z.B. zwischen der Readline-Bibliothek und einer |
| 375 |
|
|
UNIX Socket: |
| 376 |
|
|
|
| 377 |
|
|
socat readline unix:/tmp/debug |
| 378 |
|
|
|
| 379 |
root |
1.7 |
Und man hat - im Gegensatz zum Perl-Debugger - auch noch echtes Readline |
| 380 |
|
|
:-> C<socat> kann übrigens auch anders, z.B. C<netcat> ersetzen: C<socat |
| 381 |
root |
1.2 |
readline tcp:127.0.0.1:80>. Oder einen UDP-Server implementieren: C<socat |
| 382 |
root |
1.7 |
system:mcookie udp-listen:676>. Oder einen einfachen Proxy, oder... es ist |
| 383 |
|
|
sehr hilfreich! |
| 384 |
root |
1.2 |
|
| 385 |
root |
1.7 |
Der Grund, eine UNIX Socket zu benutzena, ist der Sicherheitsaspekt: Um einen |
| 386 |
|
|
TCP-Port zu schützen (die Shell macht ja keinerlei Authentifizierung) |
| 387 |
root |
1.2 |
braucht man schon einen Firewall, um eine UNIX Socket zu schützen |
| 388 |
|
|
muss man sie nur in ein Verzeichnis legen auf das nur legitime |
| 389 |
|
|
Benutzer/Programme Zugriff besitzen (F</tmp/debug> ist also ein schlechtes |
| 390 |
|
|
Beispiel). Auf diese Weise kann man diese Shell relativ unbedenklich |
| 391 |
root |
1.7 |
immer aktiviert lassen (auch z.B. in der Produktionsumgebung): |
| 392 |
root |
1.2 |
|
| 393 |
|
|
system "rm -rf /tmp/myprog"; |
| 394 |
|
|
mkdir "/tmp/myprog", 0700 or die "/tmp/myprog: $!"; |
| 395 |
|
|
$CORO_DEBUGGER = new_unix_server Coro::Debug "/tmp/myprog/debug"; |
| 396 |
|
|
|
| 397 |
|
|
Auf diese Weise haben nur diejenigen Zugriff auf den Prozess, die sowieso |
| 398 |
|
|
Zugriff haben. |
| 399 |
|
|
|
| 400 |
root |
1.7 |
Und nun zu einigen der eingebaute Befehle (es gibt auch das |
| 401 |
|
|
C<help>-Kommando, und alles, was nicht ein bekanntes Kommando ist, wird |
| 402 |
|
|
als Perl-Ausdruck interpretiert): |
| 403 |
|
|
|
| 404 |
|
|
|
| 405 |
root |
1.2 |
=head2 "Prozessliste" |
| 406 |
|
|
|
| 407 |
|
|
Mit dem C<ps>-Kommando erhält man eine Übersicht aller laufenden |
| 408 |
root |
1.7 |
Coroutinen, hier als Beispiel in einem laufenden Deliantra-Server: |
| 409 |
root |
1.2 |
|
| 410 |
|
|
> ps |
| 411 |
|
|
PID SS RSS USES Description Where |
| 412 |
|
|
10289200 US 865k 830k [main::] [/deliantra/ext/dm-support.ext:47] |
| 413 |
|
|
10289392 -- 2508 66 [coro manager] [/opt/perl/lib/perl5/Coro.pm:177] |
| 414 |
|
|
10289712 -- 2508 2094 [unblock_sub scheduler] [/opt/perl/lib/perl5/Coro.pm:589] |
| 415 |
|
|
13776976 -- 2548 13 [EV idle process] [/opt/perl/lib/perl5/Coro/EV.pm:65] |
| 416 |
|
|
18176656 -- 2964 53k timeslot manager [/deliantra/cf.pm:397] |
| 417 |
|
|
23103792 -- 19k 42k player scheduler [/deliantra/ext/login.ext:510] |
| 418 |
|
|
46912554817888 -- 2980 1 follow handler [/deliantra/ext/follow.ext:50] |
| 419 |
|
|
19457280 -- 138k 432k map scheduler [/deliantra/ext/map-scheduler.ext:65] |
| 420 |
|
|
18681088 -- 3228 6312 music scheduler [/deliantra/ext/player-env.ext:77] |
| 421 |
|
|
26391616 -- 2980 40k worldmap updater [/deliantra/ext/item-worldmap.ext:114] |
| 422 |
|
|
140130800 -- 16k 3746 [async_pool idle] [/opt/perl/lib/perl5/Coro.pm:258] |
| 423 |
|
|
286210960 -- 16k 22k [async_pool idle] [/opt/perl/lib/perl5/Coro.pm:258] |
| 424 |
|
|
196084816 -- 10k 1251 [async_pool idle] [/opt/perl/lib/perl5/Coro.pm:258] |
| 425 |
|
|
439518192 -- 3268 2 addme init [/deliantra/ext/login.ext:21] |
| 426 |
|
|
|
| 427 |
|
|
Die C<PID> ist nichts anderes als die Addresse des Perl-Coroutinenobjektes |
| 428 |
|
|
(C<$obj+0>). Die C<SS>-Spalte (State/Stack) gibt an, ob eine Coroutine |
| 429 |
|
|
gerade läuft (rB<U>nning, B<R>eady oder weder noch), bzw. ob sie einen |
| 430 |
|
|
eigenen C-Stack hat (B<S>) oder nicht (B<->), oder gerade getraced wird |
| 431 |
|
|
(C<T>), mehr dazu später. C<RSS> ist der Speicherverbrauch der Coroutine |
| 432 |
root |
1.7 |
in Bytes. C<Uses> gibt an, wie oft die Coroutine Rechenzeit zugeteilt |
| 433 |
|
|
bekommen bzw. wieder abgegeben hat. |
| 434 |
root |
1.2 |
|
| 435 |
|
|
Die C<Description>-Spalte gibt lediglich den Inhalt des C<desc>-Members |
| 436 |
|
|
der Coroutinenstruktur wieder, den man einfach setzen kann: |
| 437 |
|
|
|
| 438 |
|
|
$Coro::current->{desc} = "login phase 1"; |
| 439 |
|
|
... tue etwas |
| 440 |
|
|
$Coro::current->{desc} = "login phase 2"; |
| 441 |
|
|
... |
| 442 |
|
|
|
| 443 |
|
|
Die meisten Coroutinen setzen einfach einen Namen, manche ändern den |
| 444 |
|
|
Namen je nach Ort, so daß man sofort sehen kann, in welchen "Zustand" die |
| 445 |
|
|
Coroutine ist. |
| 446 |
|
|
|
| 447 |
root |
1.7 |
Und C<Where> schließlich gibt an, wo im Programm sich die Coroutine |
| 448 |
|
|
gerade befindet (dabei nimmt sich C<Coro> selbst aus, sonst würde dort |
| 449 |
|
|
fast immer nur C<Coro::Semaphore>, C<Coro::Signal> usw. auftauchen). |
| 450 |
|
|
|
| 451 |
root |
1.2 |
Der Server benutzt eine Menge Coroutinen (die meisten sind sehr |
| 452 |
|
|
kurzlebig): Spieler regelmäßig speichern erledigt z.B. der C<player |
| 453 |
|
|
scheduler>, der hier gerade in Zeile C<510> von C<login.ext> steckt: |
| 454 |
|
|
|
| 455 |
|
|
Coro::EV::timer_once $SCHEDULE_INTERVAL; |
| 456 |
|
|
|
| 457 |
|
|
Das macht Sinn, da er nur alle paar Sekunden aktiv wird und den Rest der |
| 458 |
root |
1.7 |
Zeit schläft (in C<timer_once>). |
| 459 |
root |
1.2 |
|
| 460 |
root |
1.8 |
Ein anderes Beispiel ist C<addme init>, was einen Teil des Login-Prozesses |
| 461 |
root |
1.2 |
darstellt. Zeile C<21> ist in einer Funktion C<query>, die den Benutzer |
| 462 |
root |
1.8 |
etwas fragt (naheligenderweise Name oder Passwort). Um mehr zu erfahren, |
| 463 |
root |
1.2 |
braucht man einen Backtrace: |
| 464 |
|
|
|
| 465 |
|
|
=head2 Backtraces |
| 466 |
|
|
|
| 467 |
|
|
Backtraces erhält man mit dem C<bt>-Kommando und der "PID" als Argument: |
| 468 |
|
|
|
| 469 |
|
|
> bt 439518192 |
| 470 |
|
|
coroutine is at /deliantra/ext/login.ext line 21 |
| 471 |
|
|
ext::login::query('cf::client::wrap=HASH(0x3d7dfb0)', |
| 472 |
|
|
0, 'What is your name?\x{a}:') |
| 473 |
|
|
called at /deliantra/ext/login.ext line 208 |
| 474 |
|
|
ext::login::__ANON__ called at -e line 0 |
| 475 |
|
|
Coro::_run_coro called at -e line 0 |
| 476 |
|
|
|
| 477 |
|
|
Nicht nur sieht man sofort, was den Benutzer gefragt wird (weil C<query> |
| 478 |
|
|
den Fragetext als Parameter übergeben bekommt), sondern man sieht auch, |
| 479 |
|
|
wo im Login-Prozess man sich gerade befindet, nämlich in Zeile C<208> |
| 480 |
|
|
(was innerhalb des C<on_addme>-Callbacks von Deliantra ist). |
| 481 |
|
|
|
| 482 |
|
|
Noch mehr Informationen erhält man mit einem Aufruftrace: |
| 483 |
|
|
|
| 484 |
|
|
=head2 Aufruftraces/Ablaufverfolgung |
| 485 |
|
|
|
| 486 |
|
|
Zuerst sollte man den "Logging Level" in seiner Shell auf 5 oder höher schrauben: |
| 487 |
|
|
|
| 488 |
|
|
> loglevel 5 |
| 489 |
|
|
|
| 490 |
|
|
Dann kann man Aufruftraces mit C<tr> und C<ut> starten und stoppen: |
| 491 |
|
|
|
| 492 |
|
|
> tr 439518192 |
| 493 |
|
|
2008-01-13Z04:03:28.1954 (5) [439518192] tracing enabled |
| 494 |
|
|
(timestanmps usf. im Folgenden gekürzt) |
| 495 |
|
|
.8374 (5) [pid] enter Coro::State::_cctx_init with (277569264) |
| 496 |
|
|
.8375 (5) [pid] leave Coro::State::_cctx_init returning () |
| 497 |
|
|
.8375 (5) [pid] leave ext::login::query returning (schmorp) |
| 498 |
|
|
.8376 (5) [pid] enter ext::login::check_playing with (cf::client::wrap=HASH(0x3d7dfb0),name) |
| 499 |
|
|
.8376 (5) [pid] enter cf::player::find_active with (schmorp) |
| 500 |
|
|
.8376 (5) [pid] leave cf::player::find_active returning () |
| 501 |
|
|
.8378 (5) [pid] enter cf::client::send_drawinfo with |
| 502 |
|
|
(cf::client::wrap=HASH(0x3d7dfb0),Welcome name, please enter your password...,5) |
| 503 |
|
|
.8378 (5) [pid] leave cf::client::send_drawinfo returning () |
| 504 |
|
|
.8379 (5) [pid] enter ext::login::query with ( |
| 505 |
|
|
cf::client::wrap=HASH(0x3d7dfb0),4,What is your password?\x{0a}:) |
| 506 |
|
|
.8380 (5) [pid] enter cf::client::query with ( |
| 507 |
|
|
cf::client::wrap=HASH(0x3d7dfb0),4,What is your password?\x{0a}:,CODE(0x6fca040)) |
| 508 |
|
|
.8380 (5) [pid] leave cf::client::query returning () |
| 509 |
|
|
|
| 510 |
|
|
Bei diesem Tracing wird bei jedem Funktionsaufruf bzw. bei jeder Rückkehr |
| 511 |
|
|
aus einer Funktion eine Zeile ausgegeben die Funktionsnamen, Argumente |
| 512 |
|
|
bzw. Resultatsliste ausgibt. |
| 513 |
|
|
|
| 514 |
|
|
Im Beispiel wurde die Ablaufverfolgung aktiviert, während der |
| 515 |
|
|
Spiel-Klient noch in der Namensabfrage steckte (der Aufruf von |
| 516 |
|
|
C<_cctx_init> ist ein Implementationsdetail von Coro, der das Starten |
| 517 |
|
|
eines neuen Interpreters anzeigt, mehr dazu später). Konsequenterweise |
| 518 |
|
|
sieht man daher nur die Rückkehr aus dem C<query>-Aufruf, mit dem Namen |
| 519 |
|
|
als Resultat (C<schmorp>). |
| 520 |
|
|
|
| 521 |
|
|
Danach schaut der Server nach, ob der User gerade spielt |
| 522 |
root |
1.9 |
(C<check_playing>), was wiederum C<find_active>) aufruft). Weil er nicht |
| 523 |
root |
1.7 |
spielt kann man mit dem Login fortfahren und nach dem Passwort fragen, |
| 524 |
root |
1.2 |
usf. |
| 525 |
|
|
|
| 526 |
|
|
Es ist gut möglich, daß der Server während der Ausführung einer |
| 527 |
|
|
Coroutine viele andere Dinge tut (wenn der Login-Callback den Spieler von |
| 528 |
|
|
der Platte lädt, läuft der Server erstmal weiter, denn der Zugriff kann |
| 529 |
|
|
durchaus mal eine Sekunde oder länger dauern, wenn z.B. gerade viel I/O |
| 530 |
root |
1.7 |
stattfindet). Da die Ablaufverfolgung nur für eine (die interessante) |
| 531 |
root |
1.2 |
Coroutine aktiviert ist, bekommt man auch nur für diese die Meldungen, |
| 532 |
|
|
was ungemein hilfreich ist. |
| 533 |
|
|
|
| 534 |
root |
1.7 |
C<Coro> selbst unterstützt auch eine zeilenweise Ablaufverfolgung |
| 535 |
|
|
ähnlich wie im Perl-Debugger, es gibt jedoch noch kein Kommando um dieses |
| 536 |
|
|
zu aktivieren (ich hab's schlicht noch nie vermisst). Wenn man sich das |
| 537 |
|
|
C<Coro::Debug>-Modul ansieht, sieht man such, daß man seine eigenen |
| 538 |
|
|
Trace-Callbacks definieren kann und damit recht viele Tricks möglich |
| 539 |
|
|
sind. |
| 540 |
root |
1.2 |
|
| 541 |
|
|
=head3 "printf-debugging" |
| 542 |
|
|
|
| 543 |
|
|
Die Ablaufverfolgung benutzt die C<Coro::Debug::log $loglevel, |
| 544 |
root |
1.7 |
$msg>-Funktion zum ausgeben der Meldungen, mit der man auch eigene Meldungen ausgeben kann, |
| 545 |
|
|
wahlweise auch nur wenn Tracing aktiviert ist: |
| 546 |
root |
1.2 |
|
| 547 |
|
|
Coro::Debug::log 6, "some log message" |
| 548 |
|
|
if $Coro::current->is_traced; |
| 549 |
|
|
|
| 550 |
|
|
(Vielleicht sollte die nächste Version von Coro::Debug einen |
| 551 |
root |
1.7 |
coroutinen/shellabhängigen Loglevel anbieten...) |
| 552 |
root |
1.2 |
|
| 553 |
|
|
=head3 Automatische Aktivierung |
| 554 |
|
|
|
| 555 |
root |
1.7 |
Die Ablaufverfolgung kann man auch progrrammatisch aktivieren, z.B. wenn |
| 556 |
|
|
die Coroutine kurzlebig ist oder man unbedingt den Anfang mitkriegen |
| 557 |
|
|
möchte: |
| 558 |
root |
1.2 |
|
| 559 |
|
|
Coro::Debug::trace; |
| 560 |
|
|
# ab hier wird getraced |
| 561 |
|
|
|
| 562 |
|
|
Coro::Debug::untrace; |
| 563 |
|
|
# ab hier nicht mehr, boa! |
| 564 |
|
|
|
| 565 |
|
|
=head3 Problemchen |
| 566 |
|
|
|
| 567 |
|
|
Ablaufverfolgung geht nicht mit allen Coroutinen: |
| 568 |
|
|
|
| 569 |
|
|
PID SS RSS USES Description Where |
| 570 |
|
|
10289200 US 865k 926k [main::] [/deliantra/ext/dm-support.ext:47] |
| 571 |
|
|
|
| 572 |
|
|
> coro tr 10289200 |
| 573 |
|
|
2008-01-13Z04:33:58.7787 (5) [123282976] unable to enable tracing: |
| 574 |
|
|
cannot enable tracing on coroutine with custom stack |
| 575 |
|
|
at /opt/perl/lib/perl5/Coro/Debug.pm line 205, <GEN88> line 2. |
| 576 |
|
|
|
| 577 |
|
|
Der Grund liegt in der Implementation: Sowohl der Perl-Debugger als auch |
| 578 |
|
|
Coro müssen eine andere Implementation des Interpreters verwenden, der |
| 579 |
|
|
langsamer läuft, aber den Ablauf verfolgen kann. Wie also kann man diese |
| 580 |
|
|
mit Coro an- oder abschalten? |
| 581 |
|
|
|
| 582 |
|
|
Coro arbeitet, indem jede Coroutine quasi ihren eigenen (abgespeckten) |
| 583 |
|
|
Interpreter verpasst bekommt. Damit der Speicherverbrauch gering und |
| 584 |
|
|
vor allem die Geschwindigkeit hoch sind, teilt Coro verschiedenen |
| 585 |
root |
1.6 |
Coroutinen nach Möglichkeit den gleichen Interpreter zu. Dies ist |
| 586 |
|
|
genau dann möglich, wenn sich die Coroutine in der äußersten |
| 587 |
root |
1.2 |
Interpreterschleife befindet (d.h. keine rekursiven Aufrufe gemacht |
| 588 |
|
|
wurden. Ein rekursiver Aufruf findet statt, wenn man eine C/XS-Funktion |
| 589 |
|
|
aufruft und diese wiederum Perl, was z.B. bei fast allen Callback-Systemen |
| 590 |
|
|
vorkommt). Außerdem hat das Hautprogramm immer seinen eigenen |
| 591 |
|
|
Interpreter. |
| 592 |
|
|
|
| 593 |
|
|
Genau dies sieht man im rechten Teil der C<SS>-Spalte: Tracing kann |
| 594 |
|
|
nur dann aktiviert werden, wenn dort ein C<-> steht, und auch nur dann |
| 595 |
|
|
deaktiviert werden, wenn die Coroutine während des Tracens keine |
| 596 |
|
|
rekursiven Mätzchen macht. |
| 597 |
|
|
|
| 598 |
|
|
In der Praxis ist das kein Problem: man kann ja einfach alles in eine |
| 599 |
|
|
Coroutine verlegen und das Hauptprogramm schlafenlegen, oder das Tracing |
| 600 |
root |
1.7 |
aktivieren (z.b. programmatisch), bevor man beispielsweise in die |
| 601 |
|
|
Event-Schleife springt: Das verlangsamt die Ausführung zwar, aber nicht |
| 602 |
|
|
sehr (einige Prozent). |
| 603 |
root |
1.2 |
|
| 604 |
|
|
=head2 Code-Injection |
| 605 |
|
|
|
| 606 |
|
|
Manchmal kann es recht hilfreich sein, einer Coroutine etwas Code |
| 607 |
|
|
unterzujubeln. Auf diese Weise kann man z.B. Backtraces erhalten: |
| 608 |
|
|
|
| 609 |
|
|
> eval 277575488 Carp::cluck "holladrio" |
| 610 |
|
|
|
| 611 |
|
|
Oder man kann auf lokale Variablen zugreifen (z.B. mit dem |
| 612 |
|
|
C<PadWalker>-Modul). |
| 613 |
|
|
|
| 614 |
root |
1.7 |
Das Ganze geht auch programmatisch: |
| 615 |
root |
1.2 |
|
| 616 |
|
|
$coro->eval ("string"); |
| 617 |
|
|
$coro->call (sub { ... }); |
| 618 |
root |
1.7 |
|
| 619 |
|
|
Und es funktioniert sogar, wenn die Coroutine sich in einem XS-Aufruf |
| 620 |
|
|
befinden sollte. |
| 621 |
root |
1.2 |
|
| 622 |
|
|
|
| 623 |
|
|
=head1 Ausblick |
| 624 |
|
|
|
| 625 |
|
|
C<Coro::Debug> ist recht neu und die sich ergebenden Möglichkeiten noch |
| 626 |
|
|
nicht wirklich ausgelotet. Ich denke, daß es aber jetzt schon sehr |
| 627 |
|
|
hilfreiche Mittel zum Debugging zur Verfügung stellt. |
| 628 |
|
|
|
| 629 |
|
|
Ich hoffe auch, ich konnte ein paar Anregungen zum Thema "jedes |
| 630 |
|
|
Perl-Programm braucht eine interaktive Shell" zu liefern: Es ist |
| 631 |
|
|
wirklich hilfreich, und mit Perl (mit oder ohne Coro) sehr einfach zu |
| 632 |
|
|
implementieren. |
| 633 |
|
|
|
| 634 |
root |
1.1 |
=head1 Autor |
| 635 |
|
|
|
| 636 |
|
|
Marc Lehmann <schmorp@schmorp.de>. |
| 637 |
root |
1.3 |
|
| 638 |
|
|
=head1 Anhang: dumpval |
| 639 |
|
|
|
| 640 |
|
|
Wen's interessiert, ich verwende die folgende "dumpval"-Funktion schon |
| 641 |
|
|
seit vielen Jahren, und sie gibt recht lesbare Debug-Dumps: |
| 642 |
|
|
|
| 643 |
|
|
# dumpval $ref |
| 644 |
|
|
|
| 645 |
|
|
sub dumpval { |
| 646 |
|
|
eval { |
| 647 |
|
|
local $SIG{__DIE__}; |
| 648 |
|
|
my $d; |
| 649 |
|
|
$d = new Data::Dumper([$_[0]], ["*var"]); |
| 650 |
|
|
$d->Terse(1); |
| 651 |
|
|
$d->Indent(2); |
| 652 |
|
|
$d->Quotekeys(0); |
| 653 |
|
|
$d->Useqq(1); |
| 654 |
|
|
#$d->Bless(...); |
| 655 |
|
|
$d->Seen($_[1]) if @_ > 1; |
| 656 |
|
|
$d = $d->Dump(); |
| 657 |
|
|
$d =~ s/([\x00-\x07\x09\x0b\x0c\x0e-\x1f])/sprintf "\\x%02x", ord($1)/ge; |
| 658 |
|
|
$d |
| 659 |
|
|
} || "[unable to dump $_[0]: '$@']"; |
| 660 |
|
|
} |
| 661 |
|
|
|
| 662 |
|
|
Früher habe ich mal Dumpvalue benutzt, ist auch nicht schlecht (das |
| 663 |
root |
1.5 |
PApp::Catch_STDOUT ist ein Hack, den man in neueren Perls durch PerlIO |
| 664 |
root |
1.3 |
ersetzen kann): |
| 665 |
|
|
|
| 666 |
|
|
sub dumpval { |
| 667 |
|
|
eval { |
| 668 |
|
|
local $SIG{__DIE__}; |
| 669 |
|
|
my $d; |
| 670 |
|
|
local *STDOUT; |
| 671 |
|
|
local *PApp::output; |
| 672 |
|
|
tie *STDOUT, PApp::Catch_STDOUT; |
| 673 |
|
|
|
| 674 |
|
|
require Dumpvalue; |
| 675 |
|
|
$d = new Dumpvalue globPrint => 1, compactDump => 0, veryCompact => 0; |
| 676 |
|
|
$d->dumpValue($_[0]); |
| 677 |
|
|
$d = $PApp::output; |
| 678 |
|
|
$d =~ s/([\x00-\x07\x09\x0b\x0c\x0e-\x1f])/sprintf "\\x%02x", ord($1)/ge; |
| 679 |
|
|
$d; |
| 680 |
|
|
} || "[unable to dump $_[0]: '$@']"; |
| 681 |
|
|
} |
| 682 |
|
|
|