I am using Indy TCPClient/TCPServer to verify registration of a mobile device. The process is fairly straight forward where I read an identifier on the Server Side, validate it against the database control file and send back a response to the client.
Everything appears to work correctly for the most part but periodically I get the EIDConnClosedGracefully Exception on the Server Side. I can't seem to pinpoint exactly where the connection is being closed improperly. In fact, it appears that the Server is executing a readln when it is not supposed to (after the Connection is closed) and I do not know why. Possibly I am not synchronizing properly. I have the Indy Silent Exceptions set to ignore in my Tools/Options/Debugger Options but I would like to know what is throwing the exception. I can execute the registration function 4 or 5 times and then the exception will be thrown but it is very inconsistent.
Any suggestions would be appreciated.
Following is my code:
Server
try
MIRec.RecType := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I')
or (MIRec.RecType = 'R') then
begin
// Verify the connecting device is registered
MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;
qryMobileDevice.Close;
qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.Open;
AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);
MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I') then
LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
else
begin
// Register the Device in STIKS
if qryMobileDevice.EOF then
begin
LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
// If Record Does not exist Add to the Control File;
NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');
qryMobileDevice.Insert;
qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
qryMobileDevice.Post;
end
else
begin
// Device has been Flagged and registration refused.
if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
else
LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
end;
qryMobileDevice.Close;
end;
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(LogEntry);
end);
end;
except
on e: exception do
begin
Memo1.Lines.Add('** An error occurred Receiving File ' + #13#10 + 'With a message: ' + E.Message);
end;
end;
Client
if MessageDlg('Register Device With Server?', TMsgDlgType.mtConfirmation, [TMsgDlgBtn.mbNo, TMsgDlgBtn.mbYes], 0) = mrNo then Exit;
try
IdTCPClient1.Connect;
try
MainForm.IdTCPClient1.IOHandler.WriteLn('R'); // Tell Server we are sending a Registration Record
Device := TUIDevice.Wrap(TUIDevice.OCClass.currentDevice);
IdTCPClient1.IOHandler.WriteLn(NSStrToStr(Device.identifierForVendor.UUIDString));
Registered := IdTCPClient1.IOHandler.ReadLn; // Get response from server
Authenticated := (Registered = 'T');
IdTCPClient1.IOHandler.WriteLn(NSStrToStr(Device.Name));
finally
IdTCPClient1.DisConnect;
if Registered <> 'T' then
MessageDlg('Registration Failed!', TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0)
else
begin
Authenticated := True;
MessageDlg('Registration Has Completed Successfully!', TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0);
end;
end;
except
on e: exception do
begin
MessageDlg('** An error occurred Registering Device ' + #13#10 + 'With a message: ' + E.Message, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], 0);
end;
end;
Output from my log.
A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:36:54 AM
A Client Disconnected
A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:37:00 AM
A Client Disconnected
A Client connected
** Registration Successful: ; 7FFC0274-AFB1-4E35-B8D9-F987B587804D; Wed. September 30/2015, 9:37:04 AM
** An error occurred Receiving File
With a message: Connection Closed Gracefully.
A Client Disconnected
Is the server code in the OnConnect
or OnExecute
event?
Assuming OnExecute
, that is a looped event, it loops continuously for the lifetime of the connection. On each iteration, you would be calling ReadLn
again to read the next command from the client. If the client disconnects, the next read that has to go back to the socket for more data (after the IOHandler.InputBuffer
has been exhausted) will raise an exception accordingly. This is normal behavior, and how Indy is designed to operate.
The real problem is that you have an exception handler that is unconditionally logging all exceptions as errors, even graceful disconnects. And your exception handler is not synchronizing with the UI thread when adding the error message to your memo, and it is not re-raising any caught Indy exception so TIdTCPServer
can handle it as needed (such as to stop the OnExecute
loop and trigger the OnDisconnect
event).
Try something more like this instead:
// if registration is only done once, this code should
// be in the OnConnect event instead...
procedure TMyForm.MyTCPServerExecute(AContext: TIdContext);
begin
try
MIRec.RecType := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I') or
(MIRec.RecType = 'R') then
begin
// Verify the connecting device is registered
MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;
qryMobileDevice.Close;
qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.Open;
AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);
MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I') then
LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
else
begin
// Register the Device in STIKS
if qryMobileDevice.EOF then
begin
LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
// If Record Does not exist Add to the Control File;
NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');
qryMobileDevice.Insert;
qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
qryMobileDevice.Post;
end
else
begin
// Device has been Flagged and registration refused.
if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
else
LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
end;
qryMobileDevice.Close;
end;
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(LogEntry);
end
);
end;
except
on E: Exception do
begin
if not (E is EIdSilentException) then
begin
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add('** An error occurred Receiving File ' + #13#10 + 'With a message: ' + E.Message);
end
);
end;
// optionally remove the below 'if' to close the
// connection on any exception, including DB errors, etc...
if E is EIdException then
raise;
end;
end;
end;
On the other hand, I would suggest getting rid of the try/except
altogether and use the server's OnException
event instead. Let any exception close the connection, and then just log why at the end:
procedure TMyForm.MyTCPServerExecute(AContext: TIdContext);
begin
MIRec.RecType := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I') or
(MIRec.RecType = 'R') then
begin
// Verify the connecting device is registered
MIRec.Identifier := AContext.Connection.IOHandler.ReadLn;
qryMobileDevice.Close;
qryMobileDevice.Parameters.ParamByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.Open;
AContext.Connection.IOHandler.WriteLn(qryMobileDevice.FindField('ACTIVE').AsString);
MIRec.DeviceName := AContext.Connection.IOHandler.ReadLn;
if (MIRec.RecType = 'I') then
LogEntry := 'A Connection Has Been Established With: ' + MIRec.DeviceName
else
begin
// Register the Device in STIKS
if qryMobileDevice.EOF then
begin
LogEntry := 'Registering: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
// If Record Does not exist Add to the Control File;
NextId := GetNextId('NEXT_MOBILE_DEVICE_ID');
qryMobileDevice.Insert;
qryMobileDevice.FieldByName('MOBILE_DEVICE_ID').Value := NextId;
qryMobileDevice.FieldByName('IDENTIFIER').Value := MIRec.Identifier;
qryMobileDevice.FieldByName('DESCRIPTION').Value := MiRec.Text;
qryMobileDevice.FieldByName('ACTIVE').Value := 'T';
qryMobileDevice.FieldByName('OPERATOR_SAVED').Value := 'From App';
qryMobileDevice.FieldByName('DATE_SAVED').Value := Now;
qryMobileDevice.Post;
end
else
begin
// Device has been Flagged and registration refused.
if qryMobileDevice.FindField('ACTIVE').AsString = 'T' then
LogEntry := '** Registration Successful: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now)
else
LogEntry := '** Registration Refused: ' + MiRec.Text + '; ' + MIRec.Identifier + '; ' + FormatDateTime('ddd. mmmm d/yyyy, h:mm:ss AM/PM', Now);
end;
qryMobileDevice.Close;
end;
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(LogEntry);
end
);
end;
end;
procedure TMyForm.MyTCPServerException(AContext: TIdContext; AException: Exception);
begin
if not (AException is EIdSilentException) then
begin
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add('** An error occurred' + sLineBreak + 'With a message: ' + AException.Message);
end
);
end;
end;
BTW, you have to be really careful when using TThread.Synchronize()
with TIdTCPServer
. If the main UI thread is busy deactivating the server when a server event handler calls Synchronize()
, a deadlock will occur between the UI thread and the synchronizing thread (the main thread is waiting for the server to finish deactivating, but the server is waiting for the thread to terminate, but the thread is waiting for the UI thread to finish deactivating the server). For simple logging as you have shown, I would suggest using TThread.Queue()
instead to avoid any deadlock potential. Or else deactivate the server in a worker thread so the UI thread is free to continue processing Synchronize()
requests.