delphi - disconnect IdHTTP in the middle of long-poll request - multithreading

I am implementing a live chat in my deplhi project, which receives new messages by doing GET request to the server. The server itself closes connection after 20 seconds if no new messages occur. Code discribed above is located in a separate thread (it is created on visiting chat "page") so it doesnt freezes the GUI. When i go to a non-chat page from chat, i call this code outside of thread_chat in order to make the thread exit:
if thread_chat <> nil then
begin
thread_chat.Terminate;
thread_chat := nil;
end;
However since my server timeout is 20 seconds, thread actualy closes only when it receives a response (yea, this sounds logically, since my thread loop is while not Terminated do). So what i am looking for is to close HTTP connection in the middle of the request.
Attemp #1
Initially i looked into TerminateThread by calling it like
TerminateThread(thread_chat.Handle, 0)
and this works fine until i try to kill thread on second time - my app completly freezes. So i went to
Attemp #2
I created a global variable URL_HTTP: TIdHTTP and i receive the server page content with this function:
function get_URL_Content(const Url: string): string;
var URL_stream: TStringStream;
begin
URL_HTTP := TIdHTTP.Create(nil);
URL_stream := TStringStream.Create(Result);
URL_HTTP.Get(Url, URL_stream);
if URL_HTTP <> nil then
try
URL_stream.Position := 0;
Result := URL_stream.ReadString(URL_stream.Size);
finally
FreeAndNil(URL_HTTP);
FreeAndNil(URL_stream);
end;
end;
and when i call this code outside of thread_chat
if thread_chat <> nil then
begin
URL_HTTP.Disconnect;
thread_chat.Terminate;
end;
i get EidClosedSocket exception (after some tests while writing this post, i get EAccessViolation error instead).
I run out of ideas. How can i close HTTP request before server response?
procedure thread_chat.Execute; //overrided
begin
while not Terminated do
if check_messages then //sends request to the server, processes response, true if new message(s) exist
show_messages;
end;

Try something like this:
type
TThreadChat = class(TThread)
private
HTTP: TIdHTTP;
function CheckMessages: Boolean;
procedure ShowMessages;
protected
procedure Execute; override;
public
constructor Create;
destructor Destroy; override;
procedure Stop;
end;
constructor TThreadChat.Create;
begin
inherited Create(False);
HTTP := TIdHTTP.Create(nil);
end;
destructor TThreadChat.Destroy;
begin
HTTP.Free;
inherited;
end;
function TThreadChat.CheckMessages: Boolean;
var
Resp: string;
begin
//...
Resp := HTTP.Get(Url);
//...
end;
procedure TThreadChat.ShowMessages;
begin
//...
end;
procedure TThreadChat.Execute;
begin
while not Terminated do
begin
if CheckMessages then
ShowMessages;
end;
end;
procedure TThreadChat.Stop;
begin
Terminate;
try
HTTP.Disconnect;
except
end;
end;
thread_chat := TThreadChat.Create;
...
if thread_chat <> nil then
begin
thread_chat.Stop;
thread_chat.WaitFor;
FreeAndNil(thread_chat);
end;

Related

Delphi CGI (on IIS) with TWebModule, how handle TThread?

In a CGI made with TWebModule i want, on request, perform a background long operation, but send an instantaneous response:
procedure TMyWebModule.MyAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
aMyThread : TMyThread;
begin
Handled := true;
aMyThread := TMyThread.Create;
Response.Content := 'request is processing...';
end;
TMyThread is
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
constructor Create;
end;
implementation
constructor TMyThread.Create;
begin
inherited Create(false);
Self.FreeOnTerminate := true;
end;
procedure TMyThread.Execute;
begin
WriteLog('START');
try
Sleep(20000);
finally
WriteLog('END');
end;
end;
The CGI seem kill the thread on server response, because i found START but not END.
IIS on response seem close the connection and send SIGTERM to the CGI process, that kills the threads.
How i can handle thread on CGI?

Delphi - Multithreading: Why I can't start thread again after thread.terminate()?

