Devel::ptkdb ist ein beliebtes CPAN Module zum Debuggen von Perl Skripten aller Art, also auch Perl/Tk Anwendungen. Bei diesen Letzten gibt es allerdings einige Besonderheiten und Einschränkungen zu beachten. In diesem Beitrag beschreiben wir sie, zeigen ob und wie diese überwunden werden können, und wie eigene Funktionalitäten in dem Debugger selbst eingespielt und angewandt werden können.
von Marco Marazzi
Perl/Tk Skripte mit Devel::ptkdb
debuggen
Die Konfigurationsdatei .ptkdbrc
EnterLeave und LeaveActions
redefinieren
Devel::ptkdb und die
Perl/TK-Ereignisschleifen

Das Perl Debugging System
besteht grundsätzlich aus drei logischen Teilen: der Compiled Code, das Package
DB und die Funktion CORE::caller.
Der erste Teil wird im Perl
Compiled Code generiert , und dient der Erzeugung von DB::DB - Aufrufen und der Aufbereitung der entsprechenden Informationen in den
globalen Variablen des Package DB.
Der zweite Teil ist das
Package DB, welches aus den drei programmierbaren Subroutinen, nämlich DB::DB, DB::sub , DB::postponed,DB::dbeval und der bereits angedeuteten Handvoll globalen
Variablen besteht. Dieser Teil, im Beitrag der Einfachheit halber als Perl
Debugger bezeichnet, wird zusammen mit Perl geliefert, kann aber durch ein
eigenes Modul ersetzt werden. Das CPAN Modul Devel::ptkdb ist sicher das
bekannteste Perl Debugger.
Der dritte Teil ist die
Funktion 'CORE::caller', welche zwingend benutzt werden muss, um die
Position der aktuellen Zeile innerhalb des angehaltenen Skripts zu bestimmen.
Die Einzelheiten des Perl
Debuggers sind in der manpage perldebguts ausführlich beschrieben. Dieses Dokument gehört zu
der Systemdokumentation von Perl (siehe dazu [9]).
Wir wollen aber zuerst
einmal, einige der wichtigsten Begriffe und Eigenschaften des Datenmodells des
Perl Debugging Systems festhalten. Denn, getreu der Maxime dass in Perl alles
etwas anders ist, weisen diese doch einige Abweichungen zu den gängigen
Definitionen auf, die man besser nicht ignoriert.
·
Die Ausführung eines
Perl-Skriptes mit oder ohne Debugger werden wir Prozess bezeichnen. Der Prozess
ist also das Objekt des Debuggings. Der Compiled Code jeder ausführbaren Zeile
des Prozesses wird vom Perl Compiler automatisch mit Debugging Code ergänzt[1].
·
Ein Prozess wird in
einer durchgehenden Debugging Session mit dem Perl Debugging System analysiert.
Der Perl Debugger übernimmt die Steuerung der Session und kann seine Optionen,
und zum Teil deren Status, am Ende der Session persistent retten, um sie am
Anfang der nächsten Session wieder zu laden. Das Ende der Debugging Session
muss nicht das Ende des Prozesses bedeuten. Der Prozess geht weiter, DB::DB Aufrufe werden immer noch generiert, sie werden jedoch vom Perl
Debugger nicht mehr als Breakpoints bearbeitet.
·
DB::DB - Aufrufe werden immer am Anfang einer Zeile
ausgelöst, wo auch ein ausführbarer Perl - Befehl beginnt. Das ist wichtig für
die Analyse von Argumentlisten mit eingebetteten Aufrufen.
·
Das Perl Debugging
System selbst entscheidet, bei welchen Zeilen DB::DB -Aufrufe
generiert werden.
·
Demzufolge kann das
Package DB nur entscheiden, welcher der erzeugten DB::DB - Aufrufen auch als Breakpoint verarbeitet werden[2].
·
Die Subroutinen DB::DB, DB::sub und DB::postponed laufen grundsätzlich synchron zum Prozess ab. Sie
können aber auch, was auch Devel::ptkdb tut, für eine beschränkte Zeitspanne,
eine eigene Ereignisschleife einsetzten.
·
Die Variablen des
Prozesses sind unter Beachtung der Perl-Regeln nur dem Quellcode des Package DB
zugänglich , das heisst sie können gelesen und auch modifiziert werden[3].
Sogar Quellcode , der nicht zum Package DB gehört, aber vom diesem aufgerufen
wird, kann das nicht tun.
·
Breakpoints einer
Debugging Session sind untereinander unabhängig. Dennoch kann es sein,
dass DB::DB und auch DB::sub so programmiert werden, dass Daten über mehrere logisch
zusammengehörenden Breakpoints gesammelt und als ein Ganzes ausgewertet werden.
·
Jeder Breakpoint zeigt
immer den Prozessstatus vor der Ausführung der angehaltenen Zeile.
·
Breakpoints werden nur
eineindeutig durch ihre Position innerhalb der Reihenfolge ihrer Generierung
identifiziert. Ihre fortlaufende Numerierung[4]
anhand einer globalen Variablen des Package DB ist erfahrungsgemäss die
verlässlichste Methode.
Das folgende Bild zeigt
etwas vereinfacht den Ablauf der Daten und Meldungen des Perl Debugging
Systems.

