Perl/Tk Skripte mit Devel::ptkdb debuggen

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

Inhaltsverzeichnis

Perl/Tk Skripte mit Devel::ptkdb debuggen.. 1

Inhaltsverzeichnis. 1

Grundlagen. 2

Das Modul Devel::ptkdb. 5

Grenzen von ptkdb. 6

Vor- und Nachteile von ptkdb. 7

Das Modul ptkdb konfigurieren. 9

Die Konfigurationsdatei .ptkdbrc. 9

EnterLeave und LeaveActions redefinieren. 9

Das Modul ptkdb erweitern. 10

Divide et impera. 10

Perl/Tk - Skripte untersuchen. 11

Callbacks analysieren. 12

Ausnahmen in callbacks. 14

Perl/Tk Resources untersuchen. 16

Modale Dialoge. 17

Devel::ptkdb und die Perl/TK-Ereignisschleifen. 19

Ausblick. 22

Schlussfolgerungen. 22

Literatur. 23

 


Grundlagen

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 .


Das Modul Devel::ptkdb

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.

Grenzen von ptkdb

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).

Vor- und Nachteile von ptkdb

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.


Das Modul ptkdb konfigurieren

Die Konfigurationsdatei .ptkdbrc

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.

EnterLeave und LeaveActions redefinieren

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 Modul ptkdb erweitern

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.

Divide et impera

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.


Perl/Tk - Skripte untersuchen

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'

Callbacks analysieren

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.

Ausnahmen in callbacks

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.

Perl/Tk Resources untersuchen

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.

Modale Dialoge

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 .


Devel::ptkdb und die Perl/TK-Ereignisschleifen

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.


Ausblick

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].

Schlussfolgerungen

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.


Literatur

 

[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.