I've coding a multithread application that send and receive TCP packages. I'm with the problem that when I call twice event confirmBoxRecognized(peerIP: string) of the code bellow. I'm getting the following exception:
Cannot call Start on a running or suspended thread
If I check in the thread object I've that terminated == true and suspended == false. Why am I coding wrong?
Following the code:
TThreadReadTCP = class(TThread)
private
context: TfrmBoxTest;
looping: Boolean;
procedure readTCP;
protected
procedure DoTerminate; override;
procedure Execute; override;
public
peerIP: String;
responseObject: TProtocolObject;
constructor Create(CreateSuspended: Boolean; ctx: TFrmBoxTest); overload;
end;
{ TThreadReadTCP }
constructor TThreadReadTCP.Create(CreateSuspended: Boolean; ctx: TFrmBoxTest);
begin
inherited Create(CreateSuspended);
Self.context := ctx;
FreeOnTerminate := True;
end;
procedure TThreadReadTCP.DoTerminate;
begin
looping := false;
inherited DoTerminate();
end;
procedure TThreadReadTCP.Execute;
begin
inherited;
looping := true;
readTCP;
end;
procedure TThreadReadTCP.readTCP;
var
buffer: TBytes;
begin
while looping do
begin
if context.tcpClientBox.Connected then
begin
try
buffer := TEncoding.ASCII.GetBytes(context.tcpClientBox.Socket.ReadLn());
//do something else
except on E:Exception do
ShowMessage('Error receiving TCP buffer with message: ' + e.Message);
end;
end;
end;
end;
procedure TfrmBoxTest.confirmBoxRecognized(peerIP: string);
begin
if (connectBoxTCP(peerIP)) then
begin
if Assigned(threadReadTCP) then
begin
threadReadTCP.Terminate();
threadReadTCP.Start(); // I get the exception here when I run this code twice...
end;
showBoxRecognized();
end;
sendBoxRecognized();
end;
Are there running thread status can I get? Or anyone can explain how can I improve this code to solve this problem?
Thanks a lot!
You get the exception because you can only call Start() on a TThread object one time. Once the thread has been started, you cannot restart it. Once it has been signaled to terminate, all you can do is wait for it to finish terminating, and then destroy the object.
If you want another thread to start running, you have to create a new TThread object, eg:
type
TThreadReadTCP = class(TThread)
private
context: TfrmBoxTest;
protected
procedure Execute; override;
procedure TerminatedSet; override;
public
peerIP: String;
responseObject: TProtocolObject;
constructor Create(ctx: TFrmBoxTest); reintroduce;
end;
constructor TThreadReadTCP.Create(ctx: TFrmBoxTest);
begin
inherited Create(False);
Self.context := ctx;
// NEVER use FreeOnTerminate=True with a thread object that you keep a reference to!
// FreeOnTerminate := True;
end;
procedure TThreadReadTCP.Execute;
var
buffer: TBytes;
begin
while not Terminated do
begin
try
buffer := TEncoding.ASCII.GetBytes(context.tcpClientBox.Socket.ReadLn());
// do something else
except
on E: Exception do
begin
// do something
raise;
end;
end;
end;
end;
procedure TThreadReadTCP.TerminatedSet;
begin
try
context.tcpClientBox.Disconnect(False);
except
end;
end;
...
procedure TfrmBoxTest.confirmBoxRecognized(peerIP: string);
begin
if Assigned(threadReadTCP) then
begin
threadReadTCP.Terminate();
threadReadTCP.WaitFor();
FreeAndNil(threadReadTCP);
end;
if connectBoxTCP(peerIP) then
begin
threadReadTCP := TThreadReadTCP.Create(Self);
showBoxRecognized();
end;
sendBoxRecognized();
end;

Multithreading and MessageDlgPos