Abbildung 1 DFD des Perl
Debugging System.
Nachdem wir uns mit den Grundbegriffen familiarisiert haben, können wir nun ein minimales Perl Debugger zusammenstellen, welches alle notwendigen Grundfunktionalitäten aufweist, das als Ausgangsversion für eigene Versionen dienen kann. Das folgende Bild zeigt den Quellcode, wobei jede Grundfunktionalität durch eine Subroutine mit einem selbsterklärenden Namen angedeutet wird.
package DB
;
use vars
qw(@dbline %dbline); ## source code
our
$VERSION = '1.0x'; ## version
sub
Initialize; ## set up
debugging session
sub
terminate; ## terminate
debugging session
sub
trace; ## trace DB calls
sub
filter; ## filter DB
calls
sub
createBreakpoint; ## collect
data at breakpoint time
sub
viewBreakpoint; ## visualise
collected data
my
$on; ## breakpoint
in progress
my
@watchedExpr; ## watched expressions
sub
DB::DB {
my @args = @_;
my ($package, $filename, $line, $subName)
= caller ;
$subName = '' unless defined
$subName; ## prevent warnings
return undef if ($on); ## prevent recursion
DB::initialise()unless defined($on); ## set up session
DB::trace(($package, $filename, $line); ##
trace DB::DB events
return undef unless
DB::filter($package,$filename,$line); ## filter
$on = 1; ## set status debugger is working
my $a = DB::createBreakpoint($package,$filename,$line,$subName);
DB::viewBreakpoint($a); ## visualize
breakpoint analysis (Tk)
$on = 0; ## set status off
return undef; ## back to process
}
sub
dbeval { ## get/put variables
my @args = @_; ## (package name,
variable’s name)
my $value = eval "package $args[0];
\$value = $args[1];";
return (defined $value) ? $value :'UNDEF'
unless; ## avoid warnings
}
sub sub {
## trace subroutine call
my @args = @_;
no strict;
return
&$DB::sub(@args); ## and execute
}
sub
postponed { ## process required modules
my @args
= @_;
local *dbline = $args[0];
my $fName = $dbline;
}
sub
createBreakpoint { ## create breakpoint watched expressions, stack
my ($package,$filename,$line) = @_;
local(*dbline) = $main::{'_<' . $filename} ;
my $offset = $dbline[1] =~ /use\s+.*Devel::_?ptkdb/ ?
1 : 0 ;
my $rv = {} ;
map {
## set up elements of $a, stack and watched expression
# $rv->{$_} =
DB::dbeval($package,$_);
} @watchedExpr;
return $rv; ## return full edited items
}
Abbildung
2 Minimales Perl
Debugger .
Dieses CPAN Module ist der
bekannteste Perl Debugger mit einer GUI - Oberfläche. Es ist eine reine Perl
Implementation, und besteht aus den Packages DB und Devel::ptkdb. DB
implementiert den Perl Debugger selbst , Devel::ptkdb implementiert das GUI mit
Perl/Tk .
Das Package DB sammelt die
Daten und steuert die Verarbeitung der Breakpoints. Am Anfang einer Debugging
Session, das heisst beim ersten DB::DB - Aufruf, legt es eine Instanz von Devel::ptkdb an,
und verwendet sie bis zum Ende der Session. Diese setzt den GUI auf, welcher
dazu dient den Ablauf der Session zu visualisieren und zu steuern. Der GUI
besteht aus einem Baum von Widgets, welche auf einer eigenen Instanz von
MainWindow basiert werden. Der GUI bleibt während der ganzen Debugging Session
aktiv, und wird bei jedem Breakpoint aktualisiert.
Dieses einfache Konzept ist
gleichzeitig die Stärke und die Schwäche dieser Implementation. Die Stärke
liegt in seiner Einfachheit (Wartung, Portabilität). Die Schwäche liegt in
seiner Nähe zum untersuchten Prozess, die Interaktionen und Abhängigkeiten
nicht ausschliessen kann. Die nachfolgende Abbildung zeigt die Struktur und das
Zusammenarbeiten der verschiedenen Komponenten eines Perl/Tk Skriptes während
einer Debugging Session mit Devel::ptkdb. Dabei werden die relevanten
Komponenten mit Rechtecken und die ausgetauschten Meldungen mit Pfeilen symbolisiert.

