Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Sonstige Fragen zu Delphi (https://www.delphipraxis.net/19-sonstige-fragen-zu-delphi/)
-   -   Delphi Ursache für hängende Applikation herausfinden (https://www.delphipraxis.net/214551-ursache-fuer-haengende-applikation-herausfinden.html)

Bodenseematze 29. Jan 2024 09:32

Ursache für hängende Applikation herausfinden
 
Hallo,

ich habe aktuell ein sehr seltsames Problem.
Eine meiner Applikationen bleibt immer an einer von zwei Stellen hängen und lässt sich nur durch abschießen beenden.
Die eine Stelle soll eine Datei von einem Verzeichnis in ein anderes kopieren.
Das Zielverzeichnis ist ein kurz vorher erzeugtes Unterverzeichnis im Temp-Verzeichnis.

Hier bleibt immer der eigentliche "Kopieraufruf" hängen - dabei ist es egal, ob ich eine Implementierung von CopyFileW, IFileOperation oder ShFileOperation verwende... :roll:

Und der ähnlich gelagerte Aufruf (Kopieren einer Datei aus dem selbem Quellverzeichnis in ein generiertes Temp-Verzeichnis) klappt in einer anderen Applikation völlig problemlos.

Die zweite Stelle, an der es immer wieder mal hängen bleibt, ist eine Datenbankabfrage.
Auch diese ist sowohl im selben als auch in anderen Programmen in gleicherweise verwendet und funktioniert dort problemlos (zu 80% auch in besagtem Programm - nur eben manchmal nicht...)

Ich bin mir also ziemlich sicher, dass das eigentliche Problem nicht an den Aufrufen liegt, bei denen das Programm hängen bleibt, sondern vermutlich irgendwo ganz anders und sich nur so auswirkt.
Ich habe aber keine Ahnung, wie ich dem Problem auf die Spur kommen soll...

Das ganze mit Delphi 7 Enterprise, Betriebssyteme (VMs) von Win7 - Win10 wurden ausprobiert; überall das selbe Verhalten.
FastMM4, MadExcept etc. sind auch integriert - melden aber (bis zum Hänger) auch nichts...

Richtig debuggen kann ich schon lange (mehrere Jahre) nicht mehr - die Delphi IDE bleibt bei mir hängen, wenn ich versuche eine Applikation im Debugger zu starten - auch, wenn ich es über Remote-Debugging auf dem gleichen Rechner versuche (das funktioniert manchmal noch, aber meistens bleibt es auch hängen, wenn ich mich auf den laufenden Prozess setze).
Mein "debuggen" passiert also ausschließlich über Log-Ausgaben - und hier kann ich eben noch "vor" dem Datei-Kopieraufruf loggen und die Ausgabe direkt danach kommt nichts mehr... :(

blawen 29. Jan 2024 09:43

AW: Ursache für hängende Applikation herausfinden
 
Meine Glaskugel ist aktuell in der Reparatur, insofern kann ich nur im Nebel stochern ;-)

Ohne etwas Code ist es schwierig Tipps zu geben.
Im Falle "Datei kopieren" tippe ich darauf, dass die Pfadangabe fehlerhaft ist - sehr oft dürfte der Fehler bei falsch gesetzten "\" liegen.
Daher würde ich den Pfadstring ausgeben lassen und vergleichen.

Bei der Datenbankabfrage dürfte es ev. darauf hinauslaufen, dass entweder der Server nicht immer erreichbar ist oder dass die SQL-Abfrage (?) Fehlerbehaftet ist.
(z.B. Timeout-Fehler, weil die Abfrage zu lange dauert)

Wie geschrieben, ohne Code bleibts beim Spekulieren.

Bodenseematze 29. Jan 2024 10:18

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von blawen (Beitrag 1532698)
Ohne etwas Code ist es schwierig Tipps zu geben.

das ist mir schon klar, der Quellcode der "Hänger"-Stelle dürfte hier aber nichts bringen, da es wie gesagt, auch bei unterschiedlichen Kopier-Varianten zu dem Hänger kommt (und bei einem anderen Programm, das einem ähnlichen Ablauf folgt, nicht passiert).

Ich habe auch schon ein Testprogramm geschrieben, das nur diesen Aufruf simuliert und ihm die exakt gleichen Pfade gegeben.
Da funktioniert es ohne Probleme.

Zitat:

Zitat von blawen (Beitrag 1532698)
Im Falle "Datei kopieren" tippe ich darauf, dass die Pfadangabe fehlerhaft ist - sehr oft dürfte der Fehler bei falsch gesetzten "\" liegen.

Wenn es daran liegen würde, müsste der Aufruf doch mit einem Fehler / Exception zurück kommen...

Im aktuell verwendeten Code wird ein IFileOperation-CopyItem Aufruf durchgeführt, als Quellpfad ist der absolute Pfad zu einer Datei angegeben (
Code:
C:\Entw\Reports\MyReport.rpt
) als Ziel ein Verzeichnis (
Code:
C:\Temp\VoH33732038\
).
Es wurde auch geprüft, dass die Quelldatei und das Zielverzeichnis existieren (und es sich im Falle des Ziels auch um ein Verzeichnis handelt).
Teswtweise ist als einziges Flag "FOF_NOCOPYSECURITYATTRIBS" angegeben.
Wie gesagt, der gleiche Aufruf in einem Testprogramm funktioniert einwandfrei...


Zitat:

Zitat von blawen (Beitrag 1532698)
Bei der Datenbankabfrage dürfte es ev. darauf hinauslaufen, dass entweder der Server nicht immer erreichbar ist oder dass die SQL-Abfrage (?) Fehlerbehaftet ist.
(z.B. Timeout-Fehler, weil die Abfrage zu lange dauert)

Wie geschrieben, ohne Code bleibts beim Spekulieren.

Da es mit den gleichen SQL-Aufrufen meistens funktioniert, bin ich mir (zimelich) sicher, dass hier kein Fehler vorliegt / vorliegen kann...

Ich habe irgendwie das Gefühl, dass es sich um irgendwelche Speicherüberschreiber handelt, die an einer ganz anderen Stelle passieren...
Wenn ich das Programm ausserhalb der IDE starte und zu dem Hänger "bringe" und mich dann über die IDE auf das laufende Programm setze ("Attach to process") und dann versuche, im angehaltenen Prozess auf das Debug-Fenster "Event Log" zu schalten, steht dort folgendes:
Code:
ODS:   *** A stack buffer overrun occurred in .\MyProc /LOG=Full AU 95211 01:  Process MyProc.exe (16344)
ODS: This is usually the result of a memory copy to a local buffer or structure where the size is not properly calculated/checked. Process MyProc.exe (16344)
ODS: If this bug ends up in the shipping product, it could be a severe security hole. Process MyProc.exe (16344)
ODS: The stack trace should show the guilty function (the function directly above __report_gsfailure). Process MyProc.exe (16344)
ODS: *** enter .exr 77453DC8 for the exception record Process MyProc.exe (16344)
ODS: *** then kb to get the faulting stack  Process MyProc.exe (16344)
Das deutet doch auf Stack-Probleme / -Überschreiber hin, oder?
Aber was soll mir diese 'Hilfe' in dem Fenster jetzt sagen? Ich habe keine "Stack Trace - Datei" (die ich unterr "Stack Traces" laden könnte) und wo soll ich ".exc 777453DC8" eingeben? Und wo "kb"?

Delphi.Narium 29. Jan 2024 10:46

AW: Ursache für hängende Applikation herausfinden
 
Hast Du mit ausführlicher MAP-Datei kompiliert? Eventuell findest Du dort eine Adresse, die dem angegebenen Wert "ähnlich" ist, dann könnte der Fehler in der Nähe des dort angebenen Bereiches zu finden sein.

Bekommst Du z. B. als Fehleradresse 002D22B4, dann könntest Du in der MAP-Datei die Zeilennummer im Quelltext finden, die das Problem verursacht.
Code:
Line numbers for Unit1(..\Unit1.pas) segment .text

   608 0001:002D22B4   608 0001:002D22BB
D. h.: In dem Beispiel wäre eventuell bei Zeile 600 in der Unit1 eine Fehlerursache zu finden.

Schau bitte einfach mal in der MAP-Datei nach, ob Du die Zeichenfolge :77453DC8 findest. Wenn nicht, versuche es mit :77453DC ..., bis Du was findest oder eben auch nicht.

Ab und an komme ich mit dieser "Suchmethode" schonmal weiter, manchmal jedoch auch nicht und es kann sehr mühsam werden :-(

Ob das jetzt ein professionelles Vorgehen ist, weiß ich nicht, aber wenn es hilft ;-)

Bodenseematze 29. Jan 2024 14:03

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Delphi.Narium (Beitrag 1532701)
Hast Du mit ausführlicher MAP-Datei kompiliert? Eventuell findest Du dort eine Adresse, die dem angegebenen Wert "ähnlich" ist, dann könnte der Fehler in der Nähe des dort angebenen Bereiches zu finden sein.

Nein, leider nicht - die Adressen in der MAP-Datei sind alle wesentlich niedriger (eher so, wie Dein Beispiel...)

Ich habe jetzt mal MadExcept mit Option "check for frozen main thread" mitlaufen lassen.
Nachdem dieser den Hänger feststellt, zeigt er mir folgenden Stack an:
Code:
7736FF74  +0044  ntdll.dll          RtlEnterCriticalSection
4002FE37  +0007  rtl70.bpl Classes  ThreadList.LockList
01984929  +015D vcl70.bpl Controls TWinControl.PaintControls
773A508B +004B ntdll.dll             KiUserCallbackDispatcher
753A101A +000A win32u.dll            NtUserPeekMessage
769FBB99  +0169  user32.dll        PeekMessageW
75D4A0EE +003E shcore.dll           # 161
75D49E73  +0013 shcore.dll          ShCreateThread
und davor kommt meine letzte Code-Zeile, in der erfolgt der Wrapper-Aufruf von IFIleOperation.PerformOperations

Es sieht hier doch so aus, als ob er auf die Freigabe / den Zugriff auf eine gesperrte Ressource wartet?!

Wenn ich das ganze statt mit meine IFileOperation-Implementierung mit "CopyFileW" mache, sieht der MadExcept Call-Stack bei der Freeze-Erkennung folgendermaßen aus:
Code:
7736FF74  +0044  ntdll.dll          RtlEnterCriticalSection
4002FE37  +0007  rtl70.bpl Classes  ThreadList.LockList
01984929  +015D vcl70.bpl Controls TWinControl.PaintControls
773A508B +004B ntdll.dll             KiUserCallbackDispatcher
769FBB99  +0169  user32.dll        PeekMessageW
75286B21  +9E1  RPCRT4.dll             NdrClientCall2
761A6C3A +000 combase.dll             ObjectSublessClient32
76139579  +0B9  combase.dll             CoCreateInstance
3788EF44  +2F4  CRPE32.dll             PEOpenEngineEx
378941A5  +0B5  CRPE32.dll             PEOpenEngine
die weiteren Aufrufe sind in meinem Code bzw. im "Crystal Reports"-Wrapper beim Setzen von Reportdaten... (also nicht beim Kopieren!)
Auch hier wieder das Warten auf den Zugriff auf eine Ressource...
(das kann aber natürlich auch einfach ein anderer und nicht der eigentliche Problemthread sein... :roll:)

Delphi.Narium 29. Jan 2024 14:27

AW: Ursache für hängende Applikation herausfinden
 
Du schreibst u. a. von Datenbank, Freigaben, Resourcen, ...

Kann es sein, dass es da "irgendwo" ein bisher nicht näher spezifiziertes Netzwerkproblem gibt, welches sich durch ein unliebsames Aufhängen des Programmes auf sich aufmerksam macht?
Dies könnte eventuell auch eine Erklärung dafür sein, dass identischer Code andernorts keine Probleme bereitet, andererseits aber vollständig unterschiedliche Implementierungen der gleichen Aufgabe zu einem identischen Problem führen.

Versuch' bitte mal herauszufinden, ob das Problem nicht eventuell von außerhalb auf Dein Programm einwirkt.

hoika 29. Jan 2024 14:40

AW: Ursache für hängende Applikation herausfinden
 
Hallo,
bei MadExcept auch "check for frozen main thread" eingestellt?

Bodenseematze 29. Jan 2024 15:38

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von hoika (Beitrag 1532712)
Hallo,
bei MadExcept auch "check for frozen main thread" eingestellt?

Zitat:

Zitat von Bodenseematze (Beitrag 1532708)
Ich habe jetzt mal MadExcept mit Option "check for frozen main thread" mitlaufen lassen.

:lol:

Zitat:

Zitat von Delphi.Narium (Beitrag 1532711)
Du schreibst u. a. von Datenbank, Freigaben, Resourcen, ...

Der Datenbankserver ist ein MS SQL-Server, der auf einem anderen Rechner läuft.
Beim Rest habe ich Zugriff auf Ressourcen (also z.B. Dateien / Verzeichnisse / Semaphoren etc.) auf dem lokalen Rechner gemeint.
Und mit Freigaben eben die üblichen Sperr-/und Freigabe-Mechanismen von Windows für diese Ressourcen (Mutex, Semaphoren, Critical Section, ...).


Zitat:

Zitat von Delphi.Narium (Beitrag 1532711)
Dies könnte eventuell auch eine Erklärung dafür sein, dass identischer Code andernorts keine Probleme bereitet, andererseits aber vollständig unterschiedliche Implementierungen der gleichen Aufgabe zu einem identischen Problem führen.

Es ist nicht andernorts, es ist auf dem selben Rechner - nur ein anderes Delphi-Programm :wink:.
Und bzgl. der unterschiedlichen Implementierungen: die Auswirkung des Problems sind die gleichen (das Programm "hängt") - aber zumindest das interne Verhalten ist doch etwas unterschiedlich.
Im CopyFileW-Fall wird wohl noch die Kopie der Datei angelegt (hier kommt noch die Log-Ausgabe nach dem Aufruf) und erst danach hängt das Programm in einem "WaitForCriticalSection"-Aufruf.
Im IFileOperation-Fall hängt es direkt im API-Aufruf (wobei es dort auch so aussieht, als ob da intern der WaitForCriticalSection-Aufruf gemacht wird).
Ich sehe nur nicht, auf _was_ da gewartet wird bzw. mit welchem System-Objekt die CriticalSection verknüpft ist... :|

Delphi.Narium 29. Jan 2024 16:08

AW: Ursache für hängende Applikation herausfinden
 
Das sieht dann wirklich so aus, als würde sich in genau diesem Programm irgendwas "verstubbeln". Wenn alles in allen Programmen gleich ist, vom Quelltext (der betroffenen Routine(n)), über den Rechner bis zur Datenbank über die Freigaben und alle weiteren Resourcen, da müsste man dann wohl mal in die Source schauen können, um Hilfestellung geben zu können.

TigerLilly 29. Jan 2024 16:18

AW: Ursache für hängende Applikation herausfinden
 
Wie lange "hängt" das Programm dann? Kommt irgendwann eine Meldung oder tut sich irgendwann etwas?

Scheinbares "ewiges" Hängen könnte sein:
- Endlosschleife irgendwo
- Dialog oder Fenster im Hintergrund oder außerhalb des Bildschirms

Was auch wie "hängen" aussieht:
- warten auf ein timeout (MSSQL Server und Locks!)
- rekursion ohne ende

jaenicke 29. Jan 2024 16:29

AW: Ursache für hängende Applikation herausfinden
 
Das sollte klar sein, aber zur Sicherheit frage ich einfach mal. Du greifst im Thread nicht auf irgendwelche VCL-Komponenten zu, oder?

Und interessant wäre der komplette MadExcept Bericht, denn wenn da ein Deadlock im Spiel ist, braucht man die Stacktraces aller Threads. Du kannst den ja zensieren oder nur privat schicken.

Bodenseematze 30. Jan 2024 09:09

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Delphi.Narium (Beitrag 1532726)
Das sieht dann wirklich so aus, als würde sich in genau diesem Programm irgendwas "verstubbeln".

Ja :(
Zitat:

Zitat von Delphi.Narium (Beitrag 1532726)
Wenn alles in allen Programmen gleich ist, vom Quelltext (der betroffenen Routine(n)), über den Rechner bis zur Datenbank über die Freigaben und alle weiteren Resourcen, da müsste man dann wohl mal in die Source schauen können, um Hilfestellung geben zu können.

Der Quelltext ist derselbe, die Datenbank ist die selbe und der pronzipielle Ablauf für den Fehlerfall sind gleich - die Programme kopieren allerdings unterschiedliche Dateien (aus dem selben lokalen Quellverzeichnis in ein jeweils selber generiertes temp. lokales Verzeichnis)...

Mit den Quellen ist das so eine Sache - mal abgesehen davon, dass die Quellen wohl etwas zu viel wären, darf ich diese nicht so ohne weiteres (komplett) rausgeben.
Und nur Teile bringt vermutlich nicht allzu viel...

Zitat:

Zitat von TigerLilly (Beitrag 1532727)
Wie lange "hängt" das Programm dann? Kommt irgendwann eine Meldung oder tut sich irgendwann etwas?

Wenn ich Freeze-Erkennung in MadExcept nicht einschalte, hängt das Programm "ewig".

Zitat:

Zitat von TigerLilly (Beitrag 1532727)
- Endlosschleife irgendwo

Ich denke, das hätte ich über MadExcept gesehen...
Zitat:

Zitat von TigerLilly (Beitrag 1532727)
- Dialog oder Fenster im Hintergrund oder außerhalb des Bildschirms

Um solche Dinge auszuschließen habe ich extra beim Kopieren die Flags (NoConfirm, NoGui, etc.) entfernt - aber ausschließen kann ich das natürlich nicht.

Zitat:

Zitat von TigerLilly (Beitrag 1532727)
- warten auf ein timeout (MSSQL Server und Locks!)

Das will ich natürlich nicht ausschließen, habe aber bereits auf dem SQL-Server mal mitgetraced und nichts auffälliges gefunden.
Zitat:

Zitat von TigerLilly (Beitrag 1532727)
- rekursion ohne ende

Auch das müsste ich eigentlich bei der MadExcept "Freeze-Erkennung" gesehen haben....

Zitat:

Zitat von jaenicke (Beitrag 1532730)
Das sollte klar sein, aber zur Sicherheit frage ich einfach mal. Du greifst im Thread nicht auf irgendwelche VCL-Komponenten zu, oder?

Nicht, dass ich wüßte :wink:

Zitat:

Zitat von jaenicke (Beitrag 1532730)
Und interessant wäre der komplette MadExcept Bericht, denn wenn da ein Deadlock im Spiel ist, braucht man die Stacktraces aller Threads. Du kannst den ja zensieren oder nur privat schicken.

Ich schicke ihn Dir mal per privater Mail...

himitsu 30. Jan 2024 09:25

AW: Ursache für hängende Applikation herausfinden
 
Mit Debugger starten, oder beim Hängen den Debugger anhängen,
auf "Pause" gedrückt (falls nicht automatisch geschehen)
und dann im Stacktrace des MainThreads nachsehn.

Kas Ob. 30. Jan 2024 10:15

AW: Ursache für hängende Applikation herausfinden
 
Hi,

Zitat:

Zitat von Bodenseematze (Beitrag 1532752)
Zitat:

Zitat von jaenicke (Beitrag 1532730)
Das sollte klar sein, aber zur Sicherheit frage ich einfach mal. Du greifst im Thread nicht auf irgendwelche VCL-Komponenten zu, oder?

Nicht, dass ich wüßte :wink:

But you are !
Zitat:

7736FF74 +0044 ntdll.dll RtlEnterCriticalSection
4002FE37 +0007 rtl70.bpl Classes ThreadList.LockList
01984929 +015D vcl70.bpl Controls TWinControl.PaintControls
See, there is no difference if you are accessing the thread itself from VCL or blocking to access something shared with a thread.

Suggestion, but i will use an example for language barrier (may be will be readable):
you a thread that perform disk operations, these operations or tasks are in in lockable thread safe list, your VCL components need to have a visual report or just output something, then the reason is right to lock within VCL events to read these tasks and their result,
now the problem is if the thread is locking on the task from beginning of performing the task (whatever the task, copy, move, write, upload ...) these tasks are I/O, so instead of the threads locking on the operation within the list, just make it lock and extract the needed operation context without performing any real work, while locking you do mark the task as in pending or processing, then release that task list,
only after that let the thread perform the operation ,after finishing or needing to update, lock again and update the result.
this will not block the VCL and UI will stay responsive all the time.
Now to capture the failure or taking-so-long operation (file, network, calculating, whatever...) put in the VCL a hidden or hard to miss click button or menu to intentionally raise an exception, MadExcept like Eurekalog can capture all the threads stack hence you will have better idea where to look.

QuickAndDirty 30. Jan 2024 15:06

AW: Ursache für hängende Applikation herausfinden
 
Also es gibt das manchmal das Systemaufrufe einfach nicht zurrückkehren...
Hatte das mal mit diesem hier
Delphi-Quellcode:
SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE,0, Longint(@Device));//Registryänderung soll vom System gelesen werden.
Der StandardDrucker wurde in der Registry geändert und durch diesen REFRESH wird die Änderung wirksam...
Das problem ist das bei einzelnen Rechnern dieser Refresh nicht mehr zurrück kam...
Es gab auch keine Fehlermeldung...Es war so als ob dieses SendMessage einfach für dauerte...
Leider gab as keine Lösung außer es nichtmehr so zu machen.

jaenicke 30. Jan 2024 15:16

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von QuickAndDirty (Beitrag 1532791)
Leider gab as keine Lösung außer es nichtmehr so zu machen.

SendMessage sollte man ausschließlich verwenden, wenn es wichtig ist, dass die Nachricht abgearbeitet ist, bevor der nachstehende Code ausgeführt wird (z.B. wegen des Rückgabewerts). Ansonsten sollte man immer PostMessage verwenden, das nicht hängen bleibt, egal was bei der Abarbeitung passiert.

himitsu 30. Jan 2024 15:21

AW: Ursache für hängende Applikation herausfinden
 
Ja, es ist nicht so leicht, die MessageQueue voll zu bekommen,
aber bei PostMessage sollte man auch aufpassen, ob es wirklich in der Queue landet.
Nicht dass die Message verschwiendet und somit nie verarbeitet wird.

Statt PostMessage könnte man auch TThread.Queue bzw. ForceQueue benutzen.
(quasie wie der Unterschied zwischen SendMessage und TThread.Syncronize)

jaenicke 30. Jan 2024 15:28

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von himitsu (Beitrag 1532795)
aber bei PostMessage sollte man auch aufpassen, ob es wirklich in der Queue landet.
Nicht dass die Message verschwiendet und somit nie verarbeitet wird.

Da hast du Recht. Man muss es auch verwenden, wenn man sicherstellen will, dass die Nachricht angekommen ist.

Bodenseematze 1. Feb 2024 10:51

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von himitsu (Beitrag 1532755)
Mit Debugger starten, oder beim Hängen den Debugger anhängen

Würde ich gerne - aber:
Zitat:

Zitat von Bodenseematze (Beitrag 1532696)
Richtig debuggen kann ich schon lange (mehrere Jahre) nicht mehr - die Delphi IDE bleibt bei mir hängen, wenn ich versuche eine Applikation im Debugger zu starten - auch, wenn ich es über Remote-Debugging auf dem gleichen Rechner versuche (das funktioniert manchmal noch, aber meistens bleibt es auch hängen, wenn ich mich auf den laufenden Prozess setze).

:(

Das hier...
Zitat:

Zitat von Kas Ob. (Beitrag 1532762)
Zitat:

Zitat von Bodenseematze (Beitrag 1532752)
Zitat:

Zitat von jaenicke (Beitrag 1532730)
Das sollte klar sein, aber zur Sicherheit frage ich einfach mal. Du greifst im Thread nicht auf irgendwelche VCL-Komponenten zu, oder?

Nicht, dass ich wüßte :wink:

But you are !
Zitat:

7736FF74 +0044 ntdll.dll RtlEnterCriticalSection
4002FE37 +0007 rtl70.bpl Classes ThreadList.LockList
01984929 +015D vcl70.bpl Controls TWinControl.PaintControls
See, there is no difference if you are accessing the thread itself from VCL or blocking to access something shared with a thread.

...und eine private Message von jaenicke hat mich auf die Spur der Ursache gebracht :-D

Da es viele Aktionen abzuarbeiten gibt und das etwas länger dauert, verwende ich eine Mini-Form soz. als "Bitte Warten..."-Anzeige.
In dieser wird ein Anzeigetext gesetzt (ändert sich mit der aktuellen Aktion) und mit zu und abnehmenden "..." wird signalisiert, dass die Arbeit noch am Laufen ist.
Für dieses Zu- und Abnehmen habe ich eine eigene Klasse TTimerThread verwendet, die einen Thread basierten Timer zur Verfügung stellt bzw. stellen soll.
Eigentlich soll diese Klasse auch die Synchronisierung mit der VCL übernehmen - aber da scheint was nicht richtig zu sein.

Was da genau schief läuft, habe ich noch nicht herausgefunden - vielleicht findet jemand von Euch den Fehler im Code?

Das hier ist die Deklaration:
Delphi-Quellcode:
type
TTimerThread        = class( TThread )
public
   constructor Create(              bSuspend_            : Boolean = false ); virtual;
   destructor Destroy(); override;

   procedure  Start();
   procedure  Terminate();

   procedure  SetTimerFunc(        evTimerFunc_         : TNotifyEvent );


protected
   procedure  Execute(); override;


private
   _evFlagCancel                                        : TSimpleEvent;
   _evFlagEnabled                                       : TSimpleEvent;
   _evTimerFunc                                         : TNotifyEvent;
   _ui32IntervalInMs                                    : UInt32;
   _bCallFuncOnMainThread                               : Boolean;

   procedure  SetEnabled(          bDoEnable_           : Boolean );
   function   GetEnabled()                             : Boolean;

   procedure  SetInterval(         ui32IntervalInMs_    : UInt32 );

   procedure  SwapToMainThread();


// Properties
public
   property   Enabled                                  : Boolean
                                    read    GetEnabled
                                    write   SetEnabled;

   property   Interval                                 : UInt32
                                    read    _ui32IntervalInMs
                                    write   SetInterval;

   // soll die OnTimer-Methode auf dem Main-Thread ausgeführt werden?
   property   CallOnTimerOnMainThread                  : Boolean
                                    read    _bCallFuncOnMainThread
                                    write   _bCallFuncOnMainThread;

   // Anmerkung: OnTimer wird im Thread ausgeführt...
   property   OnTimer                                  : TNotifyEvent
                                    read    _evTimerFunc
                                    write   SetTimerFunc;
end; (* END_CLASS TTimerThread *)
und hier die Implementierung:
Delphi-Quellcode:
constructor TTimerThread.Create(    bSuspend_            : Boolean = false );
begin
   inherited Create( true    {* CreateSuspended *} );

   _bCallFuncOnMainThread := false;
   _ui32IntervalInMs      := 1000;
   _evTimerFunc           := nil;

   _evFlagEnabled         := TSimpleEvent.Create();
   _evFlagEnabled.ResetEvent();
   _evFlagCancel          := TSimpleEvent.Create();
   _evFlagCancel.ResetEvent();

   FreeOnTerminate        := false;
   Priority               := tpNormal;

   // lass den Thread loslaufen:
   if ( NOT bSuspend_ ) then begin
      Start();
   end;
end; (* END_CONSTRUCTOR TTimerThread.Create *)


destructor TTimerThread.Destroy();
begin
   Terminate();

   // synchronisieren:
   if ( GetCurrentThreadID = MainThreadID ) then begin
      WaitFor();
   end;

   FreeAndNil( _evFlagCancel );
   FreeAndNil( _evFlagEnabled );

   inherited Destroy();
end; (* END_DESTRUCTOR TTimerThread.Destroy *)


procedure  TTimerThread.Terminate();
begin
   // Aufruf statische Methode TThread.Terminate:
   inherited Terminate();

   _evFlagEnabled.ResetEvent(); // Timer stoppen
   _evFlagCancel.SetEvent();    // Cancel-Flag setzen
end; (* END_PROC TTimerThread.Terminate *)


procedure  TTimerThread.SetTimerFunc(
                                    evTimerFunc_         : TNotifyEvent );
begin
   _evTimerFunc           := evTimerFunc_;
end; (* END_PROC TTimerThread.SetTimerFunc *)


procedure  TTimerThread.SetEnabled(bDoEnable_           : Boolean );
begin
   if ( bDoEnable_ ) then begin
      _evFlagEnabled.SetEvent();
   end
   else begin
      _evFlagEnabled.ResetEvent();
   end;
end; (* END_PROC TTimerThread.SetEnabled *)


function   TTimerThread.GetEnabled()                    : Boolean;
begin
   Result                 := false;
   if ( WaitForSingleObject(
                           _evFlagEnabled.Handle,
                           0) = WAIT_OBJECT_0 ) then begin
      Result              := true;
   end;
end; (* END_FUNC TTimerThread.GetEnabled *)


procedure  TTimerThread.SetInterval(
                                    ui32IntervalInMs_    : UInt32 );
begin
   _ui32IntervalInMs      := ui32IntervalInMs_;
end; (* END_PROC TTimerThread.SetInterval *)


procedure  TTimerThread.Start();
begin
   // startet einen bei Create nicht gestarteten Thread...
   Resume();
end; (* END_PROC TTimerThread.Start *)


procedure  TTimerThread.Execute();
var
   arrWaitHandles   : TWOHandleArray;
   i64WaitInterval  : Int64;
   i64LastProcTime  : Int64;
   i64Freq          : Int64;
   i64Start         : Int64;
   i64Stop          : Int64;
   dwWaitResult     : DWORD;
begin
   QueryPerformanceFrequency( i64Freq );

   arrWaitHandles[ 0 ]    := _evFlagEnabled.Handle;
   arrWaitHandles[ 1 ]    := _evFlagCancel.Handle;
   arrWaitHandles[ 2 ]    := INVALID_HANDLE_VALUE;
   i64LastProcTime        := 0;

   while ( (NOT Terminated) and (NOT Application.Terminated) ) do begin
      dwWaitResult        := MsgWaitForMultipleObjects(
                                 2,               {* nCount *}
                                 arrWaitHandles,  {* var pHandles *}
                                 false,           {* fWaitAll *}
                                 INFINITE,        {* dwMilliseconds *}
                                 QS_ALLINPUT      {* dwWakeMask *} );
      case dwWaitResult of
         WAIT_FAILED: begin  // 0xFFFFFFFF
            // Fehler beim warten
            // --> da hören wir auf (Thread beenden)!
            Break;
         end;

         ( WAIT_OBJECT_0 + 1 ): begin // 0x00000001
            // Cancel-Event (_evFlagCancel)
            // --> Thread beenden
            Break;
         end;

         ( WAIT_OBJECT_0 + 0 ): begin // 0x00000000
            // Enable-Event (_evFlagEnabled)
            // --> Timer-Methode aufrufen
            if ( Assigned(_evTimerFunc) ) then begin
               i64WaitInterval     := Int64( _ui32IntervalInMs ) - i64LastProcTime;
               if ( i64WaitInterval < 0 ) then begin
                  i64WaitInterval  := 0;
               end;

               if ( WaitForSingleObject(
                                       _evFlagCancel.Handle,
                                       i64WaitInterval) <> WAIT_TIMEOUT ) then begin
                  // Cancel-Event _evFlagCancel
                  // --> Thread beenden
                  Break;
               end;

               if ( Enabled ) then begin
                  QueryPerformanceCounter( i64Start );
                  if ( Terminated or Application.Terminated ) then begin
                     Break;
                  end;
                  if ( Assigned(_evTimerFunc) ) then begin
                     if ( CallOnTimerOnMainThread ) then begin
                        // Methode indirekt auf dem Main-Thread aufrufen:
                        Synchronize( SwapToMainThread );
                     end
                     else begin
                        // Methode direkt aufrufen:
                        try
                           _evTimerFunc( Self );
                        except
                        end;
                     end;
                  end; // if ( Assigned(_evTimerFunc) ) then
                  QueryPerformanceCounter( i64Stop );
                  i64LastProcTime  := ( 1000 * (i64Stop - i64Start) ) div i64Freq;
               end; // if ( Enabled ) then
            end; // if ( Assigned(_evTimerFunc) ) then
         end; // ( WAIT_OBJECT_0 + 1 ): begin

         else begin // ( WAIT_OBJECT_0 + 2 ) = 0x00000002
            // eine der Eingabe-Events (QS_ALLINPUT)
            // --> Nachrichten abarbeiten und wieder warten...
            Application.ProcessMessages();
         end;
      end; // case dwWaitResult of
   end; // while ( NOT Terminated ) do
end; (* END_PROC TTimerThread.Execute *)


procedure  TTimerThread.SwapToMainThread();
begin
   if ( Assigned(_evTimerFunc) ) then begin
      try
         _evTimerFunc( Self );
      except
      end;
   end; // if ( Assigned(_evTimerFunc) ) then
end; (* END_PROC TTimerThread.SwapToMainThread *)

jaenicke 1. Feb 2024 11:20

AW: Ursache für hängende Applikation herausfinden
 
Du rufst Application.ProcessMessages im Threadkontext auf und greifst damit ganz tief in die Messagebehandlung der VCL ein. Das kann nicht klappen.

In einer länger laufenden Methode, die im Hauptthread läuft, wird das verwendet, damit die GUI noch reagiert, indem dort die Paint-, Maus- und Tastaturmessages abgearbeitet werden. (Besser ist natürlich, das zu vermeiden.)

In einem Thread ist das nicht nötig (und auch nicht möglich). Wenn du warten möchtest, rufst du einfach Sleep auf. Da der Thread separat läuft, blockierst du damit auch nicht die GUI. Oder du wartest auf ein Event eine definierte Zeit.

Bodenseematze 1. Feb 2024 11:33

AW: Ursache für hängende Applikation herausfinden
 
Hmm, auch wenn ich die Stelle Application.ProcessMessages() im Code mit Sleep(0) oder Windows.Yield() austausche,
bleibt das ganze (wie gewohnt :wink:) hängen...

Daran kann es also nicht (nur) liegen...

Kas Ob. 1. Feb 2024 11:39

AW: Ursache für hängende Applikation herausfinden
 
Hi,

First and foremost never ever ever ever use Application.ProcessMessages(); in background thread and anywhere in general !, nothing good will come from it, and will add more failure scenarios that no one can imagine or think of.

Now to this whole big thread, it is way more complex than it should be.

My suggestion for your case (the code above) to drop it all and replace it with something like the a thread that have one event (not two), single one will do the job perfectly, or even remove the event altogether and use simple Sleep or SleepEx with very short sleep time (interval) like 50ms (1/20 of second), check triggering condition and perform one of the following
1) Enabled / Disabled nothing need to be done, loop again.
2) Exit request, then exit.
3) Need to call or notify the main thread then use PostMessage (simple like that) and loop again.
4) You want more control over timing and intervals then use Sleep(1).
5) Access the conditions from the threads itself only using.

