I was looking at this example for using TIdTCPServer/client components and I found that if there are any clients then the server component will hang when you change active to false. Specifically, it hangs on the call to the Windows "ExitThread" function call for the context thread.
To reproduce the behavior:
I want a simple TCP server to monitor a process over the LAN but I can't figure out how to prevent this lock up. I have found a lot of information that skirts around this but nothing has made sense to me yet. I'm using Delphi 10.2 on Win 8.1 with Indy 10.6.2.5366.
ExitThread()
can't hang, unless a DLL is misbehaving in its DllMain
/DllEntryPoint()
handler, causing a deadlock in the DLL loader. But, the server's Active
property setter can certainly hang, such as if any of the client threads are deadlocked.
The example you linked to is NOT a good example to follow. The threaded event handlers are doing things that are not thread-safe. They are accessing UI controls without syncing with the main UI thread, which can cause many problems including deadlocks and dead UI controls. And the server's broadcast method is implemented all wrong, making it prone to deadlocks, crashes, and data corruption.
Whoever wrote that example (not me) clearly didn't know what they were doing. It needs to be rewritten to take thread safety into account properly. Try something more like this instead:
unit UServer;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IdContext, IdComponent, Vcl.StdCtrls,
IdBaseComponent, IdCustomTCPServer, IdTCPServer, Vcl.ExtCtrls;
type
TFServer = class(TForm)
Title : TLabel;
btn_start : TButton;
btn_stop : TButton;
btn_clear : TButton;
clients_connected : TLabel;
IdTCPServer : TIdTCPServer;
Label1 : TLabel;
Panel1 : TPanel;
messagesLog : TMemo;
procedure FormShow(Sender: TObject);
procedure btn_startClick(Sender: TObject);
procedure btn_stopClick(Sender: TObject);
procedure btn_clearClick(Sender: TObject);
procedure IdTCPServerConnect(AContext: TIdContext);
procedure IdTCPServerDisconnect(AContext: TIdContext);
procedure IdTCPServerExecute(AContext: TIdContext);
procedure IdTCPServerStatus(ASender: TObject; const AStatus: TIdStatus;
const AStatusText: string);
private
{ Private declarations }
procedure broadcastMessage(p_message : string);
procedure Log(p_who, p_message: string);
procedure UpdateClientsConnected(ignoreOne: boolean);
public
{ Public declarations }
end;
// ...
var
FServer : TFServer;
implementation
uses
IdGlobal, IdYarn, IdThreadSafe;
{$R *.dfm}
// ... listening port
const
GUEST_CLIENT_PORT = 20010;
// *****************************************************************************
// CLASS : TMyContext
// HELPER CLASS FOR QUEUING OUTBOUND MESSAGES TO A CLIENT
// *****************************************************************************
type
TMyContext = class(TIdServerContext)
private
FQueue: TIdThreadSafeStringList;
FAnyInQueue: Boolean;
public
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
destructor Destroy; override;
procedure AddToQueue(p_message: string);
procedure CheckQueue;
end;
constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil);
begin
inherited Create(AConnection, AYarn, AList);
FQueue := TIdThreadSafeStringList.Create;
FAnyQueued := false;
end;
destructor TMyContext.Destroy;
begin
FQueue.Free;
inherited;
end;
procedure TMyContext.AddToQueue(p_message: string);
begin
with FQueue.Lock do
try
Add(p_message);
FAnyInQueue := true;
finally
FQueue.Unlock;
end;
end;
procedure TMyContext.CheckQueue;
var
queue, tmpList : TStringList;
i : integer;
begin
if not FAnyInQueue then Exit;
tmpList := TStringList.Create;
try
queue := FQueue.Lock;
try
tmpList.Assign(queue);
queue.Clear;
FAnyInQueue := false;
finally
FQueue.Unlock;
end;
for i := 0 to tmpList.Count - 1 do begin
Connection.IOHandler.WriteLn(tmpList[i]);
end;
finally
tmpList.Free;
end;
end;
// .............................................................................
// *****************************************************************************
// EVENT : onShow()
// ON FORM SHOW
// *****************************************************************************
procedure TFServer.FormShow(Sender: TObject);
begin
// ... INITIALIZE:
// ... clear message log
messagesLog.Lines.Clear;
// ... zero to clients connected
clients_connected.Caption := IntToStr(0);
// ... set buttons
btn_start.Visible := true;
btn_start.Enabled := true;
btn_stop.Visible := false;
// ... set context class
IdTCPServer.ContextClass := TMyContext;
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_startClick()
// CLICK ON START BUTTON
// *****************************************************************************
procedure TFServer.btn_startClick(Sender: TObject);
begin
btn_start.Enabled := false;
// ... START SERVER:
// ... clear the Bindings property ( ... Socket Handles )
IdTCPServer.Bindings.Clear;
// ... Bindings is a property of class: TIdSocketHandles;
// ... add listening ports:
// ... add a port for connections from guest clients.
IdTCPServer.Bindings.Add.Port := GUEST_CLIENT_PORT;
// ... etc..
// ... ok, Active the Server!
IdTCPServer.Active := true;
// ... hide start button
btn_start.Visible := false;
// ... show stop button
btn_stop.Visible := true;
btn_stop.Enabled := true;
// ... message log
Log('SERVER', 'STARTED!');
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_stopClick()
// CLICK ON STOP BUTTON
// *****************************************************************************
procedure TFServer.btn_stopClick(Sender: TObject);
begin
btn_stop.Enabled := false;
// ... before stopping the server ... send 'good bye' to all clients connected
broadcastMessage( 'Goodbye my Clients :)');
// ... stop server!
IdTCPServer.Active := false;
// ... hide stop button
btn_stop.Visible := false;
// ... show start button
btn_start.Visible := true;
btn_start.Enabled := true;
// ... message log
Log('SERVER', 'STOPPED!');
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_clearClick()
// CLICK ON CLEAR BUTTON
// *****************************************************************************
procedure TFServer.btn_clearClick(Sender: TObject);
begin
//... clear messages log
MessagesLog.Lines.Clear;
end;
// .............................................................................
// *****************************************************************************
// EVENT : onConnect()
// OCCURS ANY TIME A CLIENT IS CONNECTED
// *****************************************************************************
procedure TFServer.IdTCPServerConnect(AContext: TIdContext);
var
PeerIP : string;
PeerPort : TIdPort;
begin
// ... OnConnect is a TIdServerThreadEvent property that represents the event
// handler signalled when a new client connection is connected to the server.
// ... Use OnConnect to perform actions for the client after it is connected
// and prior to execution in the OnExecute event handler.
// ... see indy doc:
// http://www.indyproject.org/sockets/docs/index.en.aspx
// ... getting IP address and Port of Client that connected
PeerIP := AContext.Binding.PeerIP;
PeerPort := AContext.Binding.PeerPort;
// ... message log ...........................................................
Log('SERVER', 'Client Connected! Peer=' + PeerIP + ':' + IntToStr(PeerPort));
// ...
// ... update number of clients connected
UpdateClientsConnected(false);
// ...
// ... send the Welcome message to Client connected
AContext.Connection.IOHandler.WriteLn('Welcome GUEST Client :)');
end;
// .............................................................................
// *****************************************************************************
// EVENT : onDisconnect()
// OCCURS ANY TIME A CLIENT IS DISCONNECTED
// *****************************************************************************
procedure TFServer.IdTCPServerDisconnect(AContext: TIdContext);
var
PeerIP : string;
PeerPort : TIdPort;
begin
// ... getting IP address and Port of Client that connected
PeerIP := AContext.Binding.PeerIP;
PeerPort := AContext.Binding.PeerPort;
// ... message log ...........................................................
Log('SERVER', 'Client Disconnected! Peer=' + PeerIP + ':' + IntToStr(PeerPort));
// ...
// ... update number of clients connected
UpdateClientsConnected(true);
// ...
end;
// .............................................................................
// *****************************************************************************
// EVENT : onExecute()
// ON EXECUTE THREAD CLIENT
// *****************************************************************************
procedure TFServer.IdTCPServerExecute(AContext: TIdContext);
var
PeerIP : string;
PeerPort : TIdPort;
msgFromClient : string;
begin
// ... OnExecute is a TIdServerThreadEvents event handler used to execute
// the task for a client connection to the server.
// ... check for pending broadcast messages to the client
TMyContext(AContext).CheckQueue;
// ...
// check for inbound messages from client
if AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
AContext.Connection.IOHandler.CheckForDataOnSource(100);
AContext.Connection.IOHandler.CheckForDisconnect;
if AContext.Connection.IOHandler.InputBufferIsEmpty then Exit;
end;
// ... received a message from the client
// ... get message from client
msgFromClient := AContext.Connection.IOHandler.ReadLn;
// ... getting IP address, Port and PeerPort from Client that connected
PeerIP := AContext.Binding.PeerIP;
PeerPort := AContext.Binding.PeerPort;
// ... message log ...........................................................
Log('CLIENT', '(Peer=' + PeerIP + ':' + IntToStr(PeerPort) + ') ' + msgFromClient);
// ...
// ... process message (request) from Client
// ...
// ... send response to Client
AContext.Connection.IOHandler.WriteLn('... response from server :)');
end;
// .............................................................................
// *****************************************************************************
// EVENT : onStatus()
// ON STATUS CONNECTION
// *****************************************************************************
procedure TFServer.IdTCPServerStatus(ASender: TObject; const AStatus: TIdStatus;
const AStatusText: string);
begin
// ... OnStatus is a TIdStatusEvent property that represents the event handler
// triggered when the current connection state is changed...
// ... message log
Log('SERVER', AStatusText);
end;
// .............................................................................
// *****************************************************************************
// PROCEDURE : broadcastMessage()
// BROADCAST A MESSAGE TO ALL CLIENTS CONNECTED
// *****************************************************************************
procedure TFServer.broadcastMessage( p_message : string );
var
tmpList : TIdContextList;
contexClient : TIdContext;
i : integer;
begin
// ... send a message to all clients connected
// ... get context Locklist
tmpList := IdTCPServer.Contexts.LockList;
try
for i := 0 to tmpList.Count-1 do begin
// ... get context ( thread of i-client )
contexClient := tmpList[i];
// ... queue message to client
TMyContext(contexClient).AddToQueue(p_message);
end;
finally
// ... unlock list of clients!
IdTCPServer.Contexts.UnlockList;
end;
end;
// .............................................................................
// *****************************************************************************
// PROCEDURE : Log()
// LOG A MESSAGE TO THE UI
// *****************************************************************************
procedure TFServer.Log(p_who, p_message : string);
begin
TThread.Queue(nil,
procedure
begin
MessagesLog.Lines.Add('[' + p_who + '] - ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ': ' + p_message);
end
);
end;
// .............................................................................
// *****************************************************************************
// PROCEDURE : UpdateClientsConnected()
// DISPLAY THE NUMBER OF CLIENTS CONNECTED
// *****************************************************************************
procedure TFServer.UpdateClientsConnected(ignoreOne: Boolean);
var
NumClients: integer;
begin
with IdTCPServer.Contexts.LockList do
try
NumClients := Count;
finally
IdTCPServer.Contexts.UnlockList;
end;
if ignoreOne then Dec(NumClients);
TThread.Queue(nil,
procedure
begin
clients_connected.Caption := IntToStr(NumClients);
end
);
end;
// .............................................................................
end.
unit UClient;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdBaseComponent,
IdComponent, IdTCPConnection, IdTCPClient, IdThreadComponent;
type
TFClient = class(TForm)
Label1 : TLabel;
Label2 : TLabel;
messageToSend : TMemo;
messagesLog : TMemo;
btn_connect : TButton;
btn_disconnect: TButton;
btn_send : TButton;
// ... TIdTCPClient
IdTCPClient : TIdTCPClient;
// ... TIdThreadComponent
IdThreadComponent : TIdThreadComponent;
procedure FormShow(Sender: TObject);
procedure btn_connectClick(Sender: TObject);
procedure btn_disconnectClick(Sender: TObject);
procedure btn_sendClick(Sender: TObject);
procedure IdTCPClientConnected(Sender: TObject);
procedure IdTCPClientDisconnected(Sender: TObject);
procedure IdThreadComponentRun(Sender: TIdThreadComponent);
private
{ Private declarations }
procedure Log(p_who, p_message: string);
public
{ Public declarations }
end;
var
FClient : TFClient;
implementation
{$R *.dfm}
// ... listening port: GUEST CLIENT
const
GUEST_PORT = 20010;
// *****************************************************************************
// EVENT : onShow()
// ON SHOW FORM
// *****************************************************************************
procedure TFClient.FormShow(Sender: TObject);
begin
// ... INITAILIZE
// ... message to send
messageToSend.Clear;
messageToSend.Enabled := false;
// ... log
messagesLog.Clear;
// ... buttons
btn_connect.Enabled := true;
btn_disconnect.Enabled := false;
btn_send.Enabled := false;
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_connectClick()
// CLICK ON CONNECT BUTTON
// *****************************************************************************
procedure TFClient.btn_connectClick(Sender: TObject);
begin
btn_connect.Enabled := false;
// ... try to connect to Server
try
IdTCPClient.Connect;
except
on E: Exception do begin
Log('CLIENT', 'CONNECTION ERROR! ' + E.Message);
btn_connect.Enabled := true;
end;
end;
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_disconnectClick()
// CLICK ON DISCONNECT BUTTON
// *****************************************************************************
procedure TFClient.btn_disconnectClick(Sender: TObject);
begin
btn_disconnect.Enabled := false;
// ... disconnect from Server
IdTCPClient.Disconnect;
// ... set buttons
btn_connect.Enabled := true;
btn_send.Enabled := false;
// ... message to send
messageToSend.Enabled := false;
end;
// .............................................................................
// *****************************************************************************
// EVENT : onConnected()
// OCCURS WHEN CLIENT IS CONNECTED
// *****************************************************************************
procedure TFClient.IdTCPClientConnected(Sender: TObject);
begin
// ... messages log
Log('CLIENT', 'CONNECTED!');
// ... after connection is ok, run the Thread ... waiting messages
// from server
IdThreadComponent.Active := true;
// ... set buttons
btn_disconnect.Enabled := true;
btn_send.Enabled := true;
// ... enable message to send
messageToSend.Enabled := true;
end;
// .............................................................................
// *****************************************************************************
// EVENT : onDisconnected()
// OCCURS WHEN CLIENT IS DISCONNECTED
// *****************************************************************************
procedure TFClient.IdTCPClientDisconnected(Sender: TObject);
begin
// ... message log
Log('CLIENT', 'DISCONNECTED!');
end;
// .............................................................................
// *****************************************************************************
// EVENT : btn_sendClick()
// CLICK ON SEND BUTTON
// *****************************************************************************
procedure TFClient.btn_sendClick(Sender: TObject);
begin
// ... send message to Server
IdTCPClient.IOHandler.WriteLn(messageToSend.Text);
end;
// .............................................................................
// *****************************************************************************
// EVENT : onRun()
// OCCURS WHEN THE SERVER SEND A MESSAGE TO CLIENT
// *****************************************************************************
procedure TFClient.IdThreadComponentRun(Sender: TIdThreadComponent);
var
msgFromServer : string;
begin
// ... read message from server
msgFromServer := IdTCPClient.IOHandler.ReadLn();
// ... messages log
Log('SERVER', msgFromServer);
end;
// .............................................................................
// *****************************************************************************
// FUNCTION : Log()
// LOGS A MESSAGE TO THE UI
// *****************************************************************************
procedure TFClient.Log(p_who, p_message: string);
begin
TThread.Queue(nil,
procedure
begin
MessagesLog.Lines.Add('[' + p_who + '] - ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ': ' + p_message);
end
);
end;
// .............................................................................
end.