Abbildung
3 Struktur des
Prozesses mit eingebundenem Modul Devel::ptkdb.
Das Konzept sieht auch eine
ansehnliche Menge Möglichkeiten vor, den Perl Debugger an den eigenen
Bedürfnissen und Wünschen anzupassen und sogar zu erweitern. Diese Möglichkeiten
sind
-
Die Umgebungsvariablen geben alternative Werte für Optionen, welche
einzelne Eigenschaften setzen, wie Farben , Grössen, Positionen, usw.
-
die Datei .ptkdbrc ist ein Perl Skript, welches dazu dienen kann,
umfangreichere Daten im Debugger zu importieren, wie zum Beispiel Listen von
Breakpointsdefinitionen.
-
Die Callbacks Devel::ptkdb::EnterActions und Devel::ptkdb::LeaveActions werden von DB:DB am Anfang und am Schluss der Verarbeitung jedes
Breakpoints aufgerufen, und können vom Prozess überschrieben werden, um
spezifische Funktionalitäten zu
implementieren.
Als reine Perl
Implementation lebt der Debugger in der gleichen Umgebung wie der Prozess. Das
bedeutet, es beeinflusst den Prozess und kann von diesem selbst beeinflusst
werden. Das führt einerseits dazu, dass gewisse Prozesskonstrukte nur
beschränkt oder gar nicht untersucht
werden können, und andererseits, dass die Kontrolle der Debugging Session
verloren gehen kann.
Zu den Ersteren gehören
graphische Applikationen mit zeitabhängigen Prozessen, wie zum Beispiel
Animationen, zu den Letzteren gehören Subprozesse.
Der Grund liegt, wie wir
später im Detail sehen werden, in der mangelhaften Kooperation der existierenden MainWindows und entsprechenden Event
Loops (Mainloops).
Die Vorteile sind schnell
aufgezählt:
·
ptkdb basiert auf einem
sehr einfachen Konzept, und ist frei erhältlich. Das bedeutet, ptkdb kann sehr
einfach installiert und bedient werden.
·
ptkdb deckt die grundlegenden
Bedürfnisse und meidet feudale aber meistens unnötige raffinierte
Funktionalitäten. Das bedeutet, für die tägliche Arbeit braucht es keinen
Einführungskurs, sondern man kann auf seiner Grundausbildung, seiner Erfahrung
und Intuitionsvermögen zurückgreifen und mal loslegen! „Learning by doing!“ ,
wie es so schön modern heisst !
·
ptkdb arbeitet in der
gleichen Umgebung wie das untersuchte Prozess selbst. Es kann mit dem Prozess
ziemlich einfach und ungebunden kommunizieren, das heisst vor allem Informationen
aus dem Prozess extrahieren.
·
Gespeicherte
Breakpoints - Definitionen von verschobenen Zeilen werden automatisch bei der
Reinitialisierung auf die neuen Position verschoben. Zumindest wenn die
Verschiebung nicht sehr gross ist[5].
·
ptkdb kann an den
eigenen Bedürfnissen mit vertretbaren Aufwand angepasst werden.
·
Schlussendlich enthält
ptkdb interessanten Quellcode, der mit Vorteil auch in eigenen Projekten
angewendet werden kann.
Die Nachteile sind ebenfalls
schnell aufgezählt
-
ptkdb arbeitet in der gleichen
Umgebung wie das untersuchte Prozess selbst. Daraus folgt, dass die Tk-Umgebung
modifiziert wird, so dass die Anwendung in gewissen Fällen anders reagiert als
ohne Debugger. Das ist in den meisten Fällen nicht so schlimm, nämlich dann
wenn die Diskrepanz offensichtlich ist. In den übrigen nach meiner Erfahrung
wenigen Fällen ist leider mit zum Teil beträchtlichen Zeitverschwendung zu
rechnen (siehe dazu auch [1] , heisenbug).
-
Während der Breakpoints werden
die Dialoge der Anwendung nicht deaktiviert. Dies erlaubt der Anwendung
irgendwie sich selber zu überholen, so dass eine Diskrepanz zwischen dem
Zustand des Debuggers und demjenigen des Prozesses entsteht, was nicht selten
die Fortsetzung der Debugging Session sinnlos macht.
-
Der Inhalt der MainWindows des
Perl Debuggers wird immer nur beim Auftreten eines Breakpoint aktualisiert. Das
kann in einem ereignisorientierten Prozess zu verwirrenden Zuständen führen,
denn es fällt einem nicht auf, ob das angezeigte Breakpoint immer noch aktuell
ist oder schon längstens verlassen wurde.
-
Der Zustand der Debugging
Session wird nicht immer klar angezeigt. Das kommt daher, dass erstens ptkdb
kein Statusbar und keinen Protokoll seiner Aktivitäten führt, und zweitens ptkdb die Angaben zu den
dynamisch geladenen Module erst dann aktiviert und anzeigt, wenn diese auch
tatsächlich geladen worden sind. Es entsteht somit den Eindruck, dass etwas
fehlt, dabei wird es erst später nachgeführt. Ein gutes Beispiel dazu ist die Anzeige
der Breakpoint- Definitionen von
dynamisch geladenen Module. Sie werden zwar bei der Initialisierung korrekt
geladen, aber erst nach den jeweiligen require - Befehlen auch angezeigt.
-
Gespeicherte Breakpoints -
Definitionen von veränderten Zeilen werden bei der (Re)Initialisierung der
Debugging Session ignoriert, wenn die entsprechenden Zeilen verändert wurden.
Das führt zwar zu keinem Fehler, kann aber die Analyse des gerade veränderten
Quellcode erschweren, denn die dort angesetzten Breakpoints auf Grund der
Änderungen des bugfixes zum Teil oder überhaupt nicht mehr wieder gesetzt
wurden, was besonders bei aufwendigen Algorithmen sehr ärgerlich sein.
-
Die Tk - spezifische Analyse
ist durch die Koexistenz in der gleichen Umgebung nur beschränkt möglich. Das
kommt daher, dass wenn eine Analyse des TK - Systems bei einem Breakpoint, das
Prozess nicht im gleichen Zustand ist, als wenn der Prozess an der gleichen
Stelle ohne Breakpoint durchlaufen wird. In der Tat kehrt TK bei jedem
Breakpoint in den idle - modus, und hat sicher alle anstehenden TK-commands
auch bereits durchgeführt. Eine Tatsache welche an der gleichen Stelle ohne dem
aktivierten Debugger sehr wahrscheinlich nicht erfüllt ist. Das kann bei der
Analyse von TK - sensitiven Daten zu unsicheren Resultate führen, wie wir
später sehen werden.
Diese Datei wird, falls sie
existiert , während der Initialisierungsphase der Debugging Session als Bestandteil des Package DB ausgeführt.
Sie kann an folgenden Stellen sein:
-
configuration Folder $Config{'installprivlib'}/Devel/ptkdbrc,
-
Home Directory (Umgebungsvariable HOME),
-
current Working Directory.
Alle vorhandenen Dateien
werden nacheinander in der oben angegeben Reihenfolge evaluiert. Sind
syntaktische oder noch schlimmer semantische Fehler in diesen Skripten
vorhanden, so wird die Debugging Session nicht abgebrochen, sondern mit den
halbwegs definierten Optionen weitergeführt.
## ptkdb
startup script
##
$DB::ptkdb::stop_on_warning
= 0;
$DB::no_stop_at_start
= 0; ## don't stop at line 1
##
brkpt($fname, @lines); ## set
breakpoint
##
condbrkpt($fname,@($line,$expr));## set conditional breakpoint
##
brkonsub(@names); ## set
breakpoint at sub
##
brkonsub_regex(@regExprs); ## set
breakpoint at sub using regexp
add_exprs('$^O','$$','$_',
'@_',
'$@','$!','$1','$2') ; ## add items to the expression tab
textTagConfigure('stoppt', -foreground =>
"black", -background => ,'green') ;
## the
following definitions did not work!
register_user_window_init(sub{warn
' I was there...'},
'warn " I was
THERE..."');
register_user_DB_entry(sub{warn
' I was there too ...'},
'warn " I was
THERE too ..."');
## end of list
Abbildung
4 Inhalt der
Konfigurationsdatei .pkdbrc
In der Version 1.092 sind
nicht alle beschriebenen Optionen auch tatsächlich implementiert. So bleiben
die Callbacks 'user_window_init' und 'user_DB_entry' wirkungslos.
Grundsätzlich können in
diesem Skript alle syntaktisch erlaubte Perl-Konstrukte definiert werden,
welche auf dem Package DB angewandt werden können. In diesem Sinne kann man
sogar neue Subroutinen hinzufügen oder existierende ersetzen.
Diese zwei subroutinen sind
im package Devel::ptkdb sozusagen als Platzhalter kodiert worden. Man kann sie
entweder im Skript des Prozesses oder in der Konfigurationsdatei überschreiben.
Die Hauptaufgaben dieser Subroutinen könnten sein
use
strict;
use lib
'./';
use
Test::Simple tests => 1;
package
Devel::ptkdb ;
sub
get_Main_Window {
return shift->{'main_window'};
}
sub
EnterActions {{ ## deiconify ptkdb window during breakpoint
my($self) = @_ ;
my $cw = $self->get_Main_Window();
$cw->deiconify();
return; ## return value will be discarded
}
sub
LeaveActions { ## iconify ptkdb window during process
my($self) = @_ ;
my $cw = $self->get_Main_Window();
$cw->iconify(); ## OK, just to
demonstrate that it works
return; ## return value will be discarded
}
}
package
main;
sub main
{
my @args = @_;
my $rv;
ok(1);
return $rv
}
my
$status = &main::main(@ARGV);
exit(0);
Abbildung
5 Die Methoden
EnterActions und LeaveActions im Perl/Tk Skript überschreiben.
Das open source Konzept von
ptkdb, die brauchbare Dokumentation, seine überschaubare Grösse &
Komplexität und das relativ einfache GUI ermöglichen, ja laden geradezu ein zur
Implementation von eigenen Versionen dieses Moduls. Dabei kann man zuerst
einzelne Erweiterungen mit den verfügbaren Konfigurationsmöglichkeiten sammeln,
um sie dann in der eigentlichen erweiterten Version zu integrieren.
Dem alten römischen Motto
folgend ist es manchmal vorteilhaft eine mit ptkdb schwer zu lösende Aufgabe in
mehreren kleineren lösbaren Teilaufgaben aufzuteilen.
Der Einsatz von dedizierten
Test - Skripte für einzelne komplexe Komponenten, das heisst in Bezug auf Perl/Tk vor allem Megawidgets ,
Validierungen von Dateneingaben, umfangreiche background - Prozesse, helfen die
Nachteile von ptkdb auf einem erträglichen Minimum zu reduzieren. Mit anderen
Worten, man versucht einerseits eingehend die einzelnen Komponenten in
möglichst einfachen Test-Umgebungen, und andererseits ihre Kollaboration in der
realen Umgebung einzeln und nacheinander zu untersuchen. Diese Methodik kann
sowohl bei Neuentwicklungen als auch bei Redesign oder sogar
Refaktorisierungen angewandt werden.
Sie ist bei der Wartung einzelner Komponenten besonders effizient, denn nur die
veränderte Komponente und die Kollaboration untersucht werden müssen.
Nach diesen wichtigen aber
eher allgemeinen Betrachtungen, wollen wir uns nun den konkreten Aspekten der
Untersuchung von Perl/TK - Skripten zuwenden.
Das Modul ptkdb kann
angewandt werden, um sowohl den jeweiligen Zustand der TK Anwendung in seinen
statischen Komponenten zu analysieren, als auch den dynamischen Ablauf der
Ereignisse und Callbacks detailliert zu verfolgen.
Zu dem Zweck werden
Breakpoints innerhalb Callbacks definiert. Treten diese auf, so kann man
mittels TK-Messages den aktuellen Stand der involvierten Resourcen untersuchen.
In einem Perl/Tk Skript hat
das schrittweise Verfolgen des Ablaufs nur innerhalb eines Callbacks einen
Sinn. Da das ptkdb kein Protokoll der erfolgten DB::DB() - Aufrufe führt, kann das Auslösen der Ereignisse nur nachgewiesen
werden, wenn in den entsprechenden
Callbacks einen Breakpoint definiert. Wird der Ereignis ausgelöst, so wird auch
den Callback aufgerufen und den Breakpoint generiert.
Die statische Elemente der
Widgets - Parametrisierung wie
Optionen, Zuordnung der Ereignisse und Synchronisierung ( grab ) werden mit den
bekannten TK - Methoden in der ‚Data/Expression eval window’ . Leider hat ptkdb
dafür keine eigene spezialisierte Funktionen. Sie können aber einfach mit der
Konfigurationsdatei .ptkdbrc eingeführt werden.
sub
DB::listBindings {
my $cw = shift;
retunr unless defined($cw);
return unless Tk::Exists($cw);
my @rv = map {
"$_ ".join
"\n",$cw->eventInfo($_)
} ($cw->eventInfo());
return wantarray ? @rv : scalar(@rv)
}
## add
items to the expression tab
add_exprs('$^O','$$','$_',
'@_','$@','$!','$1','$2') ;
textTagConfigure('breaksetLine', -background => "red") ;
## end of list
Abbildung
6
Konfigurationsdatei .ptkdbrc mit der neuen Methode DB::listBindings()

Abbildung 7 Verwendung von DB::listBindings in der Window 'Evaluate Expressions'
Bei der Analyse des Ablaufs
von Callbacks muss man auf der Natur dieser Callbacks Rücksicht nehmen. Vom
Standpunkt des Debuggers aus gesehen, gibt es zwei wesentliche Arten von
Callbacks : synchrone und asynchrone.
Die weitaus meisten
Callbacks in einem Perl/Tk Skript laufen synchron zum Prozess ab, nur wenige
hingegen werden asynchron aufgerufen.
Bei den synchronen
Callbacks, wie wir sie bei Buttons oder Menu-items finden, ergeben sich beim Debugging keine Probleme,
da sie ziemlich isoliert arbeiten.
Bei den asynchronen
Callbacks hingegen ergibt sich ein Problem aus der Tatsache, dass Breakpoints
die Generierung weiterer TK - Ereignisse
generell nicht aufhalten, auch dann nicht wenn im Callback selbst ein
Breakpoint definiert ist. Ein Beispiel dazu ist das Widget Tk::Progressbar. Das bedeutet, dass während des Breakpoints die
Prozess-Umgebung möglicherweise verändert werden kann, was eine nicht zu
unterschätzende Unsicherheit der Datenanalyse verursacht und den weiteren
Verlauf des angehaltenen Callbacks beeinflussen kann. Das kann nur umgangen
werden, indem die Art der zugelassenen Tk - Ereignisse im Event-loop des Perl -
Debuggers limitiert wird (siehe dazu Argumentenliste der Methode DoOneEvent() und die Methode Devel::ptkdb::main_loop).
Leider kann das nicht mit den vorhandenen Konfigurierungsmöglichkeiten getan
werden.
Eine weitere Gruppe
Callbacks welche eine besondere Aufmerksamkeit verdient, ist diejenige, welche
auf Perl/Tk Datenelemente zugreift und/oder verändert.
Das hat seinen Grund darin,
dass ptkdb durch die Visuoalisierung der Breakpoints das Verhalten des
Prozesses wesentlich modifiziert.
Wird mit dem Perl Debugger
die Verarbeitung des Callbacks zeilenweise durchgelaufen, werden bei jedem
Breakpoint zusätzliche Perl/Tk Verarbeitungen eingeschoben. Um den Breakpoint anzuzeigen
führt Devel::ptkdb etliche TK - Befehle aus, so dass anschliessend das TK in
den idle - Modus zurückkehrt. Das bedeutet, alle Widgets, das heisst diejenige
des Perl Debuggers als auch jene des Prozesses sind bei jedem Breakpoint so
weit wie möglich nachgeführt worden, was bei einer ununterbrochenen
Verarbeitung nicht der Fall gewesen wäre. Es kann also durchaus der Fall
eintreten, dass ein Callback während einer Debugging Session sich anders
verhält als im normalen ungebrochenen durchlaufenden Prozess der Fall wäre.
Folgende Abbildung veranschaulicht dieses Sachverhalt. Die Verarbeitung des
Breakpoints wird im grünen Rahmen hervorgehoben. Darin sieht man die
zusätzlichen Perl/Tk Meldungen und den zusätzlichen Idle-Mode.

Abbildung
8
Sequenzdiagramm eines Beakpoints im Callback des Button main::Button.
Solche Erfahrungen habe ich
gemacht bei der Untersuchung von <Configure>-Callbacks, denn gewisse Widget-Optionen nicht synchron
gespeichert werden, sondern erst wenn DoOneEvent() gesendet wird. Das bedeutet, während der Debugging
session hat der Callback einwandfrei funktioniert, beim anschliessenden Test in
der normalen Testumgebung dann wieder nicht mehr!
Nun wie kann man einen
solchen heisenbug entlarven ? Sind Resultate einer Sequenz TK-Befehle
widersprüchlich, obwohl alle angegebenen Argumente korrekt aussehen, so dass
man annehmen kann, dass diese korrekt funktionierten, muss man daraus
schliessen, dass eher in der Debugging-Umgebung etwas nicht stimmt. In diesem Falle
wäre es ratsam, update-Meldungen einzubauen, um genau dort von Perl/Tk den
idle-Status zu forcieren, wo man ihn auch erwartet.
Eine weitere Methode besteht
darin, Breakpoints an verschiedenen Orten des Callbacks zu verschieben, und die
Zuständen vergleichen. Man wird dann bei gewissen Breakpoints Widersprüche
sehen, die dann verschwinden ohne dass man eine passende Erklärung nachweisen
kann. Man sieht, dass die Zustände ändern sich mit der Lage der Breakpoints,
was ein perfekter Unsinn ist!
Eine letzte bewährte Methode
, besteht darin trace statements einzufügen und nur einen Breakpoint am Schluss
des Callbacks zu setzen. Somit läuft der Callback ungestört durch, und die
Wahrscheinlichkeit steigt, dass es so läuft wie wenn kein Debugger da gewesen
wäre. Zugegeben, es klingt etwas altmodisch , aber es bringt brauchbare
Resultate.
Werden in Callbacks Ausnahmen ausgelöst so werden sie von Perl/Tk
abgefangen. In der Tat werden Callbacks evaluiert, so dass Ausnahmen nur den
Callback selbst terminieren. Das ist unter ptkdb nicht anders. Der Perl
debugger benimmt sich in solchen Fällen
völlig passiv, so dass man die Verarbeitung der Ausnahme im Detail verfolgen
kann. Einen solchen Ablauf endet in der aktuellen Ereignisschleife. Die flgenden
Bilder zeigen drei Aspekte des Ablaufs einer Ausnhame: den Breakpoint innerhalb
des Perl/Tk die-Callback, den Stack der aktiven Blocks bzw. Subroutinen und den
Breakpoint in der Ereignisschleife des Prozesses als Abschluss der
Ausnahmeverarbeitung.

Abbildung 9 Behandlung der
Ausnahme durch Perl/Tk.

Abbildung 10 Stack der
Behandlung der Ausnahme.

Abbildung 11 Nach der Ausnahme
kehrt die Kontrolle zur Ereignisschleife.
Selbstverständlich muss die
schrittweise Analyse der Ausnahmebehandlung mit einem ptkdb – Kommando ‚run’ abgeschlossen werden, damit die Ereignisschleife des Perl Debuggers
verlassen wird. Das kann verwirrend sein, wird doch die Windows des Perl Debuggers
erst beim nächsten Breakpoint aktualisiert.
Grundsätzlich können auch
Perl/Tk - Module selbst untersucht werden, soweit es sich um Perl Code handelt.
Die darunter liegende TK - Umgebung bleibt immer völlig verborgen. Mit anderen
Worten, nur die Perl/Tk Schnittstelle kann mit ptkdb untersucht werden, und
zwar:
-
die Argumentenlisten der
Perl/Tk-Befehle,
-
den Ablauf der Perl/Tk Module,
-
die Vererbungsstruktur der
Perl/Tk Widgets,
-
die lokalen und globalen
Variable, und
-
die return - Values der
Aufrufe.
Das ist nicht sehr viel,
doch schon eine ganze Menge, lässt einem undokumentierte nützliche Optionen und
Methode entdecken, und erklärt schon manche rätselhaften Verhalten von Perl/Tk.
Erfahrungsgemäss sind bei solchen Aufgaben eine gehörige Portion Geduld von Nöten, sind doch die Abläufe im Perl/Tk
ziemlich verschlungen.
Besonders interessant ist
die Anwendung des Module B::Deparse zur Kontrolle von Callbacks –Definitionen in
Widgets-Optionen und Referenzen des Typs ‚CODE’.

Abbildung
12 Quellcode
eines Callbacks im Expression tab anzeigen.
Eine besondere Beachtung
muss der Untersuchung von modalen Dialoge zugeteilt werden. Diese Dialoge
basieren auf die Widgetsklassen Tk::DialogBox, welche zur Erstellung des modalen Status einen
lokalen grab einsetzten. Nun, da dieser grab
seine Wirkung auf seine eigene MainWindow beschränkt, bleiben die Dialoge des
Perl Debuggers frei verfügbar. Somit kann man während eines modalen Dialog die
Debugging Session unbehelligt weiter steuern. Umgekehrt, will man die Perl
Debuggers Dialoge modal machen, so dass alle anderen Dialoge während eines
Breakpoint gesperrt werden, dann muss man vom Perl Debugger aus einen globalen grab
einrichten, welcher Gültigkeit für alle im Prozess vorhandenen MainWindows hat.
Dazu kann man die Methoden Devel::ptkdb::EnterCallaback und Devel::ptkdb::LeaveCallback verwenden, wie in der folgenden Abbildung gezeigt
wird.
package
Devel::ptkdb ;
{
my
$old_focus;
my
$old_grab;
sub
EnterActions {
my($self) = @_ ;
my $cw = $self->get_Main_Window();
$old_focus = $cw->focusSave;
$old_grab = $cw->grabSave;
$cw->grabGlobal();
}
sub
LeaveActions {
my($self) = @_ ;
my $cw = $self->get_Main_Window();
$cw->grabRelease;
&$old_grab() if defined $old_grab;
&$old_focus() if defined $old_focus;
$old_grab = undef;
$old_focus = undef;
}
}
package main;
Abbildung
13
Prozessdialoge sperren im Perl Debugger .
Perl/Tk - Anwendungen können
mit der Behandlung der TK - Ereignisse in mehreren Arten umgehen. Die weitaus
meisten Skripten wenden die einfachste Art, sie verlassen sich auf die
Subroutine Tk::MainLoop. Weiter senden sie bei spezifischen Situationen eine
Meldung ‚update’ zur MainWindow , um alle Widgets zu aktualisieren,
und setzen eventuell den DoWhenIdle
– Callback, um eine bestimmte
Verarbeitung beim Erreichen des nächsten idle – Mode ausführen zu lassen .
Komplexere Skripten hingegen
setzen mehrere MainWindows und modifizierte Ereignisschleifen auf, und zwar je
nach Anforderungen der einzelnen Anwendungsfunktionen. In diesem Falle geht es
darum, spezialisierte zyklische Verarbeitungen im idle - Mode auszuführen,
welche nicht mit den DoWhenIdle-
Callback möglich sind.
Muss man auf die Liste der
MainWindows zugreifen, so stellt das Modul Tk::MainWindow die Meldung Tk::MainWindow::Existing zur Verfügung, welche die Liste der zur Zeit aktiven
Instanzen liefert. In der Tat speichert dieses Modul diese Liste in einer
lokalen Klassenvariable.
package
main;
sub
preprocess {
## do some preparing stuff
}
sub
guiProcess { ## and go GUIsh
use Tk;
my $mw = MainWindow->new();
my $term = sub {$mw->destroy()}; ##
terminate Mainwindow instance
$mw->Button(-text, ‘Exit’, -command ,
$term);
$mw->protocol(‘WM_DELETE_WINDOW’,$term); ## term mw
MainLoop; ## set up and execute GUI
return 1
}
sub
postProcess {
## process what the GUI gathered
}
main::preProcess();
main::guiProcess();
## one or more GUIs
main::postProcess(); ## cannot be debugged
exit;
Abbildung 14
Verallgemeinerte Struktur eines Perl/Tk Prozesses
Nach diesen allgemeinen
Betrachtungen untersuchen wir im Detail jeden einzelnen Fall .
Fangen wir beim einfachsten
und häufigsten Fall an. Der Prozess braucht nur eine MainWindow und setzt ein
einziges MainLoop auf.
Während der Debugging
Session addiert das Modul ptkdb seine eigene MainWindow dazu, und beeinflusst
also indirekt die Tk-Umgebung des Prozesses. Das hat den Vorteil, dass die
Widgets des Perl - Debuggers abgekoppelt von denjenigen des Prozesses sind,
andererseits wird das MainLoop des Prozesses, dadurch beeinflusst, dass es
nicht endet wenn nur seine MainWindow terminiert wird, sondern erst dann wenn
auch der Debugger beendet wird[6].
Will man dieses Problem aus
dem Weg gehen, so muss man die Referenz der bereits existierenden MainWindow
dem Prozesses zugänglich machen, indem man entweder eine globale Variable $main::mw einrichtet. Die folgende
Abbildung zeigt wie man das implementieren könnte.
package
main;
use Tk;
my $debug = 1;
our $mw;
$mw = MainWindow->new() unless defined $mw &&
Tk::Exists($mw);
# $mw->myToplevel(); ## instantiate GUI
MainLoop(); ### and loop
CORE::exit(0);
Abbildung
15 Referenz zur
Mainwindow mit globaler Variable kondividieren
Eine andere Möglichkeit
besteht darin mit der Message Tk::MainWindow::Existing die Referenz einer bereits existierenden MainWindow
zu ermitteln und verwenden, wie die flgende Abbildung zeigt.
package
main;
use Tk;
my $mw = MainWindow->new() unless
Tk::MainWindow->Count;
$mw = (Tk::MainWindow::Existing())[0]
unless defined $mw;
# $mw->myToplevel(); ## instantiate GUI
MainLoop(); ### and loop
CORE::exit(0);
Abbildung
16 Liste der
existierenden mainWindow
Das hat aber auch eine
andere Konsequenz für das Debugging selbst . Soll der Prozess nach dem Schliessen
seiner MainWindow eine Post-Verarbeitung ausführen, so braucht es nur die
Instanz der MainWindow zu löschen, und das MainLoop terminiert selbsttätig.
Gerade das funktioniert aber während der Debugging Session nicht, denn die
MainWindow des Perl Debuggers selbst
immer noch aktiv ist, was das MainLoop des Prozesses nicht terminieren lässt.
Das bedeutet, dass die Post-Verarbeitung überhaupt nicht mit ptkdb untersucht
werden kann. Will man das tun, so muss man das MainLoop des Prozesses selbst
adaptieren, wie in der nächsten Abbildung vorgeschlagen wird.
sub
myMainLoop { # return when given MainWindow has been destroyed
my $mw = shift;
no strict;
unless ($inMainLoop) { ## prevent recursion
local $inMainLoop = 1;
while (grep($mw == $_,
(Tk::MainWindow::Existing()))){
DoOneEvent(0);
}
}
}
sub
runGUIprocess {
my $mw = MainWindow->new();
$mw->Button(-text,'Test',-command,[\&doTest,$mw])->pack();
$mw->Button(-text,'Exit
guiProcess',-command,sub{$mw->destroy})->pack();
$mw->Button(-text,'Exit
process',-command,\&main::do_exit)->pack();
main::myMainLoop($mw);
return 1
}
Abbildung
17 Adaptiertes
Event loop des Prozesses
Wenden wir uns jetzt dem
Fall zu, wenn der Prozess mit mehreren MainWindows und mehreren eigenen
Ereignisschleifen anwendet.
ptkdb kann via Breakpoint
diese Ereignisschleifen unterbrechen, und seine eigene Verarbeitung und GUI
durch seine eigenen Ereignisschleife steuern.
Wird die Verarbeitung eines
Breakpoint beendet, so lässt ptkdb
seine eigene Ereignisschleife terminieren, und die jeweilige
unterbrochene Ereignisschleife wird weitergeführt.
Vergessen wir nicht ,dass in
einer Perl/Tk Anwendung zu einem bestimmten Zeitpunkt nur eine einzige
Ereignisschleife aktiv sein kann, und dass sie alle anfallenden und
zugelassenen Ereignisse verarbeitet.
Das bedeutet also, dass auch
dann, wenn der Ablauf des Prozesses durch einen Breakpoint angehalten ist, das
TK System vollkommen weiterarbeitet.
Die folgende Abbildung zeigt
exemplarisch den Einsatz einer spezialisierten Ereignisschleife, in diesem
Falle diejenige des Perl debuggers ptkdb.
sub main_loop {
my ($self) = @_ ;
my ($evt, $str, $result) ;
my $i = 0;
SWITCH: for ($self->{'event'} = 'null' ; ; $self->{'event'}
= undef ) {
Tk::DoOneEvent(0);
next unless $self->{'event'} ;
$evt = $self->{'event'} ;
$evt =~ /step/o && do { last SWITCH ; } ;
$evt =~ /null/o && do { next SWITCH ; } ;
$evt =~ /run/o && do { last SWITCH ; } ;
$evt =~ /quit/o && do { $self->DoQuit ; } ;
$evt =~ /expr/o && do { return $evt ; } ; # adds an expression
to our expression window
$evt =~ /qexpr/o && do { return $evt ; } ; # does a 'quick'
expression
$evt =~ /update/o && do { return $evt ; } ; # forces an update
on our expression window
$evt =~ /reeval/o && do { return $evt ; } ; # updated the open
expression eval window
$evt =~ /balloon_eval/ && do { return $evt } ;
} # end of switch block
return $evt ;
} # end of main_loop
Abbildung
18 Das event
loop von Devel::ptkdb
Wie man sieht, die
Ereignisschleife des Debuggers führt nach der Verarbeitung der TK - Ereignisse
durch DoOneEvent() seine eigene Verarbeitung durch. Es bricht die
Schleife erst dann ab, wenn eine Fortsetzung des Prozesses verlangt wird.
Zur Zeit sind zwei Versionen des Moduls pktdb
erhältlich: auf CPAN steht seit
geraumer Zeit die Version 1.1091 zur Verfügung. Auf Sourceforge wird die
Version 1.1092 angeboten, welche aber im wesentlichen derjenige von CPAN
entspricht (siehe dazu [8]).
Nach meiner Erfahrung scheinen beide nicht mehr aktiv
gewartet zu werden.
Das Modul ptkdb sollte auch
mit Perl 5.10 zuverlässig funktionieren, ist doch das Perl Debugging System
praktisch unverändert geblieben.
Eine interessante Alternative zu ptkdb ist das CPAN Modul ebug. In der Tat dieses Modul versucht das Prozess mit Hilfe einer client/server Architektur möglichst wenig zu beeinflussen. Mehr dazu siehe [7].
Das CPAN Modul Devel::ptkdb
ist trotz seiner systemischen Schwächen gut brauchbar, um den Anwendungsteil
von Perl/Tk Skripten zu untersuchen.
Das sind die Variablen, den
Ablauf der anwendugsorientierten Callbacks und weitaus den meisten
anwendungsrelevanten Daten der Tk - Umgebung. Das sind die Widget Optionen, die
Bindings und den Synchronisationsobjekt ‚grab’.
Mit erweiterten Versionen
kann man einiges mehr erreichen, auch wenn die grundlegenden Schwächen kaum
ausgebügelt werden können.
Wendet man aber alle
geschilderten Möglichkeiten an, also mit einer projektbezogene Konfigurierung,
Debugging - freundliche Programmierung und dedizierten Komponenten
-Testumgebungen, ist es möglich mit ptkdb Resultate zu erreichen, welche
durchaus mit denjenigen aus dem Einsatz von renommierteren und teueren Debugger
verglichen werden können.
[1] Paul Butcher, 2009, Debug It!
[2] Steve Lidie, Nancy Walsh, 2002, Mastering Perl/Tk
[3] Peter Scott, Ed Wright, 2001, Perl debugged
[4] Brian D Foy, 2007, Mastering Perl
[5] Mark Jason Dominus, 2005, Higher Order Perl
[6] Richard Foley, 2004 , Perl Debugger, Pocket Reference
[7] Leon Brocard, 2008, A simple, extensible console Perl debugger
http://search.cpan.org/~lbrocard/Devel-ebug-0.49/
[8] http://sourceforge.net/projects/ptkdb/
[9] manpage perldebguts, http://perldoc.perl.org/perldebguts.html
[10] manpage perldebug, http://perldoc.perl.org/perldebug.html
[1] Das sind im wesentlichen Aufrufe des
Perl Debuggers DB::DB, DB::sub und DB::postponed.
[2]
In der Subroutine DB::DB muss ein dazu zweckmässiges Filter programmiert
werden.
[3]
Das betrifft vor allem die lexikalisch deklarierten Variablen.
[4]
Das Perl Debugging System liefert keine Identifikation. Sie muss im Perl
Debugger programmiert werden.
[5]
In der Version 1.092 beträgt diese Distanz in beiden Richtungen 20
Zeilen . Leider ist sie keine parametrisierbare Option, sondern eine Konstante
in der Methode DB:: fix_breakpoints.
[6] MainLoop terminiert wenn keine
Instanz des Typs MainWindow mehr existiert.