Hi I'm doing a code MessageDlgPos running five threads at the same time, the code is this:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
text: string;
property ReturnValue;
end;
procedure TMyThread.Execute;
begin
if Terminated then
Exit;
MessageDlgPos(text, mtInformation, [mbOk], 0, 100, 200);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
LThread: TMyThread;
i: Integer;
begin
For i := 1 to 5 do
begin
LThread := TMyThread(Sender);
try
LThread.text := 'hi';
LThread.FreeOnTerminate := True;
except
LThread.Free;
raise;
end;
LThread.Resume;
end;
end;
The problem is that Delphi XE always returns the following error and does not execute anything:
First chance exception at $ 7524B727. Exception class EAccessViolation with message 'Access violation at address 00D0B9AB. Write of address 8CC38309 '. Process tester.exe (6300)
How do I fix this problem?
As David Heffernan pointed out, MessageDlgPos() cannot safely be called outside of the main UI thread, and you are not managing the thread correctly. Your code needs to look more like this instead:
type
TMyThread = class(TThread)
protected
procedure Execute; override;
public
text: string;
property ReturnValue;
end;
procedure TMyThread.Execute;
begin
// no need to check Terminated here, TThread already
// does that before calling Execute()...
TThread.Synchronize(nil,
procedure
begin
MessageDlgPos(text, mtInformation, [mbOk], 0, 100, 200);
end
);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
LThread: TMyThread;
i: Integer;
begin
For i := 1 to 5 do
begin
LThread := TMyThread.Create(True);
LThread.text := 'hi';
LThread.FreeOnTerminate := True;
LThread.Start;
end;
end;
I would suggest a slightly different variation:
type
TMyThread = class(TThread)
private
fText: string;
protected
procedure Execute; override;
public
constructor Create(const aText: string); reintroduce;
property ReturnValue;
end;
constructor TMyThread.Create(const aText: string);
begin
inherited Create(False);
FreeOnTerminate := True;
fText := aText;
end;
procedure TMyThread.Execute;
begin
// no need to check Terminated here, TThread already
// does that before calling Execute()...
TThread.Synchronize(nil,
procedure
begin
MessageDlgPos(fText, mtInformation, [mbOk], 0, 100, 200);
end
);
end;
procedure TForm1.btnTestClick(Sender: TObject);
var
i: Integer;
begin
For i := 1 to 5 do
begin
TMyThread.Create('hi');
end;
end;
But either way, if you don't like using TThread.Synchronize() to delegate to the main thread (thus only displaying 1 dialog at a time) then you cannot use MessageDlgPos() at all, since it is only safe to call in the main UI thread. You can use Windows.MessageBox() instead, which can be safely called in a worker thread without delegation (but then you lose the ability to specify its screen position, unless you access its HWND directly by using a thread-local hook via SetWindowsHookEx() to intercept the dialog's creation and discover its HWND):
procedure TMyThread.Execute;
begin
Windows.MessageBox(0, PChar(fText), PChar(Application.Title), MB_OK or MB_ICONINFORMATION);
);
end;
There are many problems. The biggest one is here:
LThread := TMyThread(Sender);
Sender is a button. Casting to a thread is simply wrong and the cause of your exception. Casting a button to a thread doesn't make it so. It's still a button.
You likely mean to create a thread instead.
LThread := TMyThread.Create(True);
You cannot show VCL UI outside the main thread. The call to MessageDlgPos breaks that rule. If you do need to show UI at that point, you'll need to use TThread.Synchronize to have the code execute in the main thread.
Your exception handler makes no sense to me. I think you should remove it.
Resume is deprecated. Use Start instead.

Error on Close Form when open Query in Thread (Delphi)

