Jump to content
oakley

Threaded FTP and Timeouts

Recommended Posts

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

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 by programmerdelphi2k

Share this post


Link to post

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

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 by programmerdelphi2k
  • Like 1

Share this post


Link to post

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 by oakley

Share this post


Link to post

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 by programmerdelphi2k

Share this post


Link to post
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

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

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×