From the code i deduce you are using this only for Windows then hide this new small thread in small TWinControl (or an invisible form) or what ever you see fit, and implement the a Message handler for that Message coming form PostMessage, and that is it.

On side note : PostMessage will of course use a window message, and most importantly if for some reason the message queue is full or blocked (the messages are not being process for other defect or bugs) then Synchronize from a thread will not be helpful too and will only make finding the real bug/flaw harder, Synchronize will only waste your time finding the bug in almost every scenario were things went wrong.
Also if PostMessage doesn't work or fail then Synchronize will not help either.

Kas Ob. 1. Feb 2024 11:46

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Bodenseematze (Beitrag 1532870)
Hmm, auch wenn ich die Stelle Application.ProcessMessages() im Code mit Sleep(0) oder Windows.Yield() austausche,
bleibt das ganze (wie gewohnt :wink:) hängen...

Daran kann es also nicht (nur) liegen...

Remove Synchronize, redesign it without Synchronize.

jaenicke 1. Feb 2024 12:01

AW: Ursache für hängende Applikation herausfinden
 
Der Aufruf passierte laut Stacktrace 67 Zeilen weiter unten ab dem begin des Execute gezählt. Da ist im geposteten Quelltext ein end. Da müsstest du noch einmal schauen, was dort vorher stand.