I have a Query and open it in my Thread. It works correctly and I don't want to use Synchronize, because Synchronize makes main Form don't response while the Query not complete fetch.
When close the Form blow error shown:
System Error. Code: 1400. Invalid window handle
type
TMyThread = class(TThread)
public
procedure Execute; override;
procedure doProc;
end; { type }
.
.
.
procedure TMyThread.doProc;
begin
Form1.Query1.Open;
end;
procedure TMyThread.Execute;
begin
inherited;
doProc;
end;
.
.
.
procedure TForm1.Button1Click(Sender: TObject);
begin
thrd := TMyThread.Create(True);
thrd.FreeOnTerminate := True;
thrd.Resume;
end;
Note : Query has a lot of record.
The problem is that the VCL is not thread safe.
In order to have the query execute in parallel to all other things going on you'll have to decouple it from the Form.
That means you'll have to create the Query at runtime using code:
type
TMyThread = class(TThread)
private
FQuery: TQuery;
FOnTerminate: TNotifyEvent;
public
constructor Create(AQuery: TQuery);
destructor Destroy; override;
procedure Execute; override;
procedure doProc;
//Add an event handler to do cleanup on termination.
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end; { type }
constructor TMyThread.Create(AQuery: TQuery);
begin
inherited Create(True);
FQuery:= AQuery;
end;
procedure TMyThread.doProc;
begin
FQuery1.Open;
Synchronize(
//anonymous method, use a separate procedure in older Delphi versions
procedure
begin
Form1.Button1.Enabled:= true; //reenable the button when we're done.
end
);
end;
procedure TMyThread.Execute;
begin
inherited;
doProc;
end;
destructor TMyThread.Destroy;
begin
if Assigned(FOnterminate) then FOnTerminate(Self);
inherited;
end;
In the OnClick for Button1 you'll do the following:
type
TForm1 = class(TForm)
private
AQuery: TQuery;
...
end; {type}
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled:= false; //disable the button so it cannot be started twice.
thrd.Free;
AQuery:= TQuery.Create;
AQuery.SQL.Text:= .....
thrd := TMyThread.Create(AQuery);
thrd.OnTerminate:= MyTerminationHandler;
thrd.FreeOnTerminate:= False;
thrd.Resume;
end;
Finally assign cleanup code to the termination handler of the thread.
If you destroy the Query in the thread then you cannot use FreeOnTerminate:= true, but you'll have to Free the thread yourself.
procedure TForm1.MyTerminationHandler(Sender: TObject);
begin
FreeAndNil(AQuery);
end;
Warning
This code will only work if you start 1 thread.
If you want start this thread multiple times (i.e. run multiple queries at the same time), you'll have to create an array of threads e.g.:
TQueryThreads = record
MyThread: TMyThread;
MyQuery: TQuery;
constructor Create(SQL: string);
end; {record}
TForm1 = class(TForm)
private
Threads: array of TQueryThreads;
....
end; {TForm1}
Note that this code will not work in the BDE, because that library does not support multiple running queries at the same time
If you want to do that you'll have to use ZEOS or something like that.
As per TLama's suggestion:
I would suggest switching the BDE TQuery component to ADO, or downloading something like ZEOS components. The BDE is very outdated and has a lot of quirks that will never get fixed because it is no longer maintained.
The only issue that remains is cleaning up the connection if Form1 is closed.
If it's your main form it really does not matter because your whole application will go down.
If it's not your main form than you'll need to disable closing the form by filling the OnCanClose handler.
TForm1.CanClose(Sender: TObject; var CanClose: boolean);
begin
CanClose:= thrd.Finished;
end;
You should prevent any action (user and program) in the MainThread without blocking it. This can easily be done by a modal form, that cannot be closed by the user.
The thread can do anything as long as it takes and the final (synchronized) step is to close that modal form.
procedure OpenDataSetInBackground( ADataSet : TDataSet );
var
LWaitForm : TForm;
begin
LWaitForm := TForm.Create( nil );
try
LWaitForm.BorderIcons := []; // no close buttons
TThread.CreateAnonymousThread(
procedure
begin
try
ADataSet.Open;
finally
TThread.Synchronize( nil,
procedure
begin
LWaitForm.Close;
end );
end;
end );
try
LWaitForm.ShowModal;
finally
LWorkThread.Free;
end;
finally
LWaitForm.Free;
end;
end;
But you have to be careful with this and you should never try do start more than one parallel thread with this code unless you really know, what you are doing.

How do I protect my Indy socket writes with a critical section?

cs.Acquire;
try
AContext.Connection.Socket.Write(packet);
finally
cs.Release;
end;
or
EnterCriticalSection(cs);
AContext.Connection.Socket.Write(packet);
LeaveCriticalSection(cs);
I trying to send my packet to server in thread, but I have 20 threads which is also sending data to same connection socket. I'm try use Critical Section or Mutex, and they both don't work, I receive the garbage when all threads are sending.
it's all about my previous question
Packet looks like this:
LengthData
0000000010HelloWorld
Server receive data:
ReadBytes(10);
len := (Then remove zeros from begining);
ReadBytes(len); // data.
Sometimes I receive garbage in ReadBytes(10), it's a mix of Length+Data something like: "10Hellowor"
If I send data to server using only one thread, all works fine, every time.
If many threads is sending, all goes wrong.
CS/mutex locks work just fine when used properly. Make sure that your threads are locking the same CS/mutex instance, not separate instances. Since you are sending the data from the server side, I would suggest using the OnConnect event to create a per-connection CS and store it in the TIdContext.Data property, and the OnDisconnect event to free it, eg:
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Data := TCriticalSection.Create;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
begin
AContext.Data.Free;
AContext.Data := nil;
end;
Then you can do this when needed:
TCriticalSection(AContext.Data).Acquire;
try
AContext.Connection.Socket.Write(packet);
finally
TCriticalSection(AContext.Data).Release;
end;
A slightly more encapsulated usage would be to derive a new class from TIdServerContext instead, eg:
type
TMyContext = class(TIdServerContext)
private
CS: TCriticalSection;
public
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil); override;
destructor Destroy; override;
procedure SendPacket(const AData: TIdBytes); // or whatever parameters you need
end;
constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil);
begin
inherited;
CS := TCriticalSection.Create;
end;
destructor TMyContext.Destroy;
begin
CS.Free;
inherited;
end;
procedure TMyContext.SendPacket(const AData: TIdBytes);
begin
CS.Acquire;
try
Connection.IOHandler.Write(AData);
finally
CS.Release;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TMyContext;
end;
Then you can do this when needed:
TMyContext(AContext).SendPacket(packet);

Resources