TIdTcpClient has shifted LastCmdResults - delphi

I started implementing a system using a client server connection with a TIdCmdTcpServer and a TIdTcpClient.
The connection is established fine and communication seems to work in general, too. But LastCmdResults contains always the response of the command issued before the last command. It starts with an empty response for the TcpClient.Connect and then continues with a "welcome" as a response to the first TcpClient.SendCmd ('LIST'). When I issue the LIST command again I get the desired result but for the one before (tested with a counter variable).
Relevant Code Snippets:
Initialising Command Handler
CmdHandler := TCPCmdServer.CommandHandlers.Add;
CmdHandler.Name := 'cmhList';
CmdHandler.Command := 'LIST';
CmdHandler.OnCommand := Cmd_ListDevices;
CmdHandler.ExceptionReply.NumericCode := 550;
CmdHandler.Disconnect := FALSE;
TCPCmdServer.Active := TRUE;
Command handler event Cmd_ListDevices
procedure TSPM_Server.Cmd_ListDevices (aSender : TIdCommand);
begin
aSender.Reply.SetReply (200, 'List');
aSender.Reply.Text.Add ('Device 1');
aSender.Reply.Text.Add ('Device 2');
aSender.Reply.Text.Add ('Device 3');
aSender.SendReply;
end;
Client Side
function TSPM_TCPClient.Connect (var aResponseText : string) : boolean;
begin
TcpClient.Connect;
aResponseText := TcpClient.LastCmdResult.Text.Text;
result := TcpClient.Connected;
end;
function TSPM_TCPClient.RequestList (var aList : string) : integer;
begin
aList := '';
result := TcpClient.SendCmd ('LIST');
if result = 200 then
begin
aList := 'CMD: ' + TcpClient.LastCmdResult.DisplayName + sLineBreak
+ TcpClient.LastCmdResult.Text.Text;
end;
end;
Anything obviously wrong here?

LastCmdResults contains always the response of the command issued before the last command
That happens when you have the server setup to send a greeting when a new client connects (see the TIdCmdTCPServer.Greeting property), but your client code is not reading that greeting. The greeting remains in the client's receive buffer until it is read. So, the 1st SendCmd() will read the greeting, then the 2nd SendCmd() will read the response of the 1st SendCmd(), and so on.
After TIdTCPClient.Connect() is successful, call TIdTCPClient.GetResponse() immediately to read the greeting, TIdTCPClient.Connect() will not read it for you, eg:
function TSPM_TCPClient.Connect (var aResponseText : string) : boolean;
begin
TcpClient.Connect;
try
TcpClient.GetResponse(200); // <-- add this!
aResponseText := TcpClient.LastCmdResult.Text.Text;
Result := True;
except
TcpClient.Disconnect;
Result := False;
end;
end;
Then you can call TIdTCPClient.SendCmd() afterwards as needed.

Related

How can I send a Push Notification through Parse.Com using Delphi?