Und wenn nun ohne ProcessMessages die Anwendung hängt... sieht der Bugreport noch genauso aus? Dein Timer nutzt ja nicht immer Synchronize. Kann es vorkommen, dass es ohne aufgrufen wird und dort etwas mit der VCL passiert?

Zitat:

Zitat von Bodenseematze (Beitrag 1532866)
Da es viele Aktionen abzuarbeiten gibt und das etwas länger dauert, verwende ich eine Mini-Form soz. als "Bitte Warten..."-Anzeige.
In dieser wird ein Anzeigetext gesetzt (ändert sich mit der aktuellen Aktion) und mit zu und abnehmenden "..." wird signalisiert, dass die Arbeit noch am Laufen ist.
Für dieses Zu- und Abnehmen habe ich eine eigene Klasse TTimerThread verwendet, die einen Thread basierten Timer zur Verfügung stellt bzw. stellen soll.

Wenn du im Hauptthread eine Aktion ausführst, kann auch ein Thread keine Aktualisierung der GUI durchführen. Dann bleibt Synchronize schlicht hängen, weil Synchronize voraussetzt, dass der Hauptthread auch funktionsfähig ist, sprich Nachrichten abarbeitet. Das Synchronize wird in WM_IDLE abgearbeitet. Wenn der Hauptthread beschäfitgt ist, passiert das nicht.

