oakley 0 Posted January 23, 2023 Good morning everybody, I am trying to program a little FTP Client for Ios where it is likely that the network connection goes away from time to time. Based on explanations from Remy Lebeau I use this code to do threaded FTP downloads, which works perfect and is not blocking GUI. The problem is, that when the network connection aborts, where do I handle the timeout? I set the trasnfer timeout, listen timeout, and connection timeout to 2000 . Well, I think the time out kicks but where in the thread do I handle it? Rgds Mirko constructor TLoadThread.Create; begin inherited Create(True); FreeOnTerminate := True; end; procedure TLoadThread.Execute; begin try Form2.FTP1.Connect; try try Form2.Ftp1.Get('file1.txt',TPath.Combine(TPath.GetTempPath, 'file1.txt'),True,false); Form2.Ftp1.Get('file2.txt',TPath.Combine(TPath.GetTempPath, 'file2.txt'),True,false); except on e: EIdSocketError do //I thaught I could handle the timeout here but when the transfer starts the exceltion is not thrown begin Form2.Memo1.Lines.add(DateTimetoStr(now()) + ' Error: ' + InttoStr(e.LastError) + ' ' + e.Message); end; end; finally Form2.FTP1.Disconnect; end; except on e: EIdSocketError do begin Form2.Memo1.Lines.add(DateTimetoStr(now()) + ' Error: ' + InttoStr(e.LastError) + ' ' + e.Message); //Error for not connecting to ftp server end; end; end; procedure TForm2.ThreadTerminated(Sender: TObject); begin xThread := nil; Loading := False; Memo1.Lines.Add(DateTimetoStr(now()) + ' Thread Terminated'); end; procedure TForm2.FTP1Status(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); begin TThread.Queue(nil, procedure begin Memo1.Lines.Add(DateTimetoStr(now()) + ' ' + AStatusText); end ); end; Share this post Link to post
programmerdelphi2k 237 Posted January 23, 2023 (edited) maybe some like this: // OnCreate //FReconnectTime := 1; // this would be a private field in "YourThread" = private FReconnectTime : boolean;" // OnExecute(...) //... try while not(Terminated) and (FReconnectTime < 4) do begin try // Ftp connect... // Ftp transfers... // break; // get out here! except on E: Exception do begin // ... FReconnectTime := FReconnectTime + 1; // // maybe a "sleep( n )" for wait before new try... avoiding overhead end; end; end; finally // if Ftp disconnect... end; Edited January 23, 2023 by programmerdelphi2k Share this post Link to post
oakley 0 Posted January 23, 2023 Okay thanks, gonna try this. But is the transfer timeout throwing an exception? If not, the except block is never fired. Rgds Mirko Share this post Link to post
programmerdelphi2k 237 Posted January 23, 2023 (edited) I think that is your "Form1" usage into a thread! try some like this: unit Unit2; interface uses System.Classes, System.SysUtils, System.Threading, // IdBaseComponent, // IdComponent, // IdTCPConnection, // IdTCPClient, // IdExplicitTLSClientServerBase, IdFTP; type TMyProc = procedure(AValue: string) of object; // your params... TMyThread = class(TThread) private FIdFTP : TIdFTP; FProc : TMyProc; FFileToTransfer: string; protected procedure Execute; override; public constructor Create(const ASuspended: boolean; const AFileToTransfer: string; const AProc: TMyProc); destructor Destroy; override; // // etc... end; implementation { TMyThread } constructor TMyThread.Create(const ASuspended: boolean; const AFileToTransfer: string; const AProc: TMyProc); begin inherited Create(ASuspended); // FreeOnTerminate := true; FIdFTP := TIdFTP.Create(nil); FProc := AProc; FFileToTransfer := AFileToTransfer; end; destructor TMyThread.Destroy; begin FIdFTP.Free; // inherited; end; procedure TMyThread.Execute; begin if (FIdFTP = nil) or (FFileToTransfer='') then exit; // // ... use "FIdFTP" now with your FFileToTransfer or a list of files... { TThread.Synchronize(nil, procedure begin // update your UI here... end); // // or just if Assigned(FProc) then FProc('hello world'); // this run in your main-thread = app! } end; end. type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private procedure MyUpdateUI(AParam: string); // your procedure... your params public end; var Form1: TForm1; implementation {$R *.dfm} uses Unit2; procedure TForm1.MyUpdateUI(AParam: string); begin // what to do? end; procedure TForm1.Button1Click(Sender: TObject); var MyThread: TMyThread; begin MyThread := TMyThread.Create(true, 'myfile.txt', MyUpdateUI); MyThread.Start; end; end. Edited January 23, 2023 by programmerdelphi2k 1 Share this post Link to post
oakley 0 Posted January 23, 2023 (edited) Just to see what really happens, I tried this with the effect that about 18 minutes after the network is gone I get: EIdSocketError:22 Socket Error # 22 Invalid argument . So far so good but why 18 minutes after I switched the wifi off? try Form2.Ftp1.Get('file1.txt',TPath.Combine(TPath.GetTempPath, 'file1.txt'),True,false); Form2.Ftp1.Get('file2.txt',TPath.Combine(TPath.GetTempPath, 'file2.txt'),True,false); except on e: EIdSocketError do //<- I thaught that I could catch a timeoute here begin Form2.Memo1.Lines.add(Format('%s EIdSocketError: %d %s', [DateTimetoStr(now),e.LastError,e.Message]); end; on e: EIdConnectTimeout do begin Form2.Memo1.Lines.add(Format('%s EIdConnectTimeout: %d %s', [DateTimetoStr(now),e.LastError,e.Message]); end; on e: EIdReadTimeout do begin Form2.Memo1.Lines.add(Format('%s EIdReadTimeout: %d %s', [DateTimetoStr(now),e.LastError,e.Message]); end; on e: EIdException do //<- this catches any INDY-Exception begin Form2.Memo1.Lines.add(Format('%s EIdException: %d %s', [DateTimetoStr(now),e.LastError,e.Message]); end; on e: Exception do //<- this catches any exception which is not cached before begin Form2.Memo1.Lines.add(Format('%s %s: %s', [DateTimetoStr(now),e.ClassName,e.Message]); end; end; Edited January 23, 2023 by oakley Share this post Link to post
programmerdelphi2k 237 Posted January 23, 2023 (edited) NOTE: Any exception that occurs within a thread will cause its termination immediately! (immediately = it should be, but not necessary) However, the consequences depend on what you were doing at that moment! why 18mins? I dont know! It's necessary see your project and test it ... (I dont iOS)! I think that it's not real for a "unconnected network"!!! Preferably, NEVER make direct access to visual components inside a thread!!! You should not use this type of coding within a thread, generally speaking! If the visual object was not created or no longer exists = error If the visual object is not thread safe (it usually isn't) = error Thus, if it is really necessary to make this access, always use "Thread.Synchronize() = force an immediate access; or Thread.Queue() = access when you can; That's what these two procedures are for in the TThread class! you can store your "error messages" in a property from the class, then, you can review it any time! some like this: type TMyProc = procedure(AValue: TArray<string>) of object; // your params... TMyThread = class(TThread) private FTries : integer; FIdFTP : TIdFTP; FProc : TMyProc; FFileToTransfer: string; FMyErrors : TArray<string>; protected procedure Execute; override; public constructor Create(const ASuspended: boolean; const AFileToTransfer: string; const AProc: TMyProc; const ATries: integer = 1); destructor Destroy; override; // property MyErrors: TArray<string> read FMyErrors; // if you want store each message! else, dont use this! end; implementation { TMyThread } constructor TMyThread.Create(const ASuspended: boolean; const AFileToTransfer: string; const AProc: TMyProc; const ATries: integer = 1); begin inherited Create(ASuspended); // FreeOnTerminate := true; FIdFTP := TIdFTP.Create(nil); FProc := AProc; FFileToTransfer := AFileToTransfer; FMyErrors := []; FTries := ATries; // test for example: if <1 and >10 then =1 end; destructor TMyThread.Destroy; begin FIdFTP.Free; // inherited; end; procedure TMyThread.Execute; var LTry: integer; begin FMyErrors := []; LTry := 1; // try while not(Terminated) and (LTry <= FTries) do begin try FIdFTP.Connect; except // on E: Exception do // for any case begin LTry := LTry + 1; FMyErrors := FMyErrors + [Exception(ExceptObject).message]; // if Assigned(FProc) then FProc([Exception(ExceptObject).message]); // sleep(500); end; end; end; finally FIdFTP.Disconnect; end; end; end. type TForm1 = class(TForm) //.. procedure Button1Click(Sender: TObject); private procedure MyUpdateUI(AParam: TArray<string>); // your procedure... your params end; implementation {$R *.dfm} uses Unit2; procedure TForm1.MyUpdateUI(AParam: TArray<string>); begin Memo1.Lines.AddStrings(AParam); end; procedure TForm1.Button1Click(Sender: TObject); var MyThread: TMyThread; begin MyThread := TMyThread.Create(true, '', MyUpdateUI, 3); MyThread.Start; end; Edited January 23, 2023 by programmerdelphi2k Share this post Link to post
Remy Lebeau 1398 Posted January 23, 2023 9 hours ago, oakley said: The problem is, that when the network connection aborts, where do I handle the timeout? I set the trasnfer timeout, listen timeout, and connection timeout to 2000 . Well, I think the time out kicks but where in the thread do I handle it? Are you using TIdFTP in Active mode (TIdFTP.Passive=False, which is the default) or in Passive mode (TIdFTP.Passive=True)? Different modes use different timeouts, but since you are setting them all, you certainly should be getting a timeout error one way or the other. However, your try..except is catching only exceptions derived from EIdSocketError, but for instance EIdAcceptTimeout (which TIdFTP raises in Active mode if the server doesn't connect to TIdFTP within the ListenTimeout interval) is not derived from EIdSocketError. In the code you have shown, I don't really see a need to have the try..except differentiate which type of exception is raised. It should be catching all of them. You are also not synchronizing with the UI thread when accessing your UI controls. Try something more like this instead: constructor TLoadThread.Create; begin inherited Create(True); FreeOnTerminate := True; end; procedure TLoadThread.Execute; begin try Form2.FTP1.Connect; try Form2.Ftp1.Get('file1.txt', TPath.Combine(TPath.GetTempPath, 'file1.txt'), True, False); Form2.Ftp1.Get('file2.txt', TPath.Combine(TPath.GetTempPath, 'file2.txt'), True, False); finally Form2.FTP1.Disconnect; end; except on e: Exception do begin TThread.Synchronize(nil, procedure begin if e is EIdSocketError then Form2.AddToMemo('Error: ' + IntToStr(EIdSocketError(e).LastError) + ' ' + e.Message) else Form2.AddToMemo('Error: ' + e.Message); end ); end; end; end; procedure TForm2.ThreadTerminated(Sender: TObject); begin xThread := nil; Loading := False; AddToMemo('Thread Terminated'); end; procedure TForm2.FTP1Status(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); begin TThread.Queue(nil, procedure begin AddToMemo(AStatusText); end ); end; procedure TForm2.AddToMemo(const AMsg: string); begin Memo1.Lines.Add(DateTimeToStr(Now()) + ' ' + AMsg); end; Alternatively, you can move your exception logging to the thread's OnTerminate event handler instead, eg: constructor TLoadThread.Create; begin inherited Create(True); FreeOnTerminate := True; end; procedure TLoadThread.Execute; begin Form2.FTP1.Connect; try Form2.Ftp1.Get('file1.txt', TPath.Combine(TPath.GetTempPath, 'file1.txt'), True, False); Form2.Ftp1.Get('file2.txt', TPath.Combine(TPath.GetTempPath, 'file2.txt'), True, False); finally Form2.FTP1.Disconnect; end; end; procedure TForm2.ThreadTerminated(Sender: TObject); var exc: Exception; begin xThread := nil; Loading := False; if TThread(Sender).FatalException <> nil then begin exc := Exception(TThread(Sender).FatalException); if exc is EIdSocketError then AddToMemo('Error: ' + IntToStr(EIdSocketError(exc).LastError) + ' ' + e.Message) else AddToMemo('Error: ' + e.Message); end; AddToMemo('Thread Terminated'); end; procedure TForm2.FTP1Status(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); begin TThread.Queue(nil, procedure begin AddToMemo(AStatusText); end ); end; procedure TForm2.AddToMemo(const AMsg: string); begin Memo1.Lines.Add(DateTimeToStr(Now()) + ' ' + AMsg); end; Share this post Link to post
oakley 0 Posted January 24, 2023 Hi Remy, I am using passive mode . I tried both of your suggestions but I always get the same results. I start my download transfer, switch off wifi to simulate a nework interruption but I dont see a timeout. I changed the try except to catch also other errors like that, but nothing pops up. except on e: EIdSocketError do begin TThread.Synchronize(nil, procedure begin Form2.Memo1.Lines.add(Format('%s EIdSocketError: %d %s', [DateTimetoStr(now),e.LastError,e.Message])); end); end; on e: EIdConnectTimeout do begin TThread.Synchronize(nil, procedure begin Form2.Memo1.Lines.add(Format('%s EIdConnectTimeout: %d %s', [DateTimetoStr(now),e.Message])); end); end; on e: EIdReadTimeout do begin TThread.Synchronize(nil, procedure begin Form2.Memo1.Lines.add(Format('%s EIdReadTimeout: %d %s', [DateTimetoStr(now),e.Message])); end); end; on e: EIdException do begin TThread.Synchronize(nil, procedure begin Form2.Memo1.Lines.add(Format('%s EIdException: %d %s', [DateTimetoStr(now),e.Message])); end); end; on e: Exception do begin TThread.Synchronize(nil, procedure begin Form2.Memo1.Lines.add(Format('%s %s: %s', [DateTimetoStr(now),e.ClassName,e.Message])); end); end; end; Share this post Link to post