ErrorSys.prg

Aus Wiki des Deutschsprachige Xbaseentwickler e. V.
Zur Navigation springen Zur Suche springen


ErrorSys.prg

"Meine Programme haben keine Fehler, höchstens programmtechnische Ungenauigkeiten oder programmlogische Unschärfen." (Zitat eines mir bekannten Programmierers)

Weil es immer wieder Situationen gibt, in denen der normale Programmverlauf nicht fortgesetzt werden kann, wurde in Clipper die ErrorSys.prg eingeführt. Zum Programmstart wird eine Funktion definiert, die bei Laufzeitfehlern aufgerufen wird, und in welcher der Programmierer die Möglichkeit hat, gezielt auf unterschiedliche Fehler zu reagieren.


Vorsicht, Loop

Anpassungen an die ErrorSys sollten vorsichtig und nur gut getestet vorgenommen werden. Wenn in der Fehlerbehandlung erneut ein Fehler auftritt, kommt man schnell in einen Loop, bis der Stack überläuft.

Beispiel: es soll ein Fehlerprotokoll geschrieben werden, aber die Protokolldatei kann nicht geöffnet werden (weil sie schon exclusiv geöffnet ist, die erforderlichen Rechte fehlen, die Datei nicht angelegt werden kann etc.). Wird das nicht abgefangen, wird innerhalb der Fehlerbehandlung erneut die Fehlerbehandlung angestossen.

Der Master-Code

Unter \XppW32\Source\Sys findet sich der Quellcode zur ErrorSys.prg. Dieser Code kann angepasst und dann modifiziert in eigenen Programmen verwendet werden.


Aufbau der ErrorSys.prg

Es muss sichergestellt sein, dass bestimmte Punkte definiert sind:


ErrorSys()

Der Laufzeitstandard sieht vor, dass zu Programmbeginn die Funktion ErrorSys() aufgerufen wird. Wenn diese in keinem vom Programmierer definierten Modul vorhanden ist, wird die ErrorSys() aus dem zur Runtime gehörenden DLLs geladen, und wir bewegen uns auf dem Standard-Pfad.

Die Funktion ErrorSys() dient dazu, dass dem Laufzeitsystem mitgeteilt wird, welche Funktion sich um Fehler standardmässig kümmert (es besteht jederzeit die Möglichkeit, eine andere Funktion zuzuweisen):

Function ErrorSys()                             
   ErrorBlock({|oError| StandardEH(oError)})             
Return(.T.)

Diese Zuweisung registriert die Funktion StandardEH() als Standard Error Handler, und legt fest, dass ein Parameter erwartet wird, der bei Eval() angegeben werden muss. Standardmässig ist dies ein Objekt der Error()-Klasse.

Theoretisch kann der Standard Error Handler auch in einem anderen Programm definiert werden, es ist aber allgemein üblich, die Fehlerbehandlungsroutinen in einer Quelle zu hinterlegen.


StandardEH()

Es kann auch ein anderer Name gewählt werden, nur muss dieser in der Funktion ErrorSys() hinterlegt sein.

In der StandardEH() gibt es verschiedene Konstellationen, die geprüft werden müssen.

Entscheidend ist der Parameter, der von der Laufzeitumgebung an StandardEH() übergeben wird, es handelt sich hierbei um ein Error() Objekt.

Die Instanzvariable :genCode liefert Hinweise auf die Art des Fehlers und kann verwendet werden, um auf den Fehler zu reagieren.


automatische Reaktion auf Fehler

Es gibt derzeit drei Fehler, auf die automatisch reagiert wird.

Man mag sich jetzt fragen, warum dann die Laufzeitumgebung diese drei Fehler nicht automatisch bearbeitet?

Das von Alaska vorgegebene Verhalten ist ein Vorschlag, der vom betreffenden Programmierer (oder den Programmierrichtlinien) anders gesehen wird.

Die automatische Beantwortung einer Divison durch Null kann zu ungewollten Effekten führen, oder es gibt eine Vorschrift, dass vor der Division der Divisor zu prüfen ist - dann ist eine Divison durch Null ein Hinweis, dass eine Vorschrift nicht eingehalten wurde und bedingt entweder einen Programmabbruch oder einen expliziten Eintrag in einem Fehlerprotokoll.

Gründe sind viele denkbar: darum ist das Verhalten der StandardEH ein Vorschlag, der vom Programmierer an die eigenen Bedürfnisse angepasst werden kann.


Division durch Null / XPP_ERR_DIVZERO

Selbst heute ich das Ergebnis einer Division durch Null nicht definiert, und jeder Programmierer weiss, dass vor einer Division der Divisor auf Null zu prüfen ist, aber vergessen wird es trotzdem immer wieder. Die Laufzeitumgebung reagiert da abwehrend, erstellt ein Fehler-Objekt und führt den Standard-Errorhandler aus.

