Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   TidFTP in Thread (https://www.delphipraxis.net/168230-tidftp-thread.html)

nru 10. Mai 2012 12:31

Delphi-Version: 7

TidFTP in Thread
 
Verdammte Hacke. Ich dreh mich schon seit Stunden im Kreis.
Ob ihr mir bitte beim CodeReview helfen würdet.

Gegeben ist D7, Indy9
Vorhaben: Download-Threads mit TIdFTP. Die einzelnen Threads werden in einer TObjectList abgelegt (OwnObjects=False). Threads werden über eine class function erstellt und in ObjectList geadded. FreeOnTerminate=True erstellt. Aus den OnWork-Eventfunctions werden PostMessages an GUI geschickt.

Aktuelles Problem: Zugriffsverletzung beim Destroy, nach Fertigstellung des Downloads. Und ich komm einfach nicht drauf, woran es hängt. Ich meine, dass das gefühlsmäßig mit den neu aufgenommenen PostMessages zu tun hat. Denn als die noch nicht dabei waren (gestern) liefs eigentlich rund.

Vielen Dank schonmal fürs Reviewen ;)

So siehts (in Auszügen) aus:

Delphi-Quellcode:
uses windows, messages, sysutils, classes, SyncObjs, Contnrs,
     idComponent, idFTP, IdFTPList, IdFTPCommon, xProcs, ExtCtrls;

const

  WM_START_FTPDOWN = WM_USER + 401;
  WM_FINISH_FTPDOWN = WM_USER + 402;
  WM_FTP_MESSAGE = WM_USER + 403;
  WM_FTP_STATUSMSG = WM_USER + 404;

type
  PTransfer = ^TTransfer;
  TTransfer = record
    Percent: Integer;
    Aktuell,
    Overall,
    Total: Int64;
  end;

  TFTPDown = class(TThread)
  private
    FTransferInfo: TTransfer;
    FInfoTimer: TTimer;
    FLastWorkCount: Int64;
    FGesamt: Int64;
    Fmsg: PChar;
    FMainWndHandle: HWND;
    FLocalPath,
    FToLoad: String;
    FFTPClient: TidFTP;
    procedure OnStartThread();
    procedure OnPreTerminate();
    procedure OnMessage();
  protected
    procedure DownloadFolder(AFTP: TIdFtp; ARemotePath, ALocalPath:string; bOverwrite:Boolean);
    function FindFile( AFTP: TIdFtp ): Boolean;
    function GetFolderSize( ARemotePath: String ): Int64;
    procedure DoTimer( Sender: TObject );

    procedure FTPWork(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer);
    procedure FTPWorkBegin(Sender: TObject; AWorkMode: TWorkMode; const AWorkCountMax: Integer);
    procedure FTPWorkEnd( Sender: TObject; AWorkMode: TWorkMode);
  public
    class function DoDownLoad( mainwnd: HWND; const dlItem: String ): boolean;
    class procedure ClearDownloads;
    class function HasActiveDownloads: Boolean;

    constructor create( mainthr: HWND; const _Item: String );
    destructor Destroy(); override;
    procedure Execute; override;
    property DownFilename: string read FToLoad;
    property Size: Int64 read FGesamt;
  end;


implementation

var
  FDownList: TObjectList;


class function TFTPDown.DoDownLoad( mainwnd: HWND; const dlItem: String ): boolean;
begin
  FDownList.Add( Create( mainwnd, dlItem ) );
end;
class function TFTPDown.HasActiveDownloads: Boolean;
begin
  result := (FDownList.Count > 0);
end;
class procedure TFTPDown.ClearDownloads;
var
  i: integer;
begin
  for i := Pred(FDownList.Count) downto 0 do begin
    if Assigned((FDownList.Items[i] as TFTPDown)) then begin
      (FDownList.Items[i] as TFTPDown).Terminate;
    end;
  end;
end;

