Recent

Author Topic: Threads - stringlist and programming tips  (Read 5025 times)

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #15 on: May 28, 2023, 08:33:01 pm »
@Warfley

Quote
As an example, I am currently writing a program that simply downloads a large ZIP archive (lazarus sources) and unzips them via TZipper. I put it in a thread with an Progressbar update event. But here is the catch, how can I stop this thread prematurely? Neither TFPHTTPClient nor TZipper have a function to stop mid process, so once one them started, it's around 5-10 minutes of the thread not being able to terminate. So any waitfor would Freeze the application for 5-10 minutes, resulting in the App being killed by windows.
Both TFPHTTPClient/T(Un)Zipper has a terminate method to immediately stop the download/zipping process.
You are right, I have missed it for TZipper (I have looked for "Stop", or "Close"). For TFPHTTPClient it does not work as good because there still a blocking call. And when downloading the ZIP archive of GitLab, gitlab first needs to perform the zipping before it starts sending data (during which the Read will be blocking), which means that for a few seconds to around 1 minute (depending if the file is cached or not) or so it still is blocking.
« Last Edit: May 28, 2023, 08:35:00 pm by Warfley »

alpine

  • Hero Member
  • *****
  • Posts: 1295
Re: Threads - stringlist and programming tips
« Reply #16 on: May 28, 2023, 08:47:36 pm »
For TFPHTTPClient it does not work as good because there still a blocking call. And when downloading the ZIP archive of GitLab, gitlab first needs to perform the zipping before it starts sending data (during which the Read will be blocking), which means that for a few seconds to around 1 minute (depending if the file is cached or not) or so it still is blocking.
How raising an exception in the Progress event will help you then?
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #17 on: May 28, 2023, 10:23:41 pm »
How raising an exception in the Progress event will help you then?
Well, it doesn't, but thats not the point I was trying to make. The point I was making, if I would use Terminate + WaitFor, as recommendet here, my application would freeze for minutes because of this.
To repeat what I have said before, never use WaitFor in Mainthread, unless you can guarantee that the thread terminates within a few milliseconds after Terminate (which in the case of TFPHttpClient you can't).

To quote from the Wiki page about Threading:
Quote
Important: The main thread should never wait for another thread. Instead use Synchronize (see above).
It's simple as that, never wait on the main thread, which means no WaitFor.

I actually once had a "solution" for this problem of TFPHttpClient, but this required a change to the FCL, in that I added a function to simply close the socket prematurely. This causes the Read operation to fail and therefore interrupt the waiting.
But because this project is one where I intend to put out the source, having to change the FCL sources is not really feasable this time.

balazsszekely

  • Guest
Re: Threads - stringlist and programming tips
« Reply #18 on: May 29, 2023, 07:01:54 am »
I believe it can be done without modifying FCL. DisconnectFromServer is a protected virtual method which will close and free the socket internally, you can access it by subclassing TFPCustomHTTPClient.
Code: Pascal  [Select][+][-]
  1.   TFPHTTPClientEx = class(TFPCustomHTTPClients)
  2.   protected
  3.     procedure DisconnectFromServer; override;
  4.   end

alpine

  • Hero Member
  • *****
  • Posts: 1295
Re: Threads - stringlist and programming tips
« Reply #19 on: May 29, 2023, 09:42:10 am »
How raising an exception in the Progress event will help you then?
Well, it doesn't, but thats not the point I was trying to make. The point I was making, if I would use Terminate + WaitFor, as recommendet here, my application would freeze for minutes because of this.
To repeat what I have said before, never use WaitFor in Mainthread, unless you can guarantee that the thread terminates within a few milliseconds after Terminate (which in the case of TFPHttpClient you can't).


To quote from the Wiki page about Threading:
Quote
Important: The main thread should never wait for another thread. Instead use Synchronize (see above).
It's simple as that, never wait on the main thread, which means no WaitFor.
Well, we're going far beyond a simple "rules of thumb" here, but anyway ...
I believe you're citing: https://wiki.freepascal.org/Multithreaded_Application_Tutorial#Waiting_for_another_thread

In your border case, where it is hard for you to ensure the granularity of the Terminated check inside your thread, you have several other options:

 - To return CanClose=False in the OnCloseQuery on your main form during the blocking periods. You can decide when depending on your Progress event handler.

 - To continuously monitor thread.Finished after calling thread.Terminate and before thread.WaitFor:
Code: Pascal  [Select][+][-]
  1.   thread.Terminate;
  2.   while not thread.Finished do
  3.     Application.ProcessMessages;
  4.   thread.WaitFor;
  5.   thread.Free;
  6.  

  - To call thread.Terminate and start async. polling of thread.Finished:
Code: Pascal  [Select][+][-]
  1.  
  2.   procedure Form1.AsyncWaitFor(Data: PtrInt);
  3.   begin
  4.     with TThread(Data) do
  5.       if not Finished then
  6.         Application.QueueAsyncCall(@AsyncWaitFor, Data)
  7.       else
  8.       begin
  9.         WaitFor;
  10.         Free;
  11.       end;
  12.   end;
  13.  
  14.   ....
  15.   thread.Terminate;
  16.   Application.QueueAsyncCall(@AsyncWaitFor, PtrInt(thread));
  17.   ....
  18.  

In a previous post I wrote that the WaitFor can be postponed to a time when it will not block the GUI. And I'll always prefer (and recommend) to have valid references instead of such fire-and-forget contraptions.
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #20 on: May 29, 2023, 12:37:31 pm »
In your border case, where it is hard for you to ensure the granularity of the Terminated check inside your thread, you have several other options:

 - To return CanClose=False in the OnCloseQuery on your main form during the blocking periods. You can decide when depending on your Progress event handler.

 - To continuously monitor thread.Finished after calling thread.Terminate and before thread.WaitFor:
Code: Pascal  [Select][+][-]
  1.   thread.Terminate;
  2.   while not thread.Finished do
  3.     Application.ProcessMessages;
  4.   thread.WaitFor;
  5.   thread.Free;
  6.  

  - To call thread.Terminate and start async. polling of thread.Finished:
Code: Pascal  [Select][+][-]
  1.  
  2.   procedure Form1.AsyncWaitFor(Data: PtrInt);
  3.   begin
  4.     with TThread(Data) do
  5.       if not Finished then
  6.         Application.QueueAsyncCall(@AsyncWaitFor, Data)
  7.       else
  8.       begin
  9.         WaitFor;
  10.         Free;
  11.       end;
  12.   end;
  13.  
  14.   ....
  15.   thread.Terminate;
  16.   Application.QueueAsyncCall(@AsyncWaitFor, PtrInt(thread));
  17.   ....
  18.  

In a previous post I wrote that the WaitFor can be postponed to a time when it will not block the GUI. And I'll always prefer (and recommend) to have valid references instead of such fire-and-forget contraptions.
First, this border case can *always* happen when you use a blocking call. I just found it because I am right now using gitlab zip Downloads. But you know what can also happen? Bad connectivity, where you may not receive any data for some time (remember the OS kills your application after only a few seconds of freezing. It does not need minutes).

To your approaches, you can't handle events whale doing waitfor, so how can you handle the OnClose?

Second your while not finished loop, yes that works, but then you don't need WaitFor, because your while not finished does exactly what WaitFor does. When your thread is finished using WaitFor afterwards is pointless. So yes, replacing WaitFor with a non blocking alternative is fine, and all I am advocating the whole time

And your third example is a much more complicated version of the OnTerminate event (which was the very first thing I recommened) with the bonus of again calling waitfor after the thread is finished, which is pointless. Do you really think that this is polling self queuing event is easier to use than the already existing OnTerminate event?

Lastly about FreeOnTerminate, it's actually not that hard to not use the thread again, just nil it's reference:
Code: Pascal  [Select][+][-]
  1. // in main thread
  2. Thread.terminate;
  3. Thread := nil;
  4.  
  5. //In your thread
  6. If terminated then
  7. begin
  8.   FreeOnTerminate:= true;
  9.   //your termination handler
  10. end;

If you only have one thread variable, setting it to nil not only removes any chance to accidentally call something on it, it is also a way to signalize that this thread has been killed

PS: I must admire your creativity, in this post you reinvented a non blocking waitfor, as well as a pilled version of OnTerminate to call before the blocking waitfor in order to avoid the problem of blocking waitfor (with the side effect that you made the blocking waitfor completely redundant)

So I'm wondering, why do you hang so much on waitfor, that you want to use it so hard that you even write code making it completely unnecessary, only to not have it blocking (because it literally does nothing in that situation)?

alpine

  • Hero Member
  • *****
  • Posts: 1295
Re: Threads - stringlist and programming tips
« Reply #21 on: May 29, 2023, 03:56:35 pm »
First, this border case can *always* happen when you use a blocking call. I just found it because I am right now using gitlab zip Downloads. But you know what can also happen? Bad connectivity, where you may not receive any data for some time (remember the OS kills your application after only a few seconds of freezing. It does not need minutes).

To your approaches, you can't handle events whale doing waitfor, so how can you handle the OnClose?
I didn't say it will be after you called WaitFor. Returning CanClose=False will be just to inform LCL that the GUI can not be closed right now and to prevent blocking in your form Destroy where the WaitFor will be called eventually.

Second your while not finished loop, yes that works, but then you don't need WaitFor, because your while not finished does exactly what WaitFor does. When your thread is finished using WaitFor afterwards is pointless. So yes, replacing WaitFor with a non blocking alternative is fine, and all I am advocating the whole time
Are you sure while not finished do is the same as WaitFor? Better consult with the RTL sources.

And your third example is a much more complicated version of the OnTerminate event (which was the very first thing I recommened) with the bonus of again calling waitfor after the thread is finished, which is pointless. Do you really think that this is polling self queuing event is easier to use than the already existing OnTerminate event?
Do you think WaitFor is pointless?
In 2-nd and 3-rd examples WaitFor is for your reference. Actually Free will call it through thread.Destroy (Free-->Destroy-->SysDestroy-->Terminate+WaitFor)
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #22 on: May 29, 2023, 06:01:12 pm »
After finished the only thing that happens is the OnTerminate event. If you don't have one, you can free directly.

But with the third example, what makes that better than:
Code: Pascal  [Select][+][-]
  1. procedure TMyForm.FreeTerminatedThread(Sender: TObject);
  2. being
  3.   Thread.Free;
  4.   Thread := nil; //to ensure the reference is nil. If not required use Sender.Free
  5. end;
  6.  
  7. Thread.OnTerminate := @FreeTerminatedThread;
  8. ...
  9. Thread.Terminate;
As I proposed from the beginning? The OnTerminate event is built in a way you can free your thread in there, as it's the very last operation that may utilize the thread object.

It's much more concise than your third example and doesn't spam the message queue with unnecessary events.

Note that your code to make waitfor work is quite complex, while simply not using waitfor but OnTerminate or FreeOnTerminate is really easy... So much about beginners advice
« Last Edit: May 29, 2023, 06:08:20 pm by Warfley »

alpine

  • Hero Member
  • *****
  • Posts: 1295
Re: Threads - stringlist and programming tips
« Reply #23 on: May 29, 2023, 07:48:24 pm »
After finished the only thing that happens is the OnTerminate event. If you don't have one, you can free directly.
On https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L132 is where OnTerminate gets executed.
On https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L133 is where Finished becomes True. In case you have freed the thread into OnTerminate, the LThread is dangling.
There is one more reference to the dangling LThread on line 137. But since it is guarded by FreeOnTerminate we can assume it won't cause a trouble.

In fact, after Finished things do happen, but OnTerminate isn't one of them, it happens just before that.

What happens after Finished
  - in case of FreeOnTerminate the thread gets freed on line 137 then threadvars released by ThreadFunc->EndThread->cthreads.CEndThread->DoneThread, thread gets detached at https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/cthreads.pp#L450 and finally exited on next line;
  - otherwise (when not FreeOnTerminate), the ThreadFunc will finish with a thread left in a "joinable" state. Eventually, the WaitFor must be called to join the thread (https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/cthreads.pp#L508), otherwise the TThread.Destroy will call Terminate+WaitFor for the same purpose.

So, Finished means just that the Execute method was exited, OnTerminate happens just before Finished becomes true.

But with the third example, what makes that better than:
Code: Pascal  [Select][+][-]
  1. procedure TMyForm.FreeTerminatedThread(Sender: TObject);
  2. being
  3.   Thread.Free;
  4.   Thread := nil; //to ensure the reference is nil. If not required use Sender.Free
  5. end;
  6.  
  7. Thread.OnTerminate := @FreeTerminatedThread;
  8. ...
  9. Thread.Terminate;
As I proposed from the beginning? The OnTerminate event is built in a way you can free your thread in there, as it's the very last operation that may utilize the thread object.

It's much more concise than your third example and doesn't spam the message queue with unnecessary events.

Note that your code to make waitfor work is quite complex, while simply not using waitfor but OnTerminate or FreeOnTerminate is really easy... So much about beginners advice

Finished threads must be detached (in case of FreeOnTerminate) or joined with either TThread.WaitFor or TThread.Destroy (implicit WaitFor), to quote the manual:
Code: Text  [Select][+][-]
  1.        After a successful call to pthread_join(), the caller is
  2.        guaranteed that the target thread has terminated.  The caller may
  3.        then choose to do any clean-up that is required after termination
  4.        of the thread (e.g., freeing memory or other resources that were
  5.        allocated to the target thread).
  6.  
  7.        Failure to join with a thread that is joinable (i.e., one that is
  8.        not detached), produces a "zombie thread".  Avoid doing this,
  9.        since each zombie thread consumes some system resources, and when
  10.        enough zombie threads have accumulated, it will no longer be
  11.        possible to create new threads (or processes).

Further, you can't even change FreeOnTerminate inside the OnTerminate handler because of https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L131

Thus, you can't Free into OnTerminate handler, the instance memory gets invalidated for the assignment on  https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L133 . The only thing you can do is to nil your reference in the case it has FreeOnTerminate set previously (and it is just to be freed by ThreadFunc).

That is what makes your example not better than 3rd.
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #24 on: May 30, 2023, 12:10:32 am »
Thats really interesting, because unix pthreads seem to have a special implementation. The default implementation is in classes.inc: https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/objpas/classes/classes.inc#L185
Code: Pascal  [Select][+][-]
  1.     try
  2.       { The thread may be already terminated at this point, e.g. if it was intially
  3.         suspended, or if it wasn't ever scheduled for execution for whatever reason.
  4.         So bypass user code if terminated. }
  5.       if not Thread.Terminated then begin
  6.         CurrentThreadVar := Thread;
  7.         Thread.Execute;
  8.       end;
  9.     except
  10.       Thread.FFatalException := TObject(AcquireExceptionObject);
  11.     end;
  12.     FreeThread := Thread.FFreeOnTerminate;
  13.     Result := Thread.FReturnValue;
  14.     Thread.FFinished := True;
  15.     Thread.DoTerminate;
  16.     if FreeThread then
  17.       Thread.Free;
  18.  
As you can see the order of DoTerminate and Finished is different here. So it's nice that you wrote all that about how it works on your system, but thats completely irrelevant for any windows user.

Finished threads must be detached (in case of FreeOnTerminate) or joined with either TThread.WaitFor or TThread.Destroy (implicit WaitFor), to quote the manual:
Code: Text  [Select][+][-]
  1.        After a successful call to pthread_join(), the caller is
  2.        guaranteed that the target thread has terminated.  The caller may
  3.        then choose to do any clean-up that is required after termination
  4.        of the thread (e.g., freeing memory or other resources that were
  5.        allocated to the target thread).
  6.  
  7.        Failure to join with a thread that is joinable (i.e., one that is
  8.        not detached), produces a "zombie thread".  Avoid doing this,
  9.        since each zombie thread consumes some system resources, and when
  10.        enough zombie threads have accumulated, it will no longer be
  11.        possible to create new threads (or processes).
Thats the documentation of PThreads pthread_join function, not the documentation of TThread.WaitFor. TThread is a cross platform abstraction and using it should not require knowledge about the underlying library. The thing is, the concept of orphans is a Unix specific topic, under windows there is no such thing as a zombie thread, and absolutely no need to join threads.

And because we are using a High level wrapper and not pthreads directly, when you are calling Free (if you look at the SysDestroy function in tthread.inc), it will internally call WaitFor on Unix to get rid of the zombie. So the RTL does abstract that away to have a similar behavior than on Windows.

Also note, let's say I want to take your advice to heart and detach my thread... how should I exactly do that in a cross platform way? There is no TThread.Detach method... Maybe, when we talk about how to do something in Pascal with the RTL, we should stick to the documentation, features and caveats of the RTL not of some underlying system which the RTL abstracts away

Further, you can't even change FreeOnTerminate inside the OnTerminate handler because of https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L131

Thus, you can't Free into OnTerminate handler, the instance memory gets invalidated for the assignment on  https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/unix/tthread.inc#L133 . The only thing you can do is to nil your reference in the case it has FreeOnTerminate set previously (and it is just to be freed by ThreadFunc).

That is what makes your example not better than 3rd.
I did claimed that you should use FreeOnTerminate in the OnTerminate event, I said you should call Free inside. Under windows (which is where I have checked the RTL sources), this works perfectly fine, as after the OnTerminate event the thread object is only touched to perform a free on terminate. So if you are sure that there will not be a FreeOnTerminate, you can easiely call Free inside.

I was unaware that under Unix the order of setting Finished and doing the OnTerminate is swaped. This means you are right, you can't use that method on Unix. I think the reason for that swap is that in order to simulate the windows behavior, which still allows some messages to be handled (the windows message queue and waiting is kindof weird), and to simulate this, WaitFor actually performs the following loop on Unix:
Code: Pascal  [Select][+][-]
  1.     While not FFinished do  
  2.       CheckSynchronize(100);
  3.  
This allows the OnTerminate Synchronize to still be fired even if you are waiting (otherwise waitfor + OnTerminate would be a deadlock), but for this to work the order of setting Finished and DoTerminate was swapped.

But I would consider the different behavior between the platforms actually a bug, because this means two things: 1. Freeing in OnTerminate is no problem on Windows but on Unix, and second, freeing the thread once Finished is true is legal on Unix but not on Windows.
« Last Edit: May 30, 2023, 12:35:32 am by Warfley »

alpine

  • Hero Member
  • *****
  • Posts: 1295
Re: Threads - stringlist and programming tips
« Reply #25 on: May 30, 2023, 12:42:57 pm »
But I would consider the different behavior between the platforms actually a bug, because this means two things: 1. Freeing in OnTerminate is no problem on Windows but on Unix, and second, freeing the thread once Finished is true is legal on Unix but not on Windows.
Why blame the platform for our parricidal tendencies. In general, isn't it short-sighted to kill whoever called us? Wouldn't he intend to do more work after that?

But using both OnTerminate and FreeOnTerminate=True can have other dangerous implications. One of them is that the thread actually can outlive the instance of the OnTerminate handler, then calling it will result in dangling Self. That can be the case in a dynamically created child form which started a long running thread, then form cancelled, tread terminated with terminate (no waitfor) then form destroyed.

In case this is a main form, this won't happen since exiting the process will kill all threads belonging to it:
https://devblogs.microsoft.com/oldnewthing/20070503-00/?p=27003
https://learn.microsoft.com/en-us/windows/win32/procthread/terminating-a-process
But then the thread won't get a chance to make a proper shutdown.

So, the WaitFor is highly recommended.

Quote
As an example, I am currently writing a program that simply downloads a large ZIP archive (lazarus sources) and unzips them via TZipper. I put it in a thread with an Progressbar update event. But here is the catch, how can I stop this thread prematurely? Neither TFPHTTPClient nor TZipper have a function to stop mid process, so once one them started, it's around 5-10 minutes of the thread not being able to terminate. So any waitfor would Freeze the application for 5-10 minutes, resulting in the App being killed by windows.
Can you make a small example project demonstrating this, I doubt Windows will kill a program waiting on TThread.WaitFor in a GUI thread. Because of: https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/win/tthread.inc#L106
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1744
Re: Threads - stringlist and programming tips
« Reply #26 on: May 30, 2023, 01:28:00 pm »
Why blame the platform for our parricidal tendencies. In general, isn't it short-sighted to kill whoever called us? Wouldn't he intend to do more work after that?
I simply think that a cross platform library should try to archive the same behavior on all platforms. I don't expect anyone to read into all the different underlying implementations of all the different libraries (I mean this is the whole point of a cross platform library). If it is sensical to do a certain thing is a different question, but (at least to the degree that is possible) it should be equally (non)sensical on all platforms.

But using both OnTerminate and FreeOnTerminate=True can have other dangerous implications. One of them is that the thread actually can outlive the instance of the OnTerminate handler, then calling it will result in dangling Self. That can be the case in a dynamically created child form which started a long running thread, then form cancelled, tread terminated with terminate (no waitfor) then form destroyed.
Well lazarus managed forms will be created on program start and will be freed on program end. Even when you close a child form it will not be freed. So you must actively free the form for this to be a problem. And yes, you should not free memory that is still in use by the thread, but thats a more general rule, not just for events, and not just if you use FreeOnTerminate.
Also events may not just be Form Methods, in my current project I create a special object on which an event is called (so the thread is not bound to a certain form), and to make sure that this object will not outlive the thread, it registers itself in the OnTerminate to Free itself.

So I wouldn't consider this specifically an OnTerminate and FreeOnTerminate problem, but more generally a "don't free things still in use" problem.

Another issue that came to my mind a few month ago is, when Method Pointer as events are used, they consist of 2 pointers. This means it is not guaranteed that it will be atomically written or read. So this means that you need to encapsulate all event pointer accesses with a critical section. I did this in one of my projects, but it is really tedious, and it is usally (looking at RTL and LCL sources) not done. But technically this means that the use of OnTerminate is always unsafe.

Can you make a small example project demonstrating this, I doubt Windows will kill a program waiting on TThread.WaitFor in a GUI thread. Because of: https://gitlab.com/freepascal.org/fpc/source/-/blob/main/rtl/win/tthread.inc#L106
You are right, windows won't kill it, it will just freeze the form (which is already bad enough). That said since a few versions KDE also added the kill freezed applications feature.
But never the less you should not freeze the main thread for multiple seconds even if windows would not kill it. And if it is just because the user will not like it.

The problem with WaitFor is that there is not really any clean solution. The Application.ProcessMessages loop is probably the closest, but as soon as you have nested Application.ProcessMessages it breaks down. The self queing event doesn't have the problem but is really hacky and unreadable code.

OnTerminate as you pointed out only works on Windows, so after all FreeOnTerminate is by far the cleanest solution, that said, as you pointed out, it can be quite dangerous, and should only be used if there is just one reference to that thread and that it is either nilled once FreeOnTerminate is set, or is nilled during the OnTerminate event.

The latter is my favorite way of using a thread. Having the thread set FreeOnTerminate, and the OnTerminate event then nilling the reference to indicate to the main thread that it is not used anymore. But this is not always possible

cris75

  • Jr. Member
  • **
  • Posts: 65
Re: Threads - stringlist and programming tips
« Reply #27 on: June 19, 2023, 09:56:19 am »
Hi,
I'd like to apologize with everyone for my absence after I opened this thread, recent times have been really bad, I lost a close relative and at the same time I had some health problems, hope to fully recover asap  :'(
I wanted to apologize because somehow I feel indebted to all of you who every time take the time to help out without expecting anything in return (and this is not to be took for granted), thank you so much for your kind help (everytime) and all the replies to my last post,
a big thank you again,
Cris  :)
Lazarus: 3.2 / FPC: 3.2.2 [x86_64-win64-win32/win64]
Win10 x64
Debian 12 gtk2/qt5

 

TinyPortal © 2005-2018