Download and process files with IOmniPipeline - multithreading
My goal is a VCL app where I need to concurrently download some files (URLs stored in TDataSet), then each of downloaded file must be processed (one-by-one). During app working GUI must not be hanged, user user should be able to cancel (interrupt) process in any stage.
I totally rewrite my first example, now there is no third-party classes (Omni Thread Library 3.07.6 and VCL only). Of course, it's still demo and some checks was removed from code, but however, this sample still not short unfortunately.
Downloading part based on this answer (thanks!).
So, when any file downloading I need to show progress to this files in GUI. Downloader class generate "event" OnProgressChange (because in real app I'll use TALWinInetHTTPClient class instance from Alcinoe library and it has real event OnProgressChange). I think it's enough just write progress value in DB, then DBGrid show progress values correctly.
Of course I saw this answers (and some other related with OTL):
How to Stop all Pipeline tasks correctly
How to use Pipeline pattern in Delphi (it's similar to my tasks, but differences in details).
Also I saw OTL docs and examples but I can't find some real example for doing similar task.
I created some classes to solve this task, and it's works, but have some critical troubles:
At first stage downloading not starts separately (not parallels, but one-by-one).
Cancellation not works properly.
Below some code to illustrate my problem. It's contain two units, one is main form (GUI, preparing data, interaction with user), second is Pipeline wrapper and downloader.
DFM for main form is:
object fmMain: TfmMain
Left = 628
Top = 172
Caption = 'WorkSpace preparer'
ClientHeight = 262
ClientWidth = 700
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Segoe UI'
Font.Style = []
OldCreateOrder = False
Position = poScreenCenter
OnCloseQuery = FormCloseQuery
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 17
object DBGridApps: TDBGrid
AlignWithMargins = True
Left = 3
Top = 3
Width = 694
Height = 207
Align = alClient
DataSource = dsApps
ReadOnly = True
TabOrder = 0
TitleFont.Charset = DEFAULT_CHARSET
TitleFont.Color = clWindowText
TitleFont.Height = -13
TitleFont.Name = 'Segoe UI'
TitleFont.Style = []
end
object Panel1: TPanel
AlignWithMargins = True
Left = 3
Top = 216
Width = 694
Height = 43
Align = alBottom
TabOrder = 1
object bbExit: TBitBtn
AlignWithMargins = True
Left = 549
Top = 4
Width = 141
Height = 35
Align = alRight
Caption = 'Exit'
TabOrder = 0
OnClick = bbExitClick
end
object bbCancel: TBitBtn
AlignWithMargins = True
Left = 151
Top = 4
Width = 141
Height = 35
Align = alLeft
Caption = 'Cancel'
TabOrder = 1
OnClick = bbCancelClick
ExplicitTop = 0
end
object bbStart: TBitBtn
AlignWithMargins = True
Left = 4
Top = 4
Width = 141
Height = 35
Align = alLeft
Caption = 'Start'
TabOrder = 2
OnClick = bbStartClick
end
end
object dsApps: TDataSource
DataSet = cdsApps
Left = 32
Top = 88
end
object cdsApps: TClientDataSet
Aggregates = <>
Params = <>
Left = 16
Top = 72
end
end
Main form code:
unit MainForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, DBClient, Grids, DBGrids, StdCtrls, Buttons, ExtCtrls,
PipelineHolder;
type
TfmMain = class(TForm)
DBGridApps: TDBGrid;
dsApps: TDataSource;
Panel1: TPanel;
bbExit: TBitBtn;
bbCancel: TBitBtn;
bbStart: TBitBtn;
cdsApps: TClientDataSet;
procedure bbExitClick(Sender: TObject);
procedure bbCancelClick(Sender: TObject);
procedure bbStartClick(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
PH : TPipelineHolder;
procedure SwitchControlState;
public
{ Public declarations }
end;
var
fmMain: TfmMain;
implementation
{$R *.dfm}
procedure TfmMain.bbExitClick(Sender: TObject);
begin
Close;
end;
procedure TfmMain.bbCancelClick(Sender: TObject);
begin
if Assigned(PH) then
begin
SwitchControlState;
PH.Stop;
end;
end;
procedure TfmMain.bbStartClick(Sender: TObject);
begin
if not Assigned(PH) then
PH := TPipelineHolder.Create;
SwitchControlState;
PH.Make(cdsApps);
end;
procedure TfmMain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := MessageBox(0, 'Exit now?', 'Exit', MB_YESNO + MB_ICONQUESTION + MB_DEFBUTTON2 + MB_TOPMOST) = IDYES;
if CanClose then bbCancel.Click;
end;
procedure TfmMain.FormCreate(Sender: TObject);
begin
//Prepare dataset
cdsApps.Close;
With cdsApps do
begin
FieldDefs.Add('progress', ftFloat);
FieldDefs.Add('status', ftString, 30);
FieldDefs.Add('id', ftString, 30);
FieldDefs.Add('uid', ftString, 30);
FieldDefs.Add('title', ftString, 30);
FieldDefs.Add('url', ftString, 255);
FieldDefs.Add('silent_parameters', ftString, 255);
FieldDefs.Add('target_file', ftString, 255);
CreateDataSet;
LogChanges := False;
Open;
// Below you can change URL as you wish.
// For example I'll use VirtualBox distrib from this page: https://www.virtualbox.org/wiki/Downloads
// To correct progress values web-server must response with correct content-lenght values and must
// support HEAD command.
// Record 1
Append;
Fields[0].AsFloat := 0.0;
Fields[1].AsString := 'Ready';
Fields[2].AsString := '5be2e746ce46a1000cdc8b90';
Fields[3].AsString := 'SomeApp1';
Fields[4].AsString := 'VirtualBox 6.0.10';
Fields[5].AsString := 'https://download.virtualbox.org/virtualbox/6.0.10/VirtualBox-6.0.10-132072-Win.exe';
Fields[6].AsString := '/S';
Fields[7].AsString := '001_installer.exe';
Post;
// Record 2
Append;
Fields[0].AsFloat := 0.0;
Fields[1].AsString := 'Ready';
Fields[2].AsString := '5be31c63ce46a1000b268bb2';
Fields[3].AsString := 'SomeApp2';
Fields[4].AsString := 'VirtualBox 6.0.10';
Fields[5].AsString := 'https://download.virtualbox.org/virtualbox/6.0.10/VirtualBox-6.0.10-132072-Win.exe';
Fields[6].AsString := '';
Fields[7].AsString := '002_installer.exe';
Post;
// Record 3
Append;
Fields[0].AsFloat := 0.0;
Fields[1].AsString := 'Ready';
Fields[2].AsString := '5be3428ace46a1000b268bc0';
Fields[3].AsString := 'SomeApp3';
Fields[4].AsString := 'VirtualBox 6.0.10';
Fields[5].AsString := 'https://download.virtualbox.org/virtualbox/6.0.10/VirtualBox-6.0.10-132072-Win.exe';
Fields[6].AsString := '/VERY_SILENT';
Fields[7].AsString := '003_installer.exe';
Post;
// Record 4
Append;
Fields[0].AsFloat := 0.0;
Fields[1].AsString := 'Ready';
Fields[2].AsString := '5be3428ace46a1000b268bc1';
Fields[3].AsString := 'SomeApp4';
Fields[4].AsString := 'VirtualBox 6.0.10';
Fields[5].AsString := 'https://download.virtualbox.org/virtualbox/6.0.10/VirtualBox-6.0.10-132072-Win.exe';
Fields[6].AsString := '';
Fields[7].AsString := '004_installer.exe';
Post;
// Record 5 - it's not exe, just simple testing file, in this demo at
// Install method with this file will set status to error.
Append;
Fields[0].AsFloat := 0.0;
Fields[1].AsString := 'Ready';
Fields[2].AsString := '5be512bb4a9bbb000b6de944';
Fields[3].AsString := 'SomeFile';
Fields[4].AsString := 'Demo File (not executable)';
Fields[5].AsString := 'https://speed.hetzner.de/100MB.bin';
Fields[6].AsString := '';
Fields[7].AsString := '005_sample_100MB.bin';
Post;
First;
end;
end;
procedure TfmMain.SwitchControlState;
begin
bbStart.Enabled := not bbStart.Enabled;
end;
end.
Second unit for pipeline working implementation:
unit PipelineHolder;
interface
uses
Windows, SysUtils, Classes, OtlCommon, OtlCollections, OtlParallel, Forms,
DB, Generics.Defaults, StrUtils, Generics.Collections, Messages, OtlComm,
OtlTask, OtlTaskControl, ShellAPI, Dialogs, OtlSync, Math, WinInet;
// Messages
const
WM_PROGRESSCHANGED = WM_APP + 105;
// Process states
type
TAppState = (asReady = 0, asCancelled = 1, asError = 2, asDownloading = 3, asDownloaded = 4, asInstalling = 5, asCompleted = 6);
TAppStateNames = array[asReady..asCompleted] of string;
const
AppState: TAppStateNames = ('Ready', 'Canceled', 'Error', 'Downloading', 'Downloaded', 'Installing', 'Installed');
type
// Data structs for progress message
PProgressInfo = ^TProgressInfo;
TProgressInfo = record
Read : Int64;
Total : Int64;
ID : string;
URL : string;
end;
//Structure for record info
TRecordInfo = record
Filename: string;
URL: string;
ID: string;
Cmd : string;
end;
// Class for downloading
TDBAppItem = class
private
FHandle : HWND;
FDS : TDataSet;
FFilename: string;
FURL: string;
FId: string;
FCmd : string;
FFileSize : Int64;
FDownloaded : Int64;
function GetWinInetError(ErrorCode: Cardinal): string;
procedure ParseURL(const lpszUrl: string; var Host, Resource: string);
function GetRemoteFileSize(const Url : string): Integer;
function DownloadFile(const url: string; const TargetFileName: string): boolean;
procedure InternalDownloadProgress(Sender: TObject; Read: Integer; Total: Integer);
public
constructor Create(const OwnerHandle: HWND; var DS: TDataSet; const URL, ID: string; const Cmd: string; const TargetFilename: string);
destructor Destroy; override;
function Download : Boolean; overload;
end;
// Main class, pipeline holder
TPipelineHolder = class
private
FDS : TDataSet;
FHandle : HWND;
FPipeline : IOmniPipeline;
FInProcess: Boolean;
procedure Retrieve(const input: TOmniValue; var output: TOmniValue);
procedure RetrieveAll(const input, output: IOmniBlockingCollection);
procedure Install(const input, output: IOmniBlockingCollection);
procedure JobDone;
procedure WndProc(var Message: TMessage);
procedure WMProgressChanged(var msg: TMessage); message WM_PROGRESSCHANGED;
public
constructor Create;
destructor Destroy; override;
procedure Make(SourceDS : TDataSet);
function Stop: Boolean;
property InProcess: Boolean read FInProcess write FInProcess;
end;
implementation
{ Tools }
function RunAsAdmin(const Handle: HWnd; const Filename, Params: string): Boolean;
var
sei: TShellExecuteInfo;
begin
FillChar(sei, SizeOf(sei), 0);
sei.cbSize := SizeOf(sei);
sei.Wnd := Handle;
sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
sei.lpVerb := 'runas';
sei.lpFile := PChar(Filename);
sei.lpParameters := PChar(Params);
sei.nShow := SW_SHOWNORMAL;
Result := ShellExecuteEx(#sei);
end;
{TPipelineHolder}
constructor TPipelineHolder.Create;
begin
inherited Create;
FHandle := AllocateHWnd(WndProc);
FInProcess := False;
end;
destructor TPipelineHolder.Destroy;
begin
if FInProcess then
if Assigned(FPipeline) then
begin
FPipeline.Cancel;
FPipeline := nil;
FInProcess := False;
end;
if FHandle <> 0 then DeallocateHWnd(FHandle);
inherited;
end;
procedure TPipelineHolder.Install(const input, output: IOmniBlockingCollection);
var
app : TOmniValue;
appFile : string;
appParams: string;
ID : string;
State : string;
AppInfo : TRecordInfo;
begin
// In real app here is downloaded file must be started as separate process and
// we must wait when it will be completed.
for app in input do
begin
if not app.IsEmpty then
begin
AppInfo := app.ToRecord<TRecordInfo>;
appFile := AppInfo.Filename;
appParams := AppInfo.Cmd;
ID := AppInfo.ID;
if (appFile <> EmptyStr) and (FileExists(appFile)) then
begin
// Change file state
FDS.DisableControls;
try
if FDS.Locate('id', ID, [loCaseInsensitive]) then
begin
FDS.Edit;
FDS.FieldByName('Status').AsString := AppState[asInstalling];
FDS.Post;
end;
finally
FDS.EnableControls;
end;
// Try to execute intsaller
if RunAsAdmin(Application.Handle, appFile, appParams) then
begin
State := AppState[asCompleted]
end
else
begin
State := AppState[asError];
end;
// Change state again
FDS.DisableControls;
try
if FDS.Locate('id', ID, [loCaseInsensitive]) then
begin
FDS.Edit;
FDS.FieldByName('Status').AsString := State;
FDS.Post;
end;
finally
FDS.EnableControls;
end;
end;
end;
end;
end;
procedure TPipelineHolder.Retrieve(const input: TOmniValue; var output: TOmniValue);
var
App: TDBAppItem;
Info : TRecordInfo;
begin
// Checking cancellation flag
if not FInProcess then Exit;
// Preparing
Info := input.ToRecord<TRecordInfo>;
App := TDBAppItem.Create(FHandle, FDS, Info.URL, Info.ID, Info.Cmd, Info.Filename);
// Downloading
try
if App.Download then
output := TOmniValue.FromRecord<TRecordInfo>(Info)
else
output.Clear;
finally
FreeAndNil(App);
end;
end;
procedure TPipelineHolder.RetrieveAll(const input,
output: IOmniBlockingCollection);
var
App: TDBAppItem;
Info : TRecordInfo;
value : TOmniValue;
begin
// Preparing
for value in input do
begin
if not FInProcess then Exit;
Info := value.ToRecord<TRecordInfo>;
App := TDBAppItem.Create(FHandle, FDS, Info.URL, Info.ID, Info.Cmd, Info.Filename);
// Downloading
try
if App.Download then
output.Add(TOmniValue.FromRecord<TRecordInfo>(Info));
finally
FreeAndNil(App);
end;
end;
end;
function TPipelineHolder.Stop: Boolean;
begin
if FInProcess then
begin
if Assigned(FPipeline) then
begin
FPipeline.Cancel;
FPipeline := nil;
FInProcess := False;
end;
end;
Result := not FInProcess;
end;
procedure TPipelineHolder.WMProgressChanged(var msg: TMessage);
var
MsgRec : TProgressInfo;
Percent, Current : Double;
Read, Total : Int64;
ID : string;
begin
MsgRec := PProgressInfo(Msg.LParam )^;
Read := MsgRec.Read;
Total := MsgRec.Total;
Percent := 100 * Read / Total;
ID := MsgRec.ID;
// Write data to db
if FDS.Locate('id', ID, [loCaseInsensitive]) then
begin
FDS.DisableControls;
try
Current := FDS.FieldByName('Progress').AsFloat;
if Current <> Trunc(Percent) then
begin
FDS.Edit;
FDS.FieldByName('Progress').AsFloat := Round(Percent);
if Percent >= 99 then
begin
FDS.FieldByName('Status').AsString := AppState[asDownloaded];
end;
FDS.Post;
end;
finally
FDS.EnableControls;
end;
end;
end;
procedure TPipelineHolder.WndProc(var Message: TMessage);
begin
Dispatch(Message);
inherited;
end;
procedure TPipelineHolder.JobDone;
begin
FPipeline := nil;
FInProcess := False;
end;
procedure TPipelineHolder.Make(SourceDS: TDataSet);
var
BM : TBookmark;
RecInfo : TRecordInfo;
begin
if SourceDS = nil then Exit;
if not SourceDS.Active then Exit;
if SourceDS.IsEmpty then Exit;
FDS := SourceDS;
FInProcess := True;
// Here at first stage calling Retrive or RetrieveAll gives same effect, no
// matter what we uses value or queue.
FPipeline := Parallel.Pipeline
.Stage(RetrieveAll, //Retrieve
Parallel.TaskConfig.OnMessage(Self)).NumTasks(Environment.Process.Affinity.Count * 2)
.Stage(Install)
.OnStop(JobDone)
.Run;
// Get URLs to be downloaded
BM := FDS.GetBookmark;
FDS.DisableControls;
try
FDS.First;
while not FDS.Eof do
begin
// Get data from database
RecInfo.URL := Trim(FDS.FieldByName('url').AsString);
RecInfo.Id := Trim(FDS.FieldByName('id').AsString);
RecInfo.Cmd := Trim(FDS.FieldByName('silent_parameters').AsString);
RecInfo.Filename := ExtractFilePath(ParamStr(0)) + 'Downloads\' + Trim(FDS.FieldByName('target_file').AsString);
if RecInfo.URL = EmptyStr then
begin
// Skips empty URLs
FDS.Next;
Continue;
end;
FDS.Edit;
FDS.FieldByName('Status').AsString := AppState[asDownloading];
FDS.Post;
FPipeline.Input.Add(TOmniValue.FromRecord<TRecordInfo>(RecInfo));
FDS.Next;
end;
finally
if FDS.BookmarkValid(BM) then SourceDS.GotoBookmark(BM);
FDS.FreeBookmark(BM);
FDS.EnableControls;
end;
FPipeline.Input.CompleteAdding;
// Wait for pipeline to complete - I'm not use it to avoid GUI freezing
// FPipeline.WaitFor(INFINITE);
end;
constructor TDBAppItem.Create(const OwnerHandle: HWND; var DS: TDataSet; const URL, ID, Cmd, TargetFilename: string);
begin
inherited Create;
FDS := DS;
FURL := URL;
FId := ID;
FCmd := Cmd;
FFilename := TargetFilename;
FHandle := OwnerHandle;
FFileSize := -1;
FDownloaded := 0;
end;
destructor TDBAppItem.Destroy;
begin
FDS := nil;
inherited;
end;
function TDBAppItem.Download: Boolean;
var
path : string;
begin
path := ExtractFilePath(FFilename);
if not DirectoryExists(path) then
if not ForceDirectories(path) then
raise Exception.Create('Cannot create directory: "'+path+'".');
if FileExists(FFilename) then
try
if not DeleteFile(FFilename) then
raise Exception.Create('Cannot delete file: "'+FFilename+'".');
except on E: Exception do
raise Exception.Create('Cannot delete file: "'+FFilename+'".'+sLineBreak + E.Message);
end;
Result := DownloadFile(FURL, FFilename);
if Result then Result := FileExists(FFilename);
end;
function TDBAppItem.DownloadFile(const url, TargetFileName: string): boolean;
var
hInet: HINTERNET;
hFile: HINTERNET;
localFile: file;
buffer: array[1..65535] of Byte;
bytesRead: DWORD;
b: boolean;
begin
b := False;
if FFileSize < 0 then FFileSize := GetRemoteFileSize(url);
FDownloaded := 0;
hInet := WinInet.InternetOpen('MyFileAgent', INTERNET_OPEN_TYPE_DIRECT, nil, nil, 0);
if Assigned(hInet) then
begin
hFile := InternetOpenURL(hInet, PChar(url), nil, 0, INTERNET_FLAG_PRAGMA_NOCACHE, 0);
if Assigned(hFile) then
begin
AssignFile(localFile, TargetFileName);
Rewrite(localFile, 1);
bytesRead := 0;
repeat
InternetReadFile(hFile, #buffer, SizeOf(buffer), bytesRead);
BlockWrite(localFile, buffer, bytesRead);
Inc(FDownloaded, bytesRead);
//In real app this event occurs in TALWinHttpClient from Alcinoe library.
InternalDownloadProgress(Self, FDownloaded, FFileSize);
until bytesRead = 0;
CloseFile(localFile);
InternetCloseHandle(hFile);
end;
InternetCloseHandle(hInet);
b := true;
end;
DownloadFile := b;
FFileSize := -1;
FDownloaded := 0;
end;
function TDBAppItem.GetRemoteFileSize(const Url: string): Integer;
const
sUserAgent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36';
var
hInet : HINTERNET;
hConnect : HINTERNET;
hRequest : HINTERNET;
lpdwBufferLength: DWORD;
lpdwReserved : DWORD;
ServerName, Resource: string;
// Prot, Host, User, Pass, Path, Extra: string;
ErrorCode : Cardinal;
begin
Result := -1;
ParseURL(Url, ServerName, Resource);
hInet := InternetOpen(PChar(sUserAgent), INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
if hInet=nil then
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('InternetOpen Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
try
hConnect := InternetConnect(hInet, PChar(ServerName), INTERNET_DEFAULT_HTTP_PORT, nil, nil, INTERNET_SERVICE_HTTP, 0, 0);
if hConnect=nil then
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('InternetConnect Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
try
hRequest := HttpOpenRequest(hConnect, PChar('HEAD'), PChar(Resource), nil, nil, nil, 0, 0);
if hRequest<>nil then
begin
try
lpdwBufferLength := SizeOf(Result);
lpdwReserved := 0;
if not HttpSendRequest(hRequest, nil, 0, nil, 0) then
begin
ErrorCode := GetLastError;
raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
if not HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_LENGTH or HTTP_QUERY_FLAG_NUMBER, #Result, lpdwBufferLength, lpdwReserved) then
begin
Result := 0;
ErrorCode := GetLastError;
raise Exception.Create(Format('HttpQueryInfo Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
finally
InternetCloseHandle(hRequest);
end;
end
else
begin
ErrorCode:=GetLastError;
raise Exception.Create(Format('HttpOpenRequest Error %d Description %s',[ErrorCode,GetWinInetError(ErrorCode)]));
end;
finally
InternetCloseHandle(hConnect);
end;
finally
InternetCloseHandle(hInet);
end;
end;
function TDBAppItem.GetWinInetError(ErrorCode: Cardinal): string;
const
winetdll = 'wininet.dll';
var
Len: Integer;
Buffer: PChar;
begin
Len := FormatMessage(
FORMAT_MESSAGE_FROM_HMODULE or FORMAT_MESSAGE_FROM_SYSTEM or
FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_IGNORE_INSERTS or FORMAT_MESSAGE_ARGUMENT_ARRAY,
Pointer(GetModuleHandle(winetdll)), ErrorCode, 0, #Buffer, SizeOf(Buffer), nil);
try
while (Len > 0) and (CharInSet(Buffer[Len - 1], [#0..#32, '.'])) do Dec(Len);
SetString(Result, Buffer, Len);
finally
LocalFree(HLOCAL(Buffer));
end;
end;
procedure TDBAppItem.ParseURL(const lpszUrl: string; var Host,
Resource: string);
var
lpszScheme : array[0..INTERNET_MAX_SCHEME_LENGTH - 1] of Char;
lpszHostName : array[0..INTERNET_MAX_HOST_NAME_LENGTH - 1] of Char;
lpszUserName : array[0..INTERNET_MAX_USER_NAME_LENGTH - 1] of Char;
lpszPassword : array[0..INTERNET_MAX_PASSWORD_LENGTH - 1] of Char;
lpszUrlPath : array[0..INTERNET_MAX_PATH_LENGTH - 1] of Char;
lpszExtraInfo : array[0..1024 - 1] of Char;
lpUrlComponents : TURLComponents;
begin
ZeroMemory(#lpszScheme, SizeOf(lpszScheme));
ZeroMemory(#lpszHostName, SizeOf(lpszHostName));
ZeroMemory(#lpszUserName, SizeOf(lpszUserName));
ZeroMemory(#lpszPassword, SizeOf(lpszPassword));
ZeroMemory(#lpszUrlPath, SizeOf(lpszUrlPath));
ZeroMemory(#lpszExtraInfo, SizeOf(lpszExtraInfo));
ZeroMemory(#lpUrlComponents, SizeOf(TURLComponents));
lpUrlComponents.dwStructSize := SizeOf(TURLComponents);
lpUrlComponents.lpszScheme := lpszScheme;
lpUrlComponents.dwSchemeLength := SizeOf(lpszScheme);
lpUrlComponents.lpszHostName := lpszHostName;
lpUrlComponents.dwHostNameLength := SizeOf(lpszHostName);
lpUrlComponents.lpszUserName := lpszUserName;
lpUrlComponents.dwUserNameLength := SizeOf(lpszUserName);
lpUrlComponents.lpszPassword := lpszPassword;
lpUrlComponents.dwPasswordLength := SizeOf(lpszPassword);
lpUrlComponents.lpszUrlPath := lpszUrlPath;
lpUrlComponents.dwUrlPathLength := SizeOf(lpszUrlPath);
lpUrlComponents.lpszExtraInfo := lpszExtraInfo;
lpUrlComponents.dwExtraInfoLength := SizeOf(lpszExtraInfo);
InternetCrackUrl(PChar(lpszUrl), Length(lpszUrl), ICU_DECODE or ICU_ESCAPE, lpUrlComponents);
Host := lpszHostName;
Resource := lpszUrlPath;
end;
procedure TDBAppItem.InternalDownloadProgress(Sender: TObject; Read,
Total: Integer);
var
MsgRec : PProgressInfo;
begin
// Create progress changed message
New(MsgRec);
MsgRec^.ID := Fid;
MsgRec^.Read := Read;
MsgRec^.Total := Total;
MsgRec^.URL := FURL;
SendMessage(FHandle, WM_PROGRESSCHANGED, 0, LongInt(MsgRec));
end;
end.
My basic idea is creating pipeline with 2 stages:
Retrieve: downloading all files at the same time (threads count is constrained by NumTasks from OTL).
Install: As soon as any file be downloaded, it must be processed by this stage. Action in this stage must be one by one, i.e. only one action in same time (in real app I won't start many installers together).
I try to understand how OTL works here, but I have not many experience with this library yet.
So, dear community, how I must rewrite my code to:
Have parallel downloadings at Stage 1 (now it works one-by-one).
Have possibility to correctly stop Pipeline with GUI (now I call FPipeline.Cancel by TButton pressing and it cannot stop tasks immediately).
Sources also placed here.
Thanks in advance. I'll be glad meet any advice here.
1) Download works in parallel just fine - as far as OTL is concerned. On my machine the test app starts three parallel downloads each time I press F9. The other two downloads get stuck in the
hFile := InternetOpenURL(hInet, PChar(url), nil, 0, INTERNET_FLAG_PRAGMA_NOCACHE, 0);
call. IOW, all five downloader threads enter InternetOpenURL, but only three exit immediately and start downloading. I have no idea why (and it is related to WinINET, not OTL).
2) Cancellation doesn't work because noone tells the DownloadFile method to stop. IOmniPipeline.Cancel just calls CompleteAdding on each pipeline and tells each stage to stop processing input. It cannot stop the code which is already working on an input element (i.e. your DownloadFile method). You must do that yourself.
One way to do that is to create a global Cancel flag and change DownloadFile so that it checks whether this flag is set in the following loop:
repeat
InternetReadFile(hFile, #buffer, SizeOf(buffer), bytesRead);
BlockWrite(localFile, buffer, bytesRead);
Inc(FDownloaded, bytesRead);
//In real app this event occurs in TALWinHttpClient from Alcinoe library.
InternalDownloadProgress(Self, FDownloaded, FFileSize);
if FCancelled then break; // <-----------
until bytesRead = 0;
You could also change InternalDownloadProgress and add a var cancelled: boolean parameter which could be set in the event handler when the pipeline needs to shut down.
Related
Multithreaded File Preview (Lazarus + WinAPI)
Hello all, i am having problems getting the file preview (the one shown on the right side in the Windows Explorer window) for a certain file. So far fetching the file preview works fine, but it takes a long time (between 0.5 and 2 seconds). Thus i do not want it to be executed in the main thread (as this would interrupt the program gui). I tried to execute the file preview extraction in a worker thread, but this yields a SIGSEGV. The call stack is also not really useful, it only shows that the exception is raised in ShellObjHelper in Line 141 (see source code below). Source Code for main unit: type TThreadedImageInfo = record fileName: String; width: integer; height: integer; icon: TIcon; image: TImage; bmp: TBitmap; infoOut: String; memo: TMemo; end; PThreadedImageInfo = ^TThreadedImageInfo; procedure loadThumbnailImageFromFile(aData: Pointer); var XtractImage: IExtractImage; ColorDepth: integer; Flags: DWORD; RT: IRunnableTask; FileName: string; pThreadInfo: PThreadedImageInfo; begin pThreadInfo := PThreadedImageInfo(aData); if assigned(pThreadInfo) then begin FileName := pThreadInfo^.fileName; ColorDepth := 32; Flags := IEIFLAG_ASPECT or IEIFLAG_QUALITY or IEIFLAG_ORIGSIZE; // = 580 if FileExists(FileName) then begin if GetExtractImageItfPtr(FileName, XTractImage) then begin if ExtractImageGetFileThumbnail(XtractImage, pthreadinfo^.Image.Width, pthreadinfo^.Image.Height, ColorDepth, Flags, RT, pthreadinfo^.Bmp) then begin if (Flags and IEIFLAG_CACHE) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not cache the thumbnail.' + #13; if (Flags and IEIFLAG_GLEAM) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'The image has a gleam.' + #13; if (Flags and IEIFLAG_NOSTAMP) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an icon stamp on the thumbnail.' + #13; if (Flags and IEIFLAG_NOBORDER) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an a border around the thumbnail.' + #13; end else if GetFileLargeIcon(FileName, pThreadInfo^.icon) then begin pThreadInfo^.infoOut := 'Thumbnail is not available. Default icon displayed.'; end; end else begin pThreadInfo^.infoOut := 'Error loading IExtractImage.'; end; end else begin pThreadInfo^.infoOut := 'Error: File does not exist.'; end; end; end; procedure threadDone(Sender: TObject; aData: Pointer); var pThreadInfo: PThreadedImageInfo; begin pthreadInfo := PThreadedImageInfo(aData); if assigned(pThreadInfo) then begin if assigned(pthreadInfo^.Bmp) then begin pthreadinfo^.Image.Picture.Assign(pthreadInfo^.Bmp); end else if assigned(pthreadInfo^.icon) then begin pthreadinfo^.Image.Picture.Assign(pthreadInfo^.icon); end else begin pThreadInfo^.Image.Picture.Assign(nil); end; if assigned(pThreadInfo^.memo) then pThreadInfo^.memo.Lines.Text := pThreadInfo^.infoOut; if assigned(pthreadInfo^.icon) then pthreadInfo^.icon.free(); if assigned(pthreadInfo^.bmp) then pthreadInfo^.bmp.free(); end; dispose(pthreadinfo); end; procedure TForm1.Button1Click(Sender: TObject); var pThreadInfo: PThreadedImageInfo; begin new(pThreadInfo); pThreadInfo^.fileName := Edit1.Text; pThreadInfo^.image := Image1; pThreadInfo^.memo := Memo1; pThreadInfo^.icon := nil; pThreadInfo^.bmp := nil; pThreadInfo^.infoOut := ''; // use worker thread: //TThread.ExecuteInThread(#loadThumbnailImageFromFile, pThreadInfo, #threadDone); // use main thread: loadThumbnailImageFromFile(pThreadInfo); threadDone(nil, pThreadInfo); end; Source code for helper unit: unit ShellObjHelper; {$MODE objfpc}{$H+} {$IFDEF VER100}{$DEFINE DELPHI3}{$ENDIF} interface uses Windows, ShlObj, ActiveX, ShellAPI, Graphics, SysUtils, ComObj; type { from ShlObjIdl.h } IExtractImage = interface ['{BB2E617C-0920-11D1-9A0B-00C04FC2D6C1}'] function GetLocation(Buffer: PWideChar; BufferSize: DWORD; var Priority: DWORD; var Size: TSize; ColorDepth: DWORD; var Flags: DWORD): HResult; stdcall; function Extract(var BitmapHandle: HBITMAP): HResult; stdcall; end; IRunnableTask = interface ['{85788D00-6807-11D0-B810-00C04FD706EC}'] function Run: HResult; stdcall; function Kill(fWait: BOOL): HResult; stdcall; function Suspend: HResult; stdcall; function Resume: HResult; stdcall; function IsRunning: Longint; stdcall; end; const { from ShlObjIdl.h } ITSAT_MAX_PRIORITY = 2; ITSAT_MIN_PRIORITY = 1; ITSAT_DEFAULT_PRIORITY = 0; IEI_PRIORITY_MAX = ITSAT_MAX_PRIORITY; IEI_PRIORITY_MIN = ITSAT_MIN_PRIORITY; IEIT_PRIORITY_NORMAL = ITSAT_DEFAULT_PRIORITY; IEIFLAG_ASYNC = $001; // ask the extractor if it supports ASYNC extract (free threaded) IEIFLAG_CACHE = $002; // returned from the extractor if it does NOT cache the thumbnail IEIFLAG_ASPECT = $004; // passed to the extractor to beg it to render to the aspect ratio of the supplied rect IEIFLAG_OFFLINE = $008; // if the extractor shouldn't hit the net to get any content needs for the rendering IEIFLAG_GLEAM = $010; // does the image have a gleam? this will be returned if it does IEIFLAG_SCREEN = $020; // render as if for the screen (this is exlusive with IEIFLAG_ASPECT) IEIFLAG_ORIGSIZE = $040; // render to the approx size passed, but crop if neccessary IEIFLAG_NOSTAMP = $080; // returned from the extractor if it does NOT want an icon stamp on the thumbnail IEIFLAG_NOBORDER = $100; // returned from the extractor if it does NOT want an a border around the thumbnail IEIFLAG_QUALITY = $200; // passed to the Extract method to indicate that a slower, higher quality image is desired, // re-compute the thumbnail // IShellFolder methods helper procedure ShellFolderBindToObject(const ShellFolder: IShellFolder; PIDL: PItemIDList; const riid: TGUID; out pv); function ShellFolderGetUIObjectOf(const ShellFolder: IShellFolder; cidl: DWORD; var PIDL: PItemIDList; riid: TGUID; out pv): Boolean; procedure ShellFolderParseDisplayName(const ShellFolder: IShellFolder; const DisplayName: string; out PIDL: PItemIDList); function GetExtractImageItfPtr(const FileName: string; out XtractImage: IExtractImage): Boolean; function GetFileLargeIcon(const FileName: string; out LargeIcon: TIcon): Boolean; function ExtractImageGetFileThumbnail(const XtractImage: IExtractImage; ImgWidth, ImgHeight, ImgColorDepth: Integer; var Flags: DWORD; out RunnableTask: IRunnableTask; out Bmp: TBitmap): Boolean; procedure GetShellFolderItfPtr(const FolderName: string; Malloc: IMalloc; out TargetFolder: IShellFolder); implementation procedure ShellFolderBindToObject(const ShellFolder: IShellFolder; PIDL: PItemIDList; const riid: TGUID; out pv); begin OleCheck(ShellFolder.BindToObject(PIDL, nil, riid, {$IFDEF DELPHI3}Pointer(pv){$ELSE}pv{$ENDIF})); end; function ShellFolderGetUIObjectOf(const ShellFolder: IShellFolder; cidl: DWORD; var PIDL: PItemIDList; riid: TGUID; out pv): Boolean; begin Result := NOERROR = ShellFolder.GetUIObjectOf(0, cidl, PIDL, riid, nil, {$IFDEF DELPHI3}Pointer(pv){$ELSE}pv{$ENDIF}); end; procedure ShellFolderParseDisplayName(const ShellFolder: IShellFolder; const DisplayName: string; out PIDL: PItemIDList); var Attributes, Eaten: DWORD; begin OleCheck(ShellFolder.ParseDisplayName(0, nil, PWideChar(WideString(DisplayName)), Eaten, PIDL, Attributes)); end; function GetExtractImageItfPtr(const FileName: string; out XtractImage: IExtractImage): Boolean; var TargetFolder: IShellFolder; FilePath: string; ItemIDList: PItemIDList; Malloc: IMalloc; begin FilePath := ExcludeTrailingBackslash(ExtractFilePath(FileName)); OleCheck(SHGetMalloc(Malloc)); GetShellFolderItfPtr(FilePath, Malloc, TargetFolder); ShellFolderParseDisplayName(TargetFolder, ExtractFileName(FileName), ItemIDList); try Result := ShellFolderGetUIObjectOf(TargetFolder, 1, ItemIDList, IExtractImage, XtractImage); finally Malloc.Free(ItemIDList); end; end; function GetFileLargeIcon(const FileName: string; out LargeIcon: TIcon): Boolean; var SFI: TSHFileInfo; begin result := SHGetFileInfo(PChar(FileName), FILE_ATTRIBUTE_ARCHIVE, SFI, sizeof(SFI), SHGFI_ICON or SHGFI_LARGEICON) <> 0; if result then begin LargeIcon := TIcon.Create; LargeIcon.Handle := SFI.hIcon; end; end; function ExtractImageGetFileThumbnail(const XtractImage: IExtractImage; ImgWidth, ImgHeight, ImgColorDepth: Integer; var Flags: DWORD; out RunnableTask: IRunnableTask; out Bmp: TBitmap): Boolean; var Size: TSize; Buf: array[0..MAX_PATH] of WideChar; BmpHandle: HBITMAP; Priority: DWORD; GetLocationRes: HRESULT; procedure FreeAndNilBitmap; begin {$IFNDEF DELPHI3} FreeAndNil(Bmp); {$ELSE} Bmp.Free; Bmp := nil; {$ENDIF} end; begin Result := False; RunnableTask := nil; Size.cx := ImgWidth; Size.cy := ImgHeight; Priority := IEIT_PRIORITY_NORMAL; Flags := Flags or IEIFLAG_ASYNC; ////////////////////////// EXCEPTION HERE, but only when multithreading ///////////////////////////////////////////////////// GetLocationRes := XtractImage.GetLocation(Buf, sizeof(Buf), Priority, Size, ImgColorDepth, Flags); if (GetLocationRes = NOERROR) or (GetLocationRes = E_PENDING) then begin if GetLocationRes = E_PENDING then begin { if QI for IRunnableTask succeed, we can use RunnableTask interface pointer later to kill running extraction process. We could spawn a new thread here to extract image. } if S_OK <> XtractImage.QueryInterface(IRunnableTask, RunnableTask) then RunnableTask := nil; end; Bmp := TBitmap.Create; try // This could consume a long time. // If RunnableTask is available then calling Kill() method will immediately abort the process. OleCheck(XtractImage.Extract(BmpHandle)); Bmp.Handle := BmpHandle; Result := True; except on E: EOleSysError do begin //------------- OutputDebugString(PChar(string(E.ClassName) + ': ' + E.Message)); //------------- FreeAndNilBitmap; Result := False; end else begin FreeAndNilBitmap; raise; end; end; { try/except } end; end; procedure GetShellFolderItfPtr(const FolderName: string; Malloc: IMalloc; out TargetFolder: IShellFolder); var DesktopFolder: IShellFolder; ItemIDList: PItemIDList; begin OleCheck(SHGetDesktopFolder(DesktopFolder)); ShellFolderParseDisplayName(DesktopFolder, FolderName, ItemIDList); try ShellFolderBindToObject(DesktopFolder, ItemIDList, IShellFolder, TargetFolder); finally Malloc.Free(ItemIDList); end; end; end. The actual question(s): Why is the image extraction working without multithreading, but failing when using a worker thread? How can i make this work? I already started studying this post for another solution, but i am not yet sure how to do this. Useful Informations: Source for helper unit code: How to retrieve the file previews used by windows explorer in Windows vista and seven? Multithreading example: https://lazarus-ccr.sourceforge.io/docs/rtl/classes/tthread.executeinthread.html Activating PDF preview: open Adobe Acrobat Reader -> Edit -> Preferences -> General -> check "Enable PDF thumbnail previews" I am using Lazarus v2.0.10 r63526 on Windows 10 Pro 64 bit.
Thanks to the comment from #IInspectable, that's the hint i needed. Solution: Add CoInitialize before calling GetExtractImageItfPtr and add CoUninitialize after receiving the file preview, but still within the worker thread. Ensure that CoUninitialize is called even if exceptions occur by using try and finally`. Working source code for main unit with worker thread: type TThreadedImageInfo = record fileName: String; width: integer; height: integer; icon: TIcon; image: TImage; bmp: TBitmap; infoOut: String; memo: TMemo; end; PThreadedImageInfo = ^TThreadedImageInfo; procedure loadThumbnailImageFromFile(aData: Pointer); var XtractImage: IExtractImage; ColorDepth: integer; Flags: DWORD; RT: IRunnableTask; FileName: string; pThreadInfo: PThreadedImageInfo; begin pThreadInfo := PThreadedImageInfo(aData); if assigned(pThreadInfo) then begin FileName := pThreadInfo^.fileName; ColorDepth := 32; Flags := IEIFLAG_ASPECT or IEIFLAG_QUALITY or IEIFLAG_ORIGSIZE; // = 580 if FileExists(FileName) then begin CoInitialize(nil); try if GetExtractImageItfPtr(FileName, XTractImage) then begin if ExtractImageGetFileThumbnail(XtractImage, pthreadinfo^.Image.Width, pthreadinfo^.Image.Height, ColorDepth, Flags, RT, pthreadinfo^.Bmp) then begin if (Flags and IEIFLAG_CACHE) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not cache the thumbnail.' + #13; if (Flags and IEIFLAG_GLEAM) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'The image has a gleam.' + #13; if (Flags and IEIFLAG_NOSTAMP) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an icon stamp on the thumbnail.' + #13; if (Flags and IEIFLAG_NOBORDER) <> 0 then pThreadInfo^.infoOut := pThreadInfo^.infoOut + 'Extractor does not want an a border around the thumbnail.' + #13; end else if GetFileLargeIcon(FileName, pThreadInfo^.icon) then begin pThreadInfo^.infoOut := 'Thumbnail is not available. Default icon displayed.'; end; end else begin pThreadInfo^.infoOut := 'Error loading IExtractImage.'; end; finally CoUninitialize; end; end else begin pThreadInfo^.infoOut := 'Error: File does not exist.'; end; end; end; procedure threadDone(Sender: TObject; aData: Pointer); var pThreadInfo: PThreadedImageInfo; begin pthreadInfo := PThreadedImageInfo(aData); if assigned(pThreadInfo) then begin if assigned(pthreadInfo^.Bmp) then begin pthreadinfo^.Image.Picture.Assign(pthreadInfo^.Bmp); end else if assigned(pthreadInfo^.icon) then begin pthreadinfo^.Image.Picture.Assign(pthreadInfo^.icon); end else begin pThreadInfo^.Image.Picture.Assign(nil); end; if assigned(pThreadInfo^.memo) then pThreadInfo^.memo.Lines.Text := pThreadInfo^.infoOut; if assigned(pthreadInfo^.icon) then pthreadInfo^.icon.free(); if assigned(pthreadInfo^.bmp) then pthreadInfo^.bmp.free(); end; dispose(pthreadinfo); end; procedure TForm1.Button1Click(Sender: TObject); var pThreadInfo: PThreadedImageInfo; begin new(pThreadInfo); pThreadInfo^.fileName := Edit1.Text; pThreadInfo^.image := Image1; pThreadInfo^.memo := Memo1; pThreadInfo^.icon := nil; pThreadInfo^.bmp := nil; pThreadInfo^.infoOut := ''; TThread.ExecuteInThread(#loadThumbnailImageFromFile, pThreadInfo, #threadDone); end;
Error to receive file on socket inside a thread
I'm having trouble to receive a byte array containg a PNG file. When the code is executed in OnClientRead event it works fine, already when transfered for a thread, happens an error of MemoryStream that says: Out of memory while expanding memory stream. At this point: if SD.State = ReadingSize then I want to know how to solve this specific trouble and also how can I check if I'm receiving a data that contains a file or a simple String? The code: type TSock_Thread = class(TThread) private Socket: TCustomWinSocket; public constructor Create(aSocket: TCustomWinSocket); procedure Execute; override; end; type TInt32Bytes = record case Integer of 0: (Bytes: array[0..SizeOf(Int32)-1] of Byte); 1: (Value: Int32); end; TSocketState = (ReadingSize, ReadingStream); TSocketData = class public Stream: TMemoryStream; Png: TPngImage; State: TSocketState; Size: TInt32Bytes; Offset: Integer; constructor Create; destructor Destroy; override; end; { ... } constructor TSock_Thread.Create(aSocket: TCustomWinSocket); begin inherited Create(true); Socket := aSocket; FreeOnTerminate := true; end; procedure TSock_Thread.Execute; var s: String; BytesReceived: Integer; BufferPtr: PByte; SD: TSocketData; Item: TListItem; begin inherited; while Socket.Connected do begin if Socket.ReceiveLength > 0 then begin s := Socket.ReceiveText; { SD := TSocketData(Socket.Data); if SD.State = ReadingSize then begin while SD.Offset < SizeOf(Int32) do begin BytesReceived := Socket.ReceiveBuf(SD.Size.Bytes[SD.Offset], SizeOf(Int32) - SD.Offset); if BytesReceived <= 0 then Exit; Inc(SD.Offset, BytesReceived); end; SD.Size.Value := ntohl(SD.Size.Value); SD.State := ReadingStream; SD.Offset := 0; SD.Stream.Size := SD.Size.Value; end; if SD.State = ReadingStream then begin if SD.Offset < SD.Size.Value then begin BufferPtr := PByte(SD.Stream.Memory); Inc(BufferPtr, SD.Offset); repeat BytesReceived := Socket.ReceiveBuf(BufferPtr^, SD.Size.Value - SD.Offset); if BytesReceived <= 0 then Exit; Inc(BufferPtr, BytesReceived); Inc(SD.Offset, BytesReceived); until SD.Offset = SD.Size.Value; end; try SD.Stream.Position := 0; SD.Png.LoadFromStream(SD.Stream); SD.Stream.Clear; except SD.Png.Assign(nil); end; Item := Form1.ListView1.Selected; if (Item <> nil) and (Item.Data = Socket) then Form1.img1.Picture.Graphic := SD.Png; SD.State := ReadingSize; SD.Offset := 0; end; } end; Sleep(100); end; end; procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); var TST: TSock_Thread; begin TST := TSock_Thread.Create(Socket); TST.Resume; end; UPDATE: The code in the answer is not working for me because ServerType=stThreadBlocking blocks all clients connections with the server. And because of this, I'm searching for something like this (ServerType=stNonBlocking, TThread and OnAccept event): type TSock_Thread = class(TThread) private Png: TPngImage; Socket: TCustomWinSocket; public constructor Create(aSocket: TCustomWinSocket); procedure Execute; override; procedure PngReceived; end; // ... // =============================================================================== constructor TSock_Thread.Create(aSocket: TCustomWinSocket); begin inherited Create(true); Socket := aSocket; FreeOnTerminate := true; end; // =============================================================================== procedure TSock_Thread.PngReceived; var Item: TListItem; begin Item := Form1.ListView1.Selected; if (Item <> nil) and (Item.Data = Socket) then Form1.img1.Picture.Graphic := Png; end; procedure TSock_Thread.Execute; var Reciving: Boolean; DataSize: Integer; Data: TMemoryStream; s, sl: String; begin inherited; while Socket.Connected do begin if Socket.ReceiveLength > 0 then begin s := Socket.ReceiveText; if not Reciving then begin SetLength(sl, StrLen(PChar(s)) + 1); StrLCopy(#sl[1], PChar(s), Length(sl) - 1); DataSize := StrToInt(sl); Data := TMemoryStream.Create; Png := TPngImage.Create; Delete(s, 1, Length(sl)); Reciving := true; end; try Data.Write(s[1], Length(s)); if Data.Size = DataSize then begin Data.Position := 0; Png.LoadFromStream(Data); Synchronize(PngReceived); Data.Free; Reciving := false; end; except Png.Assign(nil); Png.Free; Data.Free; end; end; Sleep(100); end; end; procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); var TST: TSock_Thread; begin TST := TSock_Thread.Create(Socket); TST.Resume; end; This code has an error of conversion of data at this line: DataSize := StrToInt(sl); How can I fix this?
how solve this specific trouble You are not using TServerSocket threading the way it is meant to be used. If you want to use TServerSocket in stThreadBlocking mode (see my other answer for how to use TServerSocket in stNonBlocking mode), the correct way is to: derive a thread class from TServerClientThread override its virtual ClientExecute() method to do your I/O work (via TWinSocketStream) use the TServerSocket.OnGetThread event to instantiate the thread. If you don't do this, TServerSocket will create its own default threads (to fire the OnClient(Read|Write) events in the main thread), which will interfere with your manual threads. Also, you don't need the state machine that I showed you in my answer to your other question. That was for event-driven code. Threaded I/O code can be written linearly instead. Try something more like this: type TSock_Thread = class(TServerClientThread) private Png: TPngImage; procedure PngReceived; protected procedure ClientExecute; override; end; type TInt32Bytes = record case Integer of 0: (Bytes: array[0..SizeOf(Int32)-1] of Byte); 1: (Value: Int32); end; procedure TSock_Thread.ClientExecute; var SocketStrm: TWinSocketStream; Buffer: TMemoryStream; Size: TInt32Bytes; Offset: Integer; BytesReceived: Integer; BufferPtr: PByte; begin SocketStrm := TWinSocketStream.Create(ClientSocket, 5000); try Buffer := TMemoryStream.Create; try Png := TPngImage.Create; try while ClientSocket.Connected do begin if not SocketStrm.WaitForData(100) then Continue; Offset := 0; while Offset < SizeOf(Int32) do begin BytesReceived := SocketStrm.Read(Size.Bytes[Offset], SizeOf(Int32) - Offset); if BytesReceived <= 0 then Exit; Inc(Offset, BytesReceived); end; Size.Value := ntohl(Size.Value); Buffer.Size := Size.Value; BufferPtr := PByte(Buffer.Memory); Offset := 0; while Offset < Size.Value do begin BytesReceived := SocketStrm.Read(BufferPtr^, Size.Value - Offset); if BytesReceived <= 0 then Exit; Inc(BufferPtr, BytesReceived); Inc(Offset, BytesReceived); end; Buffer.Position := 0; try Png.LoadFromStream(Buffer); except Png.Assign(nil); end; Synchronize(PngReceived); end; finally Png.Free; end; finally Buffer.Free; end; finally SocketStrm.Free; end; end; procedure TSock_Thread.PngReceived; var Item: TListItem; begin Item := Form1.ListView1.Selected; if (Item <> nil) and (Item.Data = ClientSocket) then Form1.img1.Picture.Graphic := Png; end; procedure TForm1.ServerSocket1GetThread(Sender: TObject; ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread); begin SocketThread := TSock_Thread.Create(False, ClientSocket); end; how i can check if i'm receiving a data that contains a file or a simple String? The client needs to send that information to your server. You are already sending a value to specify the data size before sending the actual data. You should also preceed the data with a value to specify the data's type. Then you can handle the data according to its type as needed.
How to send records containing strings between applications
So, I have a class that uses WM_COPYDATA to allow applications to communicate. type TMyRec = record Name: string[255]; // I want just string Age: integer; Birthday: TDateTime; end; function TAppCommunication.SendRecord(const ARecordType: ShortString; const ARecordToSend: Pointer; ARecordSize: Integer): Boolean; var _Stream: TMemoryStream; begin _Stream := TMemoryStream.Create; try _Stream.WriteBuffer(ARecordType, 1 + Length(ARecordType)); _Stream.WriteBuffer(ARecordToSend^, ARecordSize); _Stream.Position := 0; Result := SendStreamData(_Stream, TCopyDataType.cdtRecord); finally FreeAndNil(_Stream); end; end; function TAppCommunication.SendStreamData(const AStream: TMemoryStream; const ADataType: TCopyDataType): Boolean; var _CopyDataStruct: TCopyDataStruct; begin Result := False; if AStream.Size = 0 then Exit; _CopyDataStruct.dwData := integer(ADataType); _CopyDataStruct.cbData := AStream.Size; _CopyDataStruct.lpData := AStream.Memory; Result := SendData(_CopyDataStruct); end; function TAppCommunication.SendData(const ADataToSend: TCopyDataStruct) : Boolean; var _SendResponse: integer; _ReceiverHandle: THandle; begin Result := False; _ReceiverHandle := GetRemoteReceiverHandle; if (_ReceiverHandle = 0) then Exit; _SendResponse := SendMessage(_ReceiverHandle, WM_COPYDATA, WPARAM(FLocalReceiverForm.Handle), LPARAM(#ADataToSend)); Result := _SendResponse <> 0; end; Sender application: procedure TSenderMainForm.BitBtn1Click(Sender: TObject); var _AppCommunication: TAppCommunication; _ms: TMemoryStream; _Rec: TMyRec; _Record: TAttrData; begin _AppCommunication := TAppCommunication.Create('LocalReceiverName', OnAppMessageReceived); _ms := TMemoryStream.Create; try _AppCommunication.SetRemoteReceiverName('LocalReceiverNameServer'); _AppCommunication.SendString('ąčęėįšųūž123'); _AppCommunication.SendInteger(998); _AppCommunication.SendDouble(0.95); _Rec.Name := 'Edijs'; _Rec.Age := 29; _Rec.Birthday := EncodeDate(1988, 10, 06); _Record.Len := 1988; _AppCommunication.SendRecord(TTypeInfo(System.TypeInfo(TMyRec)^).Name, #_Rec, SizeOf(_Rec)); finally FreeAndNil(_ms); FreeAndNil(_AppCommunication); end; end; Receiver app: procedure TReceiverMainForm.OnAppMessageReceived(const ASender : TPair<HWND, string>; const AReceivedData: TCopyDataStruct; var AResult: integer); var _MyRec: TMyRec; _RecType: ShortString; _RecData: Pointer; begin ... else begin if (AReceivedData.dwData) = Ord(TCopyDataType.cdtRecord) then begin _RecType := PShortString(AReceivedData.lpData)^; _RecData := PByte(AReceivedData.lpData)+1+Length(_RecType); if (_RecType = TTypeInfo(System.TypeInfo(TMyRec)^).Name) then begin _MyRec := TMyRec(_RecData^); ShowMessage(_MyRec.Name + ', Age: ' + IntToStr(_MyRec.Age) + ', birthday: ' + DateToStr(_MyRec.Birthday)); end; end; AResult := -1; end; end; The problem is that crash occur when I change Name: string[255]; to Name: string; in TMyRec. How do I overcome this? I do not want to edit all my records to change string to something else and I want to have one function to send all kind of records (as far as my idea goes none of them will contain objects). EDITED: Used answer provided by Remy and made some tweaks so I would by able to send any kind of record using only one SendRecord function: function TAppCommunication.SendRecord(const ARecordToSend, ARecordTypInfo: Pointer): Boolean; var _Stream: TMemoryStream; _RType: TRTTIType; _RFields: TArray<TRttiField>; i: Integer; begin _Stream := TMemoryStream.Create; try _RType := TRTTIContext.Create.GetType(ARecordTypInfo); _Stream.WriteString(_RType.ToString); _RFields := _RType.GetFields; for i := 0 to High(_RFields) do begin if _RFields[i].FieldType.TypeKind = TTypeKind.tkUString then _Stream.WriteString(_RFields[i].GetValue(ARecordToSend).ToString) else if _RFields[i].FieldType.TypeKind = TTypeKind.tkInteger then _Stream.WriteInteger(_RFields[i].GetValue(ARecordToSend).AsType<integer>) else if _RFields[i].FieldType.TypeKind = TTypeKind.tkFloat then _Stream.WriteDouble(_RFields[i].GetValue(ARecordToSend).AsType<Double>) end; _Stream.Position := 0; Result := SendStreamData(_Stream, TCopyDataType.cdtRecord); finally FreeAndNil(_Stream); end; end; Sender: _AppCommunication.SendRecord(#_Rec, System.TypeInfo(TMyRec));
A ShortString has a fixed size of 256 bytes max (1 byte length + up to 255 AnsiChars), so it is easy to embed in records and send as-is. A String, on the other hand, is a pointer to dynamically allocated memory for an array of Chars. So, it requires a little more work to serialize back and forth. To do what you are asking, you can't simply replace ShortString with String without also changing everything else in between to account for that difference. You already have the basic framework to send variable-length strings (send the length before sending the data), so you can expand on that to handle string values, eg: type TMyRec = record Name: string; Age: integer; Birthday: TDateTime; end; TStreamHelper = class helper for TStream public function ReadInteger: Integer; function ReadDouble: Double; function ReadString: String; ... procedure WriteInteger(Value: Integer); procedure WriteDouble(Strm: Value: Double); procedure WriteString(const Value: String); end; function TStreamHelper.ReadInteger: Integer; begin Self.ReadBuffer(Result, SizeOf(Integer)); end; function TStreamHelper.ReadDouble: Double; begin Self.ReadBuffer(Result, SizeOf(Double)); end; function TStreamHelper.ReadString: String; var _Bytes: TBytes; _Len: Integer; begin _Len := ReadInteger; SetLength(_Bytes, _Len); Self.ReadBuffer(PByte(_Bytes)^, _Len); Result := TEncoding.UTF8.GetString(_Bytes); end; ... procedure TStreamHelper.WriteInteger(Value: Integer); begin Self.WriteBuffer(Value, SizeOf(Value)); end; procedure TStreamHelper.WriteDouble(Value: Double); begin Self.WriteBuffer(Value, SizeOf(Value)); end; procedure TStreamHelper.WriteString(const Value: String); var _Bytes: TBytes; _Len: Integer; begin _Bytes := TEncoding.UTF8.GetBytes(Value); _Len := Length(_Bytes); WriteInteger(_Len); Self.WriteBuffer(PByte(_Bytes)^, _Len); end; function TAppCommunication.SendRecord(const ARecord: TMyRec): Boolean; var _Stream: TMemoryStream; begin _Stream := TMemoryStream.Create; try _Stream.WriteString('TMyRec'); _Stream.WriteString(ARecord.Name); _Stream.WriteInteger(ARecord.Age); _Stream.WriteDouble(ARecord.Birthday); _Stream.Position := 0; Result := SendStreamData(_Stream, TCopyDataType.cdtRecord); finally FreeAndNil(_Stream); end; end; // more overloads of SendRecord() // for other kinds of records as needed... procedure TSenderMainForm.BitBtn1Click(Sender: TObject); var ... _Rec: TMyRec; begin ... _Rec.Name := 'Edijs'; _Rec.Age := 29; _Rec.Birthday := EncodeDate(1988, 10, 06); _AppCommunication.SendRecord(_Rec); ... end; type TReadOnlyMemoryStream = class(TCustomMemoryStream) public constructor Create(APtr: Pointer; ASize: NativeInt); function Write(const Buffer; Count: Longint): Longint; override; end; constructor TReadOnlyMemoryStream.Create(APtr: Pointer; ASize: NativeInt); begin inherited Create; SetPointer(APtr, ASize); end; function TReadOnlyMemoryStream.Write(const Buffer; Count: Longint): Longint; begin Result := 0; end; procedure TReceiverMainForm.OnAppMessageReceived(const ASender : TPair<HWND, string>; const AReceivedData: TCopyDataStruct; var AResult: integer); var ... _Stream: TReadOnlyMemoryStream; _MyRec: TMyRec; _RecType: String; begin ... else begin if (AReceivedData.dwData = Ord(TCopyDataType.cdtRecord)) then begin _Stream := TReadOnlyMemoryStream(AReceivedData.lpData, AReceivedData.cbData); try _RecType := _Stream.ReadString; if (_RecType = 'TMyRec') then begin _MyRec.Name := _Stream.ReadString; _MyRec.Age := _Stream.ReadInteger; _MyRec.Birthday := _Stream.ReadDouble; ShowMessage(_MyRec.Name + ', Age: ' + IntToStr(_MyRec.Age) + ', birthday: ' + DateToStr(_MyRec.Birthday)); end; finally _Stream.Free; end; end; AResult := -1; end; end;
How to run code in already running thread to safely send/recv data [TidTCPServer]
UPDATE Problem Still Exists. Is it possible to run code in already running thread? for example: thread1 is running some code & i want to run code from thread2 in thread1. I want to run code in idTCPServer thread to send some data to client Edit: After research seems that my problem is that when Client data is received or is receiving same time another thread is trying to write to that socket. Edit: procedure TMainFrm.UserSRVExecute(AContext: TIdContext); var Command : String; msSize : Int64; ms : TMemoryStream; decompressedMS : TMemoryStream; H : TIdNotify; begin // Application.ProcessMessages; Command := AContext.Connection.Socket.ReadLn; // messagebox(0,'snd','',$40); if logb then mainfrm.mconnections.Lines.Add(command + ' - BEGIN'); if Command <> '' then // keepalive begin //Application.ProcessMessages; msSize := AContext.Connection.Socket.ReadInt64; ms := TMemoryStream.Create; decompressedMS := TMemoryStream.Create; try AContext.Connection.Socket.ReadStream(ms, msSize); ms.Position := 0; DecompressStream(MS,decompressedMS); decompressedMS.Position := 0; Client_ProcessData(AContext,Command,decompressedMS); finally ms.Free; decompressedMS.Free; if logb then mainfrm.mconnections.Lines.Add(command + ' - END'); end; end; end; procedure Client_ProcessData(AContext: TIdContext; cmd : String; data : TMemoryStream); var Hnd : THandle; clData : TStringArray; TmpStr1 : String; Tmp : String; TN : TIdNotify; Sync : TMySync; I,I2 : Integer; begin Hnd := AContext.Connection.Socket.Binding.Handle; if cmd = 'scr' then // RECEIVE COMMAND TO SEND TO CLIENT TO RECEIVE DATA FROM CLIENT begin Tmp := StreamToString(data); {Sync := TMySync2.Create(True); try Sync.cmd := cmd; Sync.hnd := Hnd; Sync.tmp := TmpStr1; Sync.Resume; finally //Sync.Free; end; } log('>>> CLFROMAS: '+IntToStr(HND)+':::'+cmd+':::'); // SendCMDToSocket(MainFrm.UserSRV,StrToInt(Trim(Tmp)),'scr'+IntToStr(Hnd)); I2 := StrToInt(Trim(Tmp)); for I := 0 to 100 do if USRVData[i].hnd = I2 then begin // cs.Acquire; USRVData[i].CTX.Connection.Socket.WriteLn('scr'+IntToStr(Hnd)); // PLACED ALL CONTEXTs IN GLOBAL VARIABLE + ALL SOCKET HANDLES. <--- HERE IS THE PROBLEM // cs.Release; Break; end; // log('>>> CLFROMAS: '+IntToStr(HND)+':::'+cmd+':::'+streamtostring(data)); Exit; end; if Copy(cmd,1,Length('scr4u')) = 'scr4u' then // RECEIVE DATA FROM CLIENT TO SEND IT TO ADMIN CLIENT REQUEST ABOVE begin if Length(cmd) > Length('scr4u') then begin Delete(cmd,1,Length('scr4u')); Data.Position := 0; { Sync := TMySync.Create; try Sync.cmd := cmd; Sync.hnd := Hnd; Sync.data := TMemoryStream.Create; Sync.data.CopyFrom(data,data.Size); Sync.data.Position := 0; Sync.DoNotify; finally Sync.data.Free; Sync.Free; end; } SendStreamToSocket(MainFrm.UserSRV,strtoint(cmd),'scr4u',Data); log('>>>>> ADMIN: '+IntToStr(HND)+':::'+cmd+':::'{+streamtostring(data)}); end else TmpStr1 := ''; Exit; end; ... UPDATE procedure TMainFrm.UserSRVExecute(AContext: TIdContext); var Command : String; msSize : Int64; ms : TMemoryStream; decompressedMS : TMemoryStream; H : TIdNotify; I : Integer; List, Messages : TStringList; begin Messages := nil; try List := TMyContext(AContext).OutgoingMessages.Lock; try if List.Count > 0 then begin Messages := TStringList.Create; Messages.Assign(List); List.Clear; end; finally TMyContext(AContext).OutgoingMessages.Unlock; end; if Messages <> nil then begin for I := 0 to Messages.Count-1 do begin AContext.Connection.IOHandler.WriteLn(Messages.Strings[I]); end; end; finally Messages.Free; end; if AContext.Connection.IOHandler.InputBufferIsEmpty then begin AContext.Connection.IOHandler.CheckForDataOnSource(100); AContext.Connection.IOHandler.CheckForDisconnect; if AContext.Connection.IOHandler.InputBufferIsEmpty then Exit; end; Command := AContext.Connection.Socket.ReadLn; if logb then mainfrm.mconnections.Lines.Add(command + ' - BEGIN'); if Command <> '' then begin msSize := AContext.Connection.Socket.ReadInt64; ms := TMemoryStream.Create; decompressedMS := TMemoryStream.Create; try AContext.Connection.Socket.ReadStream(ms, msSize); ms.Position := 0; DecompressStream(MS,decompressedMS); decompressedMS.Position := 0; Client_ProcessData(AContext,Command,decompressedMS); finally ms.Free; decompressedMS.Free; if logb then mainfrm.mconnections.Lines.Add(command + ' - END'); end; end; end;
Is it possible to run code in already running thread? for example: thread1 is running some code & i want to run code from thread2 in thread1. No. Thread1 needs to be explicitly coded to stop what it is currently doing, do something else, and then go back to what it was previous doing. All Thread2 can do is signal Thread1 to perform that stop+continue at its earliest convenience. I want to run code in idTCPServer thread to send some data to client Your TIdTCPServer.OnExecute event handler needs to check for that data periodically and send it when it is available. You can use the TIdContext.Data property, or derive a custom class from TIdServerContext and assign it to the TIdTCPServer.ContextClass property, to provide a per-client thread-safe buffer for your outbound data. Your OnExecute handler can then access that buffer when needed. For example: type TMyContext = class(TIdServerContext) public constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override; destructor Destroy; override; OutgoingMessages: TIdThreadSafeStringList; end; constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); begin inherited; OutgoingMessages := TIdThreadSafeStringList.Create; end; destructor TMyContext.Destroy; begin OutgoingMessages.Free; inherited; end; procedure TMyForm.FormCreate(Sender: TObject); begin // this must be set before activating the server... IdTCPServer1.ContextClass := TMyContext; end; procedure TMyForm.IdTCPServer1Execute(AContext: TIdContext); var List, Messages: TStringList; begin // check for outgoing data... Messages := nil; try List := TMyContext(AContext).OutgoingMessages.LockList; try if List.Count > 0 then begin Messages := TStringList.Create; Messages.Assign(List); List.Clear; end; finally TMyContext(AContext).OutgoingMessages.UnlockList; end; if Messages <> nil then begin // send Messages using AContext.Connection.IOHandler as needed... end; finally Messages.Free; end; // check for incoming data... if AContext.Connection.IOHandler.InputBufferIsEmpty then begin AContext.Connection.IOHandler.CheckForDataOnSource(100); AContext.Connection.IOHandler.CheckForDisconnect; if AContext.Connection.IOHandler.InputBufferIsEmpty then Exit; end; // process incoming data as needed... end; procedure TForm1.SomeProcedure; var List: TIdContextList; Context: TMyContext; begin List := IdTCPServer1.Contexts.LockList; try Context := TMyContext(List[SomeIndex]); Context.OutgoingMessages.Add('something'); finally IdTCPServer1.Contexts.UnlockList; end; end;
Delphi: Multithreading, Thread safe not working
When data is sending to "tunnel" socket, it's sometimes merged, implemented the Critical Section but it's not working.. What I'm doing wrong ? type my_ff_thread = class; my_ss_thread = class; Tmy_tunnel_from_MappedPortTCP = class; Tmy_thread_list = class ff_id : string; ff_connection : TIdTCPConnection; constructor Create(local_ff_id: string; local_ss_c: TIdTCPConnection); end; Tmy_tunnel_from_MappedPortTCP = class(TIdBaseComponent) protected procedure InitComponent; override; public function my_connect:boolean; end; my_ff_thread = class(TThread) protected procedure Execute; override; public constructor Create; end; my_ss_thread = class(TThread) protected Fff_id : string; Fff_cmd : string; Fff_data : TIdBytes; procedure Execute; override; public constructor Create(ff_id:string; ff_cmd:string; ff_data:TIdBytes); function prepare_cmd(cmd:string; id:string; data:string):string; function set_nulls_at_begin(s:string):string; end; var my_list : TThreadList; CS: TRTLCriticalSection; tunnel: TIdTCPConnection; Implementation constructor my_ff_thread.Create; begin inherited Create(True); end; constructor my_ss_thread.Create(ff_id:string; ff_cmd:string; ff_data:TIdBytes); begin inherited Create(True); Fff_id := ff_id; Fff_cmd := ff_cmd; Fff_data := ff_data; end; constructor Tmy_thread_list.Create(local_ff_id: string; local_ss_c: TIdTCPConnection); begin ff_id := local_ff_id; ff_connection := local_ss_c; end; function my_ss_thread.set_nulls_at_begin(s:string):string; var len, i : integer; res : string; begin if s='' then begin Result := ''; Exit; end; res := ''; len := Length(s); if len < 10 then for i:=1 to (10 - len) do begin res := res + '0'; end; Result := res + s; end; function my_ss_thread.prepare_cmd(cmd:string; id:string; data:string):string; var packet : string; begin packet := set_nulls_at_begin(IntToStr(Length(cmd))) + cmd; packet := packet + set_nulls_at_begin(IntToStr(Length(id))) + id; packet := packet + set_nulls_at_begin(IntToStr(Length(data))) + data; Result := packet; end; function del_ff_from_list(firefox_id:string):boolean; var i : integer; begin Result := True; try with my_list.LockList do begin for i:=0 to Count-1 do begin if Tmy_thread_list(Items[i]).ff_id = firefox_id then begin Delete(i); break; end; end; end; finally my_list.UnlockList; end; end; procedure my_ss_thread.Execute; var ss : TIdTCPClient; unix_time : integer; data : TIdBytes; packet : string; packet_stream: TStringStream; begin ss := TIdTCPClient.Create(nil); try with TIdTcpClient(ss) do begin Host := '127.0.0.1'; Port := 6666; ReadTimeout := 1000 * 5; Connect; end; except on E:Exception do begin ss.Disconnect; exit; end; end; try my_list.LockList.Add(Tmy_thread_list.Create(Fff_id, ss)); finally my_list.UnlockList; end; try ss.Socket.Write(Fff_data); except on E:Exception do begin {Fmy_memo.Lines.Add('First data not sent!');} end; end; unix_time := DateTimeToUnix(NOW); while True do begin ss.Socket.CheckForDataOnSource(5); if not ss.Socket.InputBufferIsEmpty then begin SetLength(data, 0); ss.Socket.InputBuffer.ExtractToBytes(data); packet := prepare_cmd('data_from_ss', Fff_id, TIdEncoderMIME.EncodeBytes(data)); packet_stream := TStringStream.Create(packet); packet_stream.Position := 0; ss.Socket.InputBuffer.Clear; unix_time := DateTimeToUnix(NOW); try EnterCriticalSection(CS); tunnel.Socket.Write(packet_stream, -1, True); LeaveCriticalSection(CS); except on E:Exception do begin end; end; end; if (DateTimeToUnix(NOW) - unix_time) > 120 then begin ss.Disconnect; break; end; if not ss.Connected then begin break; end; if not tunnel.Connected then begin ss.Disconnect; break; end; end; try if tunnel.Connected then begin EnterCriticalSection(CS); packet := prepare_cmd('disconnect', Fff_id, 'x'); packet_stream := TStringStream.Create(packet); packet_stream.Position := 0; tunnel.Socket.Write(packet_stream, -1, True); LeaveCriticalSection(CS); end; except on E:Exception do begin end; end; Terminate; end; procedure my_ff_thread.Execute; var t : my_ss_thread; cmd, id : string; i : integer; found_ss : TIdTCPConnection; list : TList; packet : string; cmd_len, id_len, data_len : integer; data : TIdBytes; orig_data : string; packet_stream: TStringStream; cmd_len_str, id_len_str, data_len_str : string; begin packet_stream := TStringStream.Create; while not Terminated do begin packet_stream.Position := 0; try tunnel.Socket.ReadStream(packet_stream); except on E:Exception do begin end; end; packet := packet_stream.DataString; if packet = '0000' then continue; try cmd_len_str := Copy(packet, 1, 10); cmd_len := StrToInt(cmd_len_str); except on E:Exception do begin end; end; Delete(packet, 1, 10); cmd := Copy(packet, 1, cmd_len); Delete(packet, 1, cmd_len); try id_len_str := Copy(packet, 1, 10); id_len := StrToInt(id_len_str); except on E:Exception do begin end; end; Delete(packet, 1, 10); id := Copy(packet, 1, id_len); Delete(packet, 1, id_len); SetLength(data, 0); try data_len_str := Copy(packet, 1, 10); data_len := StrToInt(data_len_str); except on E:Exception do begin end; end; Delete(packet, 1, 10); data := TIdDecoderMIME.DecodeBytes(Copy(packet, 1, data_len)); orig_data := Copy(packet, 1, data_len); Delete(packet, 1, data_len); found_ss := nil; try list := my_list.LockList; for i:=0 to list.Count-1 do begin if Tmy_thread_list(list[i]).ff_id = id then begin found_ss := Tmy_thread_list(list[i]).ff_connection; break; end; end; finally my_list.UnlockList; end; if cmd = 'disconnect' then begin if found_ss <> nil then if found_ss.Connected then begin found_ss.Disconnect; del_ff_from_list(id); continue; end; end; if found_ss = nil then begin t := my_ss_thread.Create(id, cmd, data); t.Start; end else begin if found_ss <> nil then try if found_ss.Connected then begin found_ss.Socket.Write(data); end; except on E:Exception do begin end; end; end; if not tunnel.Connected then begin Terminate; break; end; end; end; function Tmy_tunnel_from_MappedPortTCP.my_connect:boolean; var t : my_ff_thread; begin Result := True; try with TIdTcpClient(tunnel) do begin Host := '192.168.0.157'; Port := 8099; Connect; end; except on E:Exception do begin tunnel.Disconnect; exit; end; end; t := my_ff_thread.Create; t.Start; end; initialization InitializeCriticalSection(CS); my_list := TThreadList.Create; tunnel := TIdTCPClient.Create(nil); finalization DeleteCriticalSection(CS); end.
Try something like this: type my_ff_thread = class; my_ss_thread = class; Tmy_tunnel_from_MappedPortTCP = class; Tmy_thread_list = class public ff_id : string; ff_connection : TIdTCPConnection; constructor Create(const local_ff_id: string; local_ss_c: TIdTCPConnection); end; Tmy_tunnel_from_MappedPortTCP = class(TIdBaseComponent) protected procedure InitComponent; override; public function my_connect: boolean; function my_disconnect: boolean; end; my_ff_thread = class(TThread) protected procedure Execute; override; public constructor Create; end; my_ss_thread = class(TThread) protected Fff_id : string; Fff_cmd : string; Fff_data : TIdBytes; procedure Execute; override; public constructor Create(const ff_id, ff_cmd: string; const ff_data: TIdBytes); end; var my_list : TThreadList = nil; CS: TCriticalSection = nil; tunnel: TIdTCPClient = nil; tunnel_thread: my_ff_thread = nil; implementation constructor Tmy_thread_list.Create(const local_ff_id: string; local_ss_c: TIdTCPConnection); begin ff_id := local_ff_id; ff_connection := local_ss_c; end; constructor my_ss_thread.Create(const ff_id, ff_cmd: string; const ff_data: TIdBytes); begin inherited Create(False); Fff_id := ff_id; Fff_cmd := ff_cmd; Fff_data := Copy(ff_data, 0, Length(ff_data)); end; procedure my_ss_thread.Execute; var ss : TIdTCPClient; data : TIdBytes; packet : string; procedure WriteStrToStream(strm: TStream; const s: String); var buf: TIdBytes; len: Integer; begin buf := ToBytes(s, IndyUTF8Encoding); len := Length(buf); strm.WriteBuffer(len, SizeOf(Integer)); if bytes <> nil then strm.WriteBuffer(buf[0], len); end; procedure WritePacketToTunnel(const cmd: string; const bytes: TIdBytes = nil); var strm: TMemoryStream; begin strm := TMemoryStream.Create; try WriteStrToStream(strm, cmd); WriteStrToStream(strm, Fff_id); WriteStrToStream(strm, TIdEncoderMIME.EncodeBytes(bytes)); CS.Enter; try tunnel.IOHandler.Write(strm, 0, True); finally CS.Leave; end; finally strm.Free; end; end; begin ss := TIdTCPClient.Create(nil); try ss.Host := '127.0.0.1'; ss.Port := 6666; ss.ReadTimeout := 1000 * 120; ss.Connect; try my_list.Add(Tmy_thread_list.Create(Fff_id, ss)); try ss.IOHandler.Write(Fff_data); except {Fmy_memo.Lines.Add('First data not sent!');} raise; end; while not Terminated do begin SetLength(data, 0); ss.IOHandler.ReadBytes(data, -1); if Length(data) = 0 then break; WritePacketToTunnel('data_from_ss', data); end; WritePacketToTunnel('disconnect'); finally ss.Disconnect; end; finally ss.Free; end; end; constructor my_ff_thread.Create; begin inherited Create(False); end; procedure my_ff_thread.Execute; var cmd, id : string; data : TIdBytes; i : integer; found_ss : TIdTCPConnection; list : TList; function ReadStrFromStream(strm: TStream): string; var len: Integer; begin strm.ReadBuffer(len, SizeOf(Integer)); if len > 0 then Result := IdGlobal.ReadStringFromStream(strm, len, IndyUTF8Encoding) else Result := ''; end; procedure ReadPacketFromTunnel(var v_cmd, v_id: string; var v_data: TIdBytes); var strm: TMemoryStream; begin strm := TMemoryStream.Create; try tunnel.IOHandler.ReadStream(strm, -1, False); strm.Position := 0; v_cmd := ReadStrFromStream(strm); v_id := ReadStrFromStream(strm); v_data := TIdDecoderMIME.DecodeBytes(ReadStrFromStream(strm)); finally strm.Free; end; end; begin while not Terminated do begin ReadPacketFromTunnel(cmd, id, data); found_ss := nil; list := my_list.LockList; try for i := 0 to list.Count-1 do begin if Tmy_thread_list(list[i]).ff_id = id then begin found_ss := Tmy_thread_list(list[i]).ff_connection; break; end; end; finally my_list.UnlockList; end; if cmd = 'disconnect' then begin if found_ss <> nil then found_ss.Disconnect; del_ff_from_list(id); continue; end; if found_ss <> nil then begin try found_ss.IOHandler.Write(data); except end; Continue; end; my_ss_thread.Create(id, cmd, data); end; end; function Tmy_tunnel_from_MappedPortTCP.my_connect: boolean; begin Result := True; try tunnel.Host := '192.168.0.157'; tunnel.Port := 8099; tunnel.Connect; tunnel_thread := my_ff_thread.Create(tunnel); except tunnel.Disconnect; Result := False; end; end; function Tmy_tunnel_from_MappedPortTCP.my_disconnect: boolean; begin Result := True; try if tunnel_thread <> nil then tunnel_thread.Terminate; try tunnel.Disconnect; finally if tunnel_thread <> nil then begin tunnel_thread.WaitFor; FreeAnNil(tunnel_thread); end; end; except Result := False; end; end; initialization CS := TCriticalSection.Create; my_list := TThreadList.Create; tunnel := TIdTCPClient.Create(nil); finalization tunnel.Free; my_list.Free; CS.Free; end.