Das Standardverhalten in diesem Fall ist, den Wert Null zurückzugeben, der dann als Ergebnis einer Division durch Null verwendet wird. Mathematisch nicht ganz sauber, aber wer als Programmierer so etwas zulässt, der schluckt auch grössere Kröten.

Bei diesem Fehler kommt nur der :genCode zum Tragen, bei den beiden folgend dokumentierten Fehlerursachen werden weitere Informationen herangezogen:


Fehler beim Öffnen einer Datei im Netzwerk / XPP_ERR_OPEN

Bei der Behandlung dieses Fehlers kommen weitere Instanzvariablen von Error() zum Tragen, nametlich :osCode und :canDefault.

Der in :osCode abgefrage Betriebssystemfehler 32 weist auf eine "sharing violation" hin [Quelle[1]], d.h. ein anderer Prozess hat die Datei so geöffnet (nicht immer zwingend exclusiv), dass das Programm die Datei nicht wie gewünscht öffnen kann. (Natürlich kann dieser Fehler heutzutage auch in einem PC auftreten, der nicht an ein Netzwerk angeschlossen ist, wofür haben wir Multitasking?)

Entscheidend ist dann noch das Votum der Instanzvariablen :canDefault. Diese Variable gibt an, ob ein weiterer Versuch unternommen werden kann, die Anweisung, die zum Fehler geführt hat, erneut auszuführen.

Weshalb hier in der Standard-ErrorSys.prg ein FALSE zurückgegeben wird, ist leider nicht dokumentiert.


Fehler beim APPEND eines Satzes / XPP_ERR_APPENDLOCK

Zum Anhängen eines neuen Datensatzes muss die Datei kurzfristig gesperrt werden. Ist dies nicht möglich, wird nach einer kurzen Wartezeit dieser Fehlercode gesetzt und die Fehlerbehandlung angestossen.

Wenn :canDefault eine Wiederholung zulässt wird die Fehlerbehandlung ebenfalls abgebrochen und mit der normalen Programmausführung fortgefahren (ebenfalls durch die Rückgabe von FALSE).


nicht automatisierte Fehlerfälle

Die weitere Fehler werden - abhängig von den Instanzvariablen :canDefault und :canRetry - mittels einer variablen Abfrage behandelt.


Fehler-Reporting

Neben der Information an den Anwender, dass etwas schief gelaufen ist, produziert die Standard-ErrorSys.prg auch ein (sich automatisch überschreibendes) Protokoll namens XppError.LOG (der Name kann über eine Änderung der #DEFINE-Anweisung angepasst werden), in dem wichtige Eckpunkte enthalten sind.

Wenn das Protokoll in anderer Form, z.B. in einer Datei abgelegt werden soll, muss besonders viel Sorgfalt aufgewandt werden, da wir bereits in einer Fehler-Routine sind, und unsaubere oder unklare Programmierung möglicherweise einen weiteren Fehler produziert.

Änderungen an dieser Stelle sind also zwingend auf ihre Robustheit zu prüfen!

Das Standard-Log umfasst folgende Informationen:

- Name und Pfad der Anwendung, sowie Datum und Uhrzeit des Fehlers - Xbase Version - Betriebssystemversion - den Inhalt von Error() - den Callstack des aktuellen Thread, in dem der Fehler aufgetreten ist

Denkbare Ergänzungen: eine Übersicht der aktuell offenen Dateien, mit Satzzahl, Recordpointer, Zugriff, etc.; eine Auflistung der anderen Threads (tiefergehende Informationen sind derzeit aus anderen Threads nicht vernünftig abrufbar).


House-Keeping

Xbase++ führt ein implizites dbCloseAll() am Programmende durch. Was aber, wenn andere Datensysteme verwendet werden, oder externe Komponenten?

SQLExpress z.B. besitzt die Möglichkeit, die vorhandenen Verbindungen abzufragen und diese dann gezielt zu schliessen.

Den MySQL-Wrapper von Hector Peroza habe ich so erweitert, dass auch hier eine Abfrage der offenen MySQL-Connections möglich ist, um diese vor dem Programmende schliessen zu können.

Genauso betroffen sind noch offene Transaktionen, bei denen explzit ein ROLLBACK ausgelöst werden kann.

Eine weiterer Punkt sind die AutomationObject() Verbindungen zu Office-Programmen. Werden diese nicht sauber geschlossen, verbleibt die Instanz des entsprechenden Programms im Speicher und bedingt in manchen Fällen ein etwas abweichendes Verhalten, wenn das Programm zur normalen Bearbeitung gestartet wird.

Es lohnt sich also, im Rahmen der Fehlerbehandlung auch ein wenig Hausputz zu betreiben und solche offenen Verbindung zu prüfen und zu schliessen.