constructor TFTPDown.create( mainthr: HWND; const _Item: String );
begin
  FMainWndHandle := mainthr;       {Hauptanwendung bekommt Messages}
  FToLoad := _Item;                {Datei- oder Verzeichnisname des DL}
  FLocalPath := strAddSlash(regReadDefValue( HKEY_CURRENT_USER, 'software\FloSoft\DVBase\TSRSS\btFTPDLDir', dtString, ExtractFilePath(ParamStr(0))) );

  FFTPClient := TidFTP.Create(nil);
  FFTPClient.Username := regReadValue( HKEY_CURRENT_USER, 'software\FloSoft\DVBase\TSRSS\btUser', dtString);
  FFTPClient.Password := regReadValue( HKEY_CURRENT_USER, 'software\FloSoft\DVBase\TSRSS\btPass', dtString);
  FFTPClient.Host    := regReadValue( HKEY_CURRENT_USER, 'software\FloSoft\DVBase\TSRSS\btHost', dtString);
  FFTPClient.Port    := 21;
  FFTPClient.Passive := True;
  FFTPClient.TransferType := ftBinary;
  FFTPClient.OnWork  := FTPWork;
  FFTPClient.OnWorkBegin := FTPWorkBegin;
  FFTPClient.OnWorkEnd := FTPWorkEnd;


  FInfoTimer := TTimer.Create( nil );
  FInfoTimer.Enabled := False;
  FInfoTimer.Interval := 1000;
  FInfoTimer.OnTimer := DoTimer;

  inherited Create( false );
  FreeOnTerminate := True;
end;

destructor TFTPDown.Destroy();
begin
  FreeAndNil( FInfoTimer );
  FreeAndNil( FFTPClient );
  FDownList.Remove(self);
  inherited Destroy;
end;


procedure TFTPDown.Execute;
var
  cRemotePath: String;
  lSingleFile: Boolean;
begin

  FLastWorkCount := 0;
  FTransferInfo.Percent := 0;
  FTransferInfo.Aktuell := 0;
  FTransferInfo.Overall := 0;
  FTransferInfo.Total  := 0;

  Synchronize( OnStartThread );
  FInfoTimer.Enabled := True;

  try
    FFTPClient.Connect;
  except
    on E:Exception do begin
      FMsg := PChar('Fehler bei der Verbindungsaufnahme ('+e.Message+')');
      Synchronize( OnMessage );
      FFTPClient.Disconnect;
      FFTPClient.Quit;
      exit;
    end;
  end;

  // Prüfen - Datei oder Ordner?
  try
      lSingleFile := False;
      cRemotePath := 'files/'+FToLoad;
      FFTPClient.ChangeDir(cRemotePath);
  except
      // Only Dateidownload
      lSingleFile := True;
      cRemotePath := 'files/';
      FFTPClient.ChangeDir(cRemotePath);
  end;

  // Transfer einleiten
  if not Terminated then begin
    FMsg := 'Download startet';
    Synchronize( OnMessage );

    if lSingleFile then begin
      if FindFile( FFTPClient ) then begin
        FTransferInfo.Total := FGesamt;
        if Fileexists(FLocalPath+FToLoad) then DeleteFile(FLocalPath+FToLoad);
        FFTPClient.Get(FToLoad, FLocalPath+FToLoad);
      end;
    end else begin
      FGesamt := GetFolderSize( cRemotePath );
      FTransferInfo.Total := FGesamt;
      ForceDirectories(FLocalPath+FToLoad);
      DownloadFolder(FFTPClient, cRemotePath, FLocalPath+FToLoad, true);
    end;
  end;

  // = Disconnect and Free FTP-Instance
  FFTPClient.Disconnect;
  FFTPClient.Quit;

  FInfoTimer.Enabled := False;

  if not Terminated then
    Synchronize( OnPreTerminate );

end;

DownloadFolder ist eine rekursiv aufgerufene Function, die evtl. vorhandene SubDirs berücksichtigt.
Delphi-Quellcode:
procedure TFTPDown.FTPWork(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer);
begin
  // Abbruch des Get wird hier ermöglicht.
  // Das geschieht in Indy via Exception. Und das muss passieren, wenn der Thread
  // gekillt werden soll (z.B. Programmende)
  if Terminated then Abort;

  Inc( FTransferInfo.Overall, AWorkCount-FLastWorkCount );
  FLastWorkCount := AWorkCount;
  if FTransferInfo.Total > 0 then
    FTransferInfo.Percent := round((100 / FTransferInfo.Total) * FTransferInfo.Overall);
end;
procedure TFTPDown.FTPWorkBegin(Sender: TObject; AWorkMode: TWorkMode; const AWorkCountMax: Integer);
begin
end;
procedure TFTPDown.FTPWorkEnd( Sender: TObject; AWorkMode: TWorkMode);
begin
  FLastWorkCount := 0;
end;


procedure TFTPDown.OnStartThread();
begin
  if FMainWndHandle <> 0 then
    PostMessage( FMainWndHandle, WM_START_FTPDOWN, Integer(self), 0 );