Nicht die Statusanzeige oder deren Steuerung gehören in den Thread sondern die lange dauernden Aktionen selbst.

Alternativ (aber nicht so schön) könntest du auch ein threadbasiertes Fenster anzeigen. Das kann auch direkt aus dem Thread ohne Synchonisierung verwendet werden. Ich habe dafür mal eine kleine Bibliothek angefangen:
https://github.com/jaenicke/MTCL
Da es dafür mittlerweile doch Interesse gibt, arbeite ich daran vielleicht doch mal weiter...

himitsu 1. Feb 2024 12:34

AW: Ursache für hängende Applikation herausfinden
 
Wenn in Application.ProcessMessages wirklich nur die MessageList direkt verarbeitet würde,
dann wäre es egal, die der Thread vermutlich eh nichts in "seiner" Queue hat (bzw. noch keine Queue erstellt war)

Aber dort gibt es eben auch noch anderen Code, welcher auf globale Objekte/Variablen zugreift, welche halt nicht thread-safe sind.

peterbelow 1. Feb 2024 12:54

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Bodenseematze (Beitrag 1532870)
Hmm, auch wenn ich die Stelle Application.ProcessMessages() im Code mit Sleep(0) oder Windows.Yield() austausche,
bleibt das ganze (wie gewohnt :wink:) hängen...