I need to send a Push notification out through Parse.com's API using Delphi.
I see there is a TParseApi but the documentation is, as usual, rather sparse on the subject.
How can I do this?
Drop a TParseProvider and a TBackendPush component onto a form or datamodule. Connect them and enter your credentials in the appropriate properties of the provider. Set the backend Message property to the message to send and call Push.
There are at least three ways of doing this:
1) A direct method would be to create your own HTTP request with custom headers and JSON
Procedure TForm1.ParseDotComPushNotification(pushMessage: string);
var
parseDotComUrl: string;
JSON: TStringStream;
webRequest: TIDHttp;
response: string;
whereJson: TJSONObject;
alertJson: TJSONObject;
mainJsonObject: TJSONObject;
begin
parseDotComUrl := 'https://api.parse.com/1/push';
// Modify the JSON as required to push to whomever you want to.
// This one is set up to push to EVERYONE.
// JSON := TStringStream.Create('{ "where": {}, ' + '"data" : {"alert":"'
// + pushMessage + '"}' + '}', TEncoding.UTF8);
mainJsonObject := TJSONObject.Create;
whereJson := TJSONObject.Create;
mainJsonObject.AddPair(TJSONPair.Create('where', whereJson));
alertJson := TJSONObject.Create;
alertJson.AddPair(TJSONPair.Create('alert', pushMessage));
mainJsonObject.AddPair(TJSONPair.Create('data', alertJson));
JSON := TStringStream.Create(mainJsonObject.ToJSON);
mainJsonObject.Free; // free all the child objects.
webRequest := TIDHttp.Create(nil);
webRequest.Request.Connection := 'Keep-Alive';
webRequest.Request.CustomHeaders.Clear;
webRequest.Request.CustomHeaders.AddValue('X-Parse-Application-Id',
'YourApplicationID');
webRequest.Request.CustomHeaders.AddValue('X-Parse-REST-API-KEY',
'YourRestApiKey');
webRequest.Request.ContentType := 'application/json';
webRequest.Request.CharSet := 'utf-8';
webRequest.Request.ContentLength := JSON.Size;
try
try
response := webRequest.Post(parseDotComUrl, JSON);
except
on E: Exception do
begin
showmessage(response);
end;
end;
finally
webRequest.Free;
JSON.Free;
end;
end;
Thus bypassing the need for TParseApi
2) Based on UweRabbe's answer, you can also do it like this in code:
procedure TForm1.parseProviderCodeButtonClick(Sender: TObject);
var
myParseProvider: TParseProvider;
myBackendPush: TBackendPush;
myStrings: Tstrings;
whereJson: TJSONObject;
alertJson: TJSONObject;
mainJsonObject: TJSONObject;
begin
mainJsonObject := TJSONObject.Create;
whereJson := TJSONObject.Create;
mainJsonObject.AddPair(TJSONPair.Create('where', whereJson));
alertJson := TJSONObject.Create;
alertJson.AddPair(TJSONPair.Create('alert', pushMessage));
mainJsonObject.AddPair(TJSONPair.Create('data', alertJson));
myParseProvider := TParseProvider.Create(nil);
myParseProvider.ApiVersion := '1';
myParseProvider.ApplicationID := 'YourApplicationID';
myParseProvider.MasterKey := 'YourMasterKey';
myParseProvider.RestApiKey := 'YourRestApiKey';
myBackendPush := TBackendPush.Create(nil);
myBackendPush.Provider := myParseProvider;
// myBackendPush.Message := 'Hello world';
myStrings := TStringList.Create;
myStrings.Clear;
// I like putting the message in when I generate the JSON for the Target
// (since it seems I have to do it anyways, my not do it all in one place).
// You could however us TBackendPush.Message as I've commented out above.
// myStrings.Add('{ "where": { }, "data" : {"alert":"goodbye world"}}');
myStrings.Add(mainJsonObject.ToJSON);
myBackendPush.Target := myStrings;
myStrings.Free;
mainJsonObject.Free; // free all the child objects.
myBackendPush.Push;
myBackendPush.Free;
myParseProvider.Free;
end;
3) And to round this out into one complete answer (again based on UweRabbe's answer)
On your form/datamodule:
Place a TParseProvider
Place a TBackendPush - this should automatically set its Provider filed to the name of the TParseProvider you created in the previous step.
Set the TBackendPush's ApplicationID, MasterKey, RestApiKey, and Message properties
Set the TBackendPush's Push method from code.
e.g.,
procedure TForm1.Button1(Sender: TObject);
begin
BackendPush1.Push;
end;

What is real web-url for mORMot web-service?

Please help to understand about routing and web-url of web-service below.
type
TAirportService = class(TInterfacedObject, IAirportService)
public
procedure GetAirportDefinition(const AirPortID: integer; out Definition: TDTOAirportDefinition);
end;
procedure TAirportService.GetAirportDefinition(const AirPortID: integer;
out Definition: TDTOAirportDefinition);
begin
// create an object from static data
// (real application may use database and complex code to retrieve the values)
with Definition.Airport.Add do begin
Location := 'LAX';
Terminal := TRawUTF8DynArrayFrom(['terminalA', 'terminalB', 'terminalC']);
Gate := TRawUTF8DynArrayFrom(['gate1', 'gate2', 'gate3', 'gate4', 'gate5']);
BHS := 'Siemens';
DCS := 'Altiea';
end;
with Definition.Airline.Add do begin
CX := TRawUTF8DynArrayFrom(['B777', 'B737', 'A380', 'A320']);
QR := TRawUTF8DynArrayFrom(['A319', 'A380', 'B787']);
ET := '380';
SQ := 'A320';
end;
Definition.GroundHandler := TRawUTF8DynArrayFrom(['Swissport','SATS','Wings','TollData']);
end;
procedure StartWebService();
var
aModel: TSQLModel;
aDB: TSQLRestServer;
aServer: TSQLHttpServer;
begin
// set the logs level to only important events (reduce .log size)
TSQLLog.Family.Level := LOG_STACKTRACE+[sllInfo,sllServer];
// initialize the ORM data model
aModel := TSQLModel.Create([]);
try
// create a fast in-memory ORM server
aDB := TSQLRestServerFullMemory.Create(aModel,'test.json',false,false);
try
// register our TAirportServer implementation
// aDB.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculatorXML)],sicShared);
aDB.ServiceRegister(TAirportService,[TypeInfo(IAirportService)],sicShared);
// launch the HTTP server
aServer := TSQLHttpServer.Create('8092', [aDB], '+', useHttpApiRegisteringURI);
try
aServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
writeln('Background server is running'#10);
write('Press [Enter] to close the server.');
ConsoleWaitForEnterKey;
finally
aServer.Free;
end;
finally
aDB.Free;
end;
finally
aModel.Free;
end;
end;
I try to call follow web-urls:
http://localhost:8092/root/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService.GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService/GetAirportDefinition
http://localhost:8092/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/AirportService.GetAirportDefinition?AirPortID=1
http://localhost:8092/AirportService/GetAirportDefinition
but every time I get:
{
"errorCode":400,
"errorText":"Bad Request"
}
or Bad request
Where am I wrong?
A was wrong, really urls below works as needed:
http://localhost:8092/root/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService.GetAirportDefinition?AirPortID=1

How to properly start, working and finish a transaction?

I'm using MySQL, and I know that Nested Connection are not allowed - use "save points" for this - but I would like create a more generic code that could also be used with other DBMS.
So, I would like know how to properly start, working and finish a transaction in the code below?
Once ExampleDAO.Save() function could be used inside other function, like OtherExampleDAO.Save(), I need verify a transaction has been started before I try start a new one.
The lines with the verification if Assigned(dbTransaction) then always returns true, so how to properly verify if dbTransaction was instantiated?
function TExampleDAO.Save(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
Result := False;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
You need to properly initialize dbxTransaction to nil at the start of your function. Local variables in Delphi (on the Win32 platform, at least) are not initialized until a value is assigned to them, meaning that the content is unknown. Passing any value other than nil to Assigned will result in True. I recommend never testing a local variable's content on any platform until it has had a value assigned in your code.
Here's an example of how to make it work. (I've also removed the unnecessary assignment to Result in the exception block.)
function TExampleDAO.Salve(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
dbxTransaction := nil; // Initialize the transaction variable here
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
As was noted by #SirRufo in the comments to your question, failing to pass Example as a parameter should probably raise an exception as well, which would mean that it could become a procedure instead of a function and Result would no longer apply at all.

How to implement long running queries on IdhttpServer?

What is good way to implement long running queries on IdHttpServer. I have written simple logic to do so, please advise suggest better way to achive the same as I'm struggling with its performance.
I am using D2010 and Indy 10.5.8 to achieve the goal, also suggest if we retrive values frequently from session will that be a resource intensive ?
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
SessionObj : TSessionData;
begin
if ARequestInfo.Document = 'EXECUTEQUERY' then
begin
if not Assigned(ARequestInfo.Session.Content) then
begin
SessionObj := TSessionData.Create;
ARequestInfo.Session.Content.AddObject('U_SESSION', SessionObj);
SessionObj.RunLongQuery;
end;
end;
if ARequestInfo.Document = 'GETDATA' then
begin
SessionObj := TSessionData(ARequestInfo.Session.Content.Objects[ ARequestInfo.Session.Content.IndexOf('U_SESSION')]);
if SessionObj.GetQueryStat = Done then
begin
AResponseInfo.ContentStream.CopyFrom(SessionObj.GetMemStream, SessionObj.GetMemStream.Size);
SessionObj.GetMemStream.Clear;
AResponseInfo.ResponseNo := 200;
end else if SessionObj.GetQueryStat = Error then
AResponseInfo.ResponseNo := 500
else AResponseInfo.ResponseNo := 102;
end;
end;
procedure TForm1.ServerSessionEnd(Sender: TIdHTTPSession);
begin
TSessionData(Sender.Content.Objects[ Sender.Content.IndexOf('U_SESSION')]).Free;
end;
{ TProcessQuery }
constructor TProcessQuery.Create;
begin
myConn := TMyConnection.Create(nil);
myConn.LoginPrompt := False;
myConn.UserName := 'UserName';
myConn.Password := 'Password';
myConn.Server := 'Host';
myConn.Database := 'DBName';
myConn.Connected := True;
myQuery := TMyQuery.Create(nil);
myQuery.Unidirectional := True;
myQuery.Options.CreateConnection := False;
myQuery.Connection := myConn;
Fstat := None;
Fstream := TMemoryStream.Create;
end;
destructor TProcessQuery.Destroy;
begin
if Assigned(myConn) then begin
myConn.Close;
myConn.Disconnect;
FreeAndNil(myConn);
end;
end;
procedure TProcessQuery.ExecuteQuery;
begin
Status := Started;
myQuery.SQL.Text := '<Some Query>';
myQuery.Open;
try
try
while not myQuery.Eof do
begin
Status := Inprogress;
//Add to FStream which would be returned to user.
end;
except
on Exception do
Status := Error;
end;
finally
myQuery.Close;
end;
end;
{ TSessionData }
constructor TSessionData.Create;
begin
FProcessQuery := TProcessQuery.Create;
end;
function TSessionData.GetMemStream: TMemoryStream;
begin
result := FProcessQuery.Fstream;
end;
function TSessionData.GetQueryStat: TStatus;
begin
result := FProcessQuery.Status;
end;
procedure TSessionData.RunLongQuery;
begin
FProcessQuery.ExecuteQuery
end;
You are running the actual query in the context of ServerCommandGet(), so the client will not receive a reply until the query has finished. For what you are attempting, you need to move the query to its own thread and let ServerCommandGet() exit so the client gets a reply and can move on, thus freeing it to send subsequent GETDATA requests. In ServerSessionEnd(), you will have to terminate the query thread if it is still running, and free the TSessionData object.
There are some other problems with your code as well.
ServerCommandGet() is checking for not Assigned(ARequestInfo.Session.Content) and then calling ARequestInfo.Session.Content.AddObject() when ARequestInfo.Session.Contentis nil. I don't see any code that is creating the ARequestInfo.Session.Content object.
If the client issues multiple EXECUTEQUERY requests, you are storing them all in AResponseInfo.Session.Content using the same name, 'U_SESSION'. GETDATA will return the results of only the first query it finds, and ServerSessionEnd() only frees the first query it finds. So either give each query a unique name and send that back to the client so it can include it in GETDATA and make ServerSessionEnd() loop through the entire Sender.Content, or else do not allow multiple queries in the same HTTP session. If the client issues a new EXECUTEQUERY while a previous query is still active, kill the previous query before starting the new one.
When the client issues GETDATA, the code needs to take into account that the requested query may not exist, such as if it previously expired and was freed by ServerSessionEnd(). Also, if a query does exist and has finished, you are calling AResponseInfo.ContentStream.CopyFrom() but AResponseInfo.ContentStream is nil when ServerCommandGet() is called. You are responsible for providing your own ContentStream object. So either take ownership of TSessionData's memory stream and assign it as the AResponseInfo.ContentStream object, or else create a new TMemoryStream to copy into and then assign that as the AResponseInfo.ContentStream object. Either way, TIdHTTPServer will free the AResponseInfo.ContentStream after sending it to the client.

how to block unknown clients in indy (Delphi)

I have a public server(configured with indy 10) . some unknown clients are sending thousands of no content messages that it change the server's cpu usage to 50% . i have no firewall on my server , so i tried to block the unknown clients with this codes :
This is a function that works with a Timer :
var
i, j: integer;
begin
IX2 := IX2 + 1;
SetLength(ClientIPs, IX2);
ClientIPs[IX2 - 1] := StrIP;
j := 0;
for i := low(ClientIPs) to high(ClientIPs) do
begin
Application.ProcessMessages;
if ClientIPs[i] = StrIP then
j := j + 1;
end;
if j > 10 then
begin
Result := false;
exit;
end;
Result := true;
And it's my Timer code :
//Reset filtering measures
IX2 := 0;
SetLength(ClientIPs, 0);
So i use it in OnExecute event :
LogIP := AContext.Connection.Socket.Binding.PeerIP;
if IPFilter(LogIP) <> true then
begin
AContext.Connection.disconnect;
exit;
end;
//Get Data *********
Data := AContext.Connection.IOHandler.ReadLn();
finally , if a client sends many message in a short time , it will be disconnect . but there is a problem . in fact , after client disconnection , the Onexecute event is still working and i can not stop the operation Fully .anyway i need to block some IPs completely .
Thank you
The OnConnect event would be a better place to disconnect blacklisted IPs. The only reason to do the check in the OnExecute event is if the IP is not being blacklisted until after OnConnect has already been fired.
As for why OnExecute keeps running after you disconnect - the only way that can happen is if your OnExecute handler has a try..except block that is catching and discarding Indy's internal notifications. Any exception handling you do needs to re-raise EIdException-derived exceptions so the server can process them.
Followup to my earlier comment:
function TForm1.IPFilter(const StrIP: string): Boolean;
var
i, j: integer;
list: TList;
begin
j := 0;
list := IdTCPServer1.Contexts.LockList;
try
for i := 0 to list.Count-1 do
begin
if TIdContext(list[i]).Binding.PeerIP = StrIP then
Inc(j);
end;
Result := j <= 10;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
begin
// the simpliest way to force a disconnect and stop
// the calling thread is to raise an exception...
if not IPFilter(AContext.Binding.PeerIP) then
Abort();
// alternatively, if you call Disconnect(), make sure
// the IOHandler's InputBuffer is empty, or else
// AContext.Connection.Connected() will continue
// returning True!...
{if not IPFilter(AContext.Binding.PeerIP) then
begin
AContext.Connection.Disconnect;
AContext.Connection.IOHandler.InputBuffer.Clear;
Exit;
end;}
//Get Data *********
Data := AContext.Connection.IOHandler.ReadLn();
end;

Resources