end;
procedure TFTPDown.OnPreTerminate();
begin
  if FMainWndHandle <> 0 then
    PostMessage( FMainWndHandle, WM_FINISH_FTPDOWN, Integer(self), 0 );
end;
procedure TFTPDown.OnMessage();
begin
  if FMainWndHandle <> 0 then
    PostMessage( FMainWndHandle, WM_FTP_MESSAGE, Integer(Fmsg), 0 );
end;
procedure TFTPDown.DoTimer( Sender: TObject );
begin
  if FMainWndHandle <> 0 then
    PostMessage( FMainWndHandle, WM_FTP_STATUSMSG, 0, LPARAM(@FTransferInfo) );
end;



initialization
  FDownList := TObjectList.Create( false );   // OwnObjects=False, damit Freigabe gesteuert werden kann
finalization
  FreeAndNil( FDownList );


Start des Threads und Messagehandling in der GUI wie folgt:

Delphi-Quellcode:
procedure TfrmMain.dxFTPDownClick(Sender: TObject);
var
  cOrdner: String;
begin

  RSSView.DataController.DataSource.DataSet.Bookmark := RSSView.DataController.GetSelectedBookmark(RSSView.Controller.SelectedRecordCount-1);
  cOrdner := RSSDB.ap.GetTrimString('FTPNAME');

  if cOrdner = '' then begin
    AddLog( 'FTP: Verzeichnis od. Dateiname nicht hinterlegt!' );
    exit;
  end;

  // DownloadThread starten und in interne ObjectList setzen
  TFTPDown.DoDownload( self.Handle, cOrdner );

end;

procedure TfrmMain.OnStartFTPDownloadThread( var Msg: TMessage );
begin
  AddLog( 'FTP: Transfer Thread gestartet - ' + TFTPDown(msg.wparam).DownFilename );
end;
procedure TfrmMain.OnFinishFTPDownloadThread( var Msg: TMessage );
begin
  AddLog( 'FTP: FTPTransfer abgschlossen - ' + TFTPDown(msg.wparam).DownFilename );
  jvStatusbar1.Panels[1].Text := '';
end;
procedure TfrmMain.OnFTPThreadMessage( var Msg: TMessage );
begin
  AddLog( 'FTP: ' + PChar(msg.wparam) );
end;
procedure TfrmMain.OnFTPStatusMessage( var Msg: TMessage );
var
  cLog: String;
begin
  cLog := Format('FTP: %s / %s geladen ( %d%% )',
                  [FileSizeToStr(PTransfer(msg.lparam)^.Overall),
                   FileSizeToStr(PTransfer(msg.lparam)^.Total),
                   PTransfer(msg.lparam)^.Percent ]);
  AddLog( cLog );
  jvStatusbar1.Panels[1].Text := cLog;
end;

nru 10. Mai 2012 13:18

AW: TidFTP in Thread
 
:idea:

War der Timer, nicht die Messages.

TTimer gegen TJvTimer ersetzt und nun läufts.

sx2008 11. Mai 2012 06:48

AW: TidFTP in Thread
 
Da ist noch ein Bug:
Delphi-Quellcode:
try
  FFTPClient.Connect;
except
  on E:Exception do begin
    // FMsg ist ein Zeiger (PChar)
    FMsg := PChar('Fehler bei der Verbindungsaufnahme ('+e.Message+')');
    Synchronize( OnMessage );
    FFTPClient.Disconnect;
    FFTPClient.Quit;
    exit;
    // auf was wird der Zeiger wohl zeigen, wenn die Methode verlassen wird?
    // -> auf ungültigen Speicher !!
    // und was passiert dann in der Procedure OnMessage?
    // -> ungültiger Speicher wird ausgelesen
    // weil ja zuerst "exit" ausgeführt wird und wegen dem Synchronize-Aufruf
    // die Procedure OnMessage danach abgearbeitet wird
  end;
end;
Nachtrag:
da sind ZWEI Bugs.
Der 2. Bug ist der Aufruf von
Delphi-Quellcode:
FFTPClient.Disconnect;
FFTPClient.Quit;
Wenn man eine aktive FTP-Verbindung beenden möchte schickt man zuerst QUIT und danach kommt der Disconnect.
Also ist die Reihenfolge falsch.
Aber, da der Connect ja sowieso nicht geklappt hat, ist es unlogisch die Verbindung beenden zu wollen.

nru 11. Mai 2012 10:02

AW: TidFTP in Thread
 
Dankeschön fürs Feedback :thumb:


Alle Zeitangaben in WEZ +1. Es ist jetzt 20:39 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