Daran kann es also nicht (nur) liegen...

Nur noch was am Rande: MsgWait.. macht innerhalb eines sekundären Threads keinen Sinn da ein solcher Thread keine message queue hat! Er wird also niemals eine input-message (QS_ALLINPUT) bekommen, alle vom Benutzer generierten Messages (mouse, keyboard) landen in der Queue des main threads und der sekundäre thread sieht sie nicht.

Bodenseematze 1. Feb 2024 13:11

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von jaenicke (Beitrag 1532876)
Der Aufruf passierte laut Stacktrace 67 Zeilen weiter unten ab dem begin des Execute gezählt. Da ist im geposteten Quelltext ein end. Da müsstest du noch einmal schauen, was dort vorher stand.

Im geposteten Quellcode wurden die Aufrufe für mein Logging entfernt - deswegen die Diskrepanz.

Das wäre dann die Zeile
Delphi-Quellcode:
                        // Methode direkt aufrufen:
                        try
-->                          _evTimerFunc( Self );
                        except
Zitat:

Zitat von jaenicke (Beitrag 1532876)
Und wenn nun ohne ProcessMessages die Anwendung hängt... sieht der Bugreport noch genauso aus? Dein Timer nutzt ja nicht immer Synchronize. Kann es vorkommen, dass es ohne aufgrufen wird und dort etwas mit der VCL passiert?

