1 |
root |
1.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>. |