es ist auch aktuell die o.a. Zeile...

Zitat:

Zitat von jaenicke (Beitrag 1532876)
Nicht die Statusanzeige oder deren Steuerung gehören in den Thread sondern die lange dauernden Aktionen selbst.

Da gebe ich Dir prinzipiell schon recht, aber das ist auch nicht so einfach.
Da werden externe Prozesse gestartet, Dateien mehrmals hin und her kopiert und angepasst, Reportdaten aktualisiert, eine Paradox-Tabelle mit Daten befüllt und andere Dinge...
...der Ablauf war ursprünglich nicht so aufwändig - es ist aber mit der Zeit immer mehr geworden und jetzt merkt man eben, dass es eine deutlich Verzögerung im Ablauf gibt.

Deswegen der Gedanke mit dem "Bitte warten..."-Dialog, in dem auch die aktuell ausgeführte Aktion textuell angezeigt werden soll.
Da in der ursprünglichen Version der angezeigte Text nicht aktualisiert wurde (bzw. die Aktualisierung nicht angezeigt wurde, hatte ich den Gedanken mit dem Thread, der den Text im Dialog animiert und refresht.


Zitat:

Zitat von jaenicke (Beitrag 1532876)
Wenn du im Hauptthread eine Aktion ausführst, kann auch ein Thread keine Aktualisierung der GUI durchführen. Dann bleibt Synchronize schlicht hängen, weil Synchronize voraussetzt, dass der Hauptthread auch funktionsfähig ist, sprich Nachrichten abarbeitet. Das Synchronize wird in WM_IDLE abgearbeitet. Wenn der Hauptthread beschäfitgt ist, passiert das nicht.

Das würde erklären, warum die Dialoganzeige nicht aktualisiert wird...

Zitat:

Zitat von jaenicke (Beitrag 1532876)
Alternativ (aber nicht so schön) könntest du auch ein threadbasiertes Fenster anzeigen. Das kann auch direkt aus dem Thread ohne Synchonisierung verwendet werden.

Wenn es da einen "einfachen" Dialog gäbe, dessen Text selber aktualisiert, wäre das nicht schlecht!

Zitat:

Zitat von jaenicke (Beitrag 1532876)
https://github.com/jaenicke/MTCL
Da es dafür mittlerweile doch Interesse gibt, arbeite ich daran vielleicht doch mal weiter...

Ich schaue mir die mal an, vielleicht kann ich da was raus extrahieren...


Zitat:

Zitat von peterbelow (Beitrag 1532879)
Nur noch was am Rande: MsgWait.. macht innerhalb eines sekundären Threads keinen Sinn da ein solcher Thread keine message queue hat! Er wird also niemals eine input-message (QS_ALLINPUT) bekommen, alle vom Benutzer generierten Messages (mouse, keyboard) landen in der Queue des main threads und der sekundäre thread sieht sie nicht.

Ich sehe schon, ich werde diese Klasse komplett wegschmeissen müssen - die macht so keinen Sinn...


Zitat:

Zitat von Kas Ob. (Beitrag 1532871)
Now to this whole big thread, it is way more complex than it should be.

Thanks a lot for your help / suggestions.
But I'm not sure if I understand everything you mentioned.
Maybe you have some example code for it? :oops:

So actually I need a dialog who shows some progress messages - the thread should be able to set the text on that dialog (or the whole dialog is the thread) to show the progress even when the main thread is busy...

EDIT: BTW, the dialog itself could also be a generated native windows dialog - it must not be Delphi at all ;-)

TiGü 1. Feb 2024 13:20

AW: Ursache für hängende Applikation herausfinden
 
Der ganze Ansatz ist schon komplett für die Füße.
Du musst deine komplexen und langen Operationen in Tasks/Threads packen und die geben zwischendrin oder am Ende Bescheid, was Sachlage ist.

jaenicke 1. Feb 2024 13:42

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Bodenseematze (Beitrag 1532880)
Da gebe ich Dir prinzipiell schon recht, aber das ist auch nicht so einfach.
Da werden externe Prozesse gestartet, Dateien mehrmals hin und her kopiert und angepasst, Reportdaten aktualisiert, eine Paradox-Tabelle mit Daten befüllt und andere Dinge...

Wenn du da nicht ran möchtest, bleibt dir nur der Weg über ein Fenster, das außerhalb des Hauptthreads läuft. Da sollte die MTCL in der aktuellen Form schon reichen. Eine Textanzeige und eine ProgressBar hat sie drin.

Kas Ob. 1. Feb 2024 14:28

AW: Ursache für hängende Applikation herausfinden
 
Zitat:

Zitat von Bodenseematze (Beitrag 1532880)
the thread should be able to set the text on that dialog

That is the problem !

Don't push the info (or data) from background thread to main thread or GUI (VCL), let the VCL/GUI poll the status from the threads, but if it is necessary then notify by (well highly recommended) a message, also make the thread limit its notification to time based condition, like lets say we are moving/copying a folder, if that folder have 10k of small files, it makes no sense to notify or update the UI for each of them, right !?, the copying thread will notify once every 1/10 or 1/20 of second and when finished, and that it is, or will not notify anyone/anything, thread is doing his job and updating single place with data, UI will update with timer, here if the OS itself is way overwhelmed with other application or the CPU is very slow, the application and its UI will not block and will not fail beyond what the OS allow it, but will recover nicely when CPU (hardware in general) is relaxed.

A modal form can block the VCL/GUI/UI.. and show "please wait.." or "working...", while have a timer to get the current status/operation form the worker threads, here you don't need to time your background threads or their operation, they just updating a list (may be a list, or properties ... whatever) in thread safe way, while your dialog or modal form will have a timer and update.

one problem though if the thread operation might take long time and can't be cut or sliced in shorter job, (as example coping 8GB file on SSD or HDD) then nothing can be sliced and here you need to switch to overlapped file operation, https://learn.microsoft.com/en-us/wi...put-and-output

Also look at this nice answers https://stackoverflow.com/questions/...fast-file-copy

Notice also these are not utilizing the overlapped IO, yet these operation (reading/writing) can have it and you can have timeout on any of these operation, also as in the second answer 512KB buffer while the first will be very slow with 1KB, i would suggest to try to perform a read with less than 64KB on any modern hardware to not waste time, and 512KB sound good too with modern hardware cache buffers, in all cases if one of these operation will block for long time then you need to switch to overlapped and use them exactly as sockets !, you call the operation with overlapped structure having an event then poll on that event, in such design you can cancel the operation any time.

The code above can be adjusted to run in background thread and store the current filename(, size, position...etc) inside the thread itself, while your gui with a timer will update the status.

I keep telling that : you should not push form a background thread to main, because i saw this many times, code working perfectly for years, here comes new faster and powerful hardware makes things light speed for UI to keep up, that working code in the past is overwhelming the UI, that happened not because the developer was short sighted, but hardware changed and evolved, few months back i fixed a code (not mine but been asked to modernize it) was working for ages, now it fail with unexpected behavior because the client (owner of that legacy software) started to use hardware with SSD and 2.5Gbs connection, in the past it was on HDD and ADSL.

Hope that helps.

jaenicke 1. Feb 2024 15:37

AW: Ursache für hängende Applikation herausfinden
 
You are right, but those optimization details do not match the problem. The problem is, that the main thread is blocked. So a timer won't work better.

As I wrote there are only two possibilities:
- A bigger refactoring, which includes moving the long running operations to a background thread. Of course this can be a really big task.
- Or using a status window, which runs in its own thread, so it can be displayed and updated even when the main thread is fully blocked (as I do it with my MTCL). This is a simple and quick solution (which of course does not solve the design flaws).

Bodenseematze 27. Feb 2024 07:16

AW: Ursache für hängende Applikation herausfinden
 
Auch wenn es wieder eine Weile her ist - ich wollte noch kurz berichten, was ich jetzt schlussendlich unternommen habe...

Zitat:

Zitat von jaenicke (Beitrag 1532892)
As I wrote there are only two possibilities:
- A bigger refactoring, which includes moving the long running operations to a background thread. Of course this can be a really big task.
- Or using a status window, which runs in its own thread, so it can be displayed and updated even when the main thread is fully blocked (as I do it with my MTCL). This is a simple and quick solution (which of course does not solve the design flaws).

Und ich habe die dritte Möglichkeit gewählt :-D

Ich habe das Fenster komplett eliminiert und mir die einzelnen durchgeführten Arbeitsschritte, die so lange gedauert haben, genauer angeschaut.
Das habe ich dann optimiert und teilweise auch auf den Datenbankserver verlagert.
Dadurch hat sich die Laufzeit des Codes drastisch reduziert - es ist zwar immer noch keine "Nullzeit", aber zumindest soweit erträglich, dass ich kein "Wartefenster" und auch kein riesiges Refactoring mehr brauche... :wink:


Alle Zeitangaben in WEZ +1. Es ist jetzt 07:54 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz