Search code examples
delphiindy10

Upgrading Delphi 7 Indy 9 app. to Indy 10


I have inherited an extensive (199 commands) Delphi 7 Indy 9 app that I am upgrading to Indy 10 (in D10.1). I have upgraded all the code, and it compiles and runs. The problem I have is that now in Indy 10 all the handlers also return a response code (and text) in addition to the coded response that they did under Indy 9.

For example:

// server 
procedure TFormMain.IdCmdTCPServer1loginCommand(ASender: TIdCommand);
var
  Rights: String;
begin

   if BillingUserRegistered(ASender.Params[0], ASender.Params[1], Rights) then
   begin
      myClient := TClientData.Create;
      myClient.ClientName := ASender.Params[0];
      myClient.ClientHost := #32; // indy9 was .Thread.Connection.LocalName; 
      myClient.ID := Now;
      ASender.Context.Data := myClient;

      ListBox1.Items.AddObject(
        PadR(myClient.ClientName,12,' ') + '=' +
        FormatDateTime('yyyy-mm-dd hh:nn:ss', myClient.ID),
        TDateTimeO.Create(myClient.ID));
      ASender.Context.Connection.IOHandler.WriteLn('SUCCESS' + ' ' + Rights)  
  end
  else
      ASender.Context.Connection.IOHander.WriteLn('Login failed!');

end;

...

// client side
function TfrmLogin.VerifyUserNameAndPassword(username, password: String): Boolean;
var
  response, response1: String;
begin

  frmMain.IdTCPClient1.IOHandler.WriteLn('login' + ' ' + 
     username + ' ' + password)
  response := frmMain.IdTCPClient1.IOHandler.ReadLn();
  // I have to add this now to capture the response code too!
  response1 := frmMain.IdTCPClient1.IOHandler.ReadLn(); // 200 OK
  // ------------------------------------------------------
  if Copy(response,1,7) = 'SUCCESS' then
  begin
    rights := Copy(response,9,4);

There are a lot of command handlers, and they all have their own custom responses. That's a lot of code to change at the client. Is there a way I can tell the IdCmdTCPServer to suppress the standard '200 Ok' response if the command handler already provides it's own? Or am I in for a long night?

Thanks


Solution

  • If you need to suppress the default command responses, you can either:

    1. clear the TIdCommandHandler's ReplyNormal and ExceptionReply properties (this also works in Indy 9, except that ExceptionReply was ReplyExceptionCode in that version), and the server's CommandHandlers.ExceptionReply property (Indy 10 only).

    2. set the TIdCommand.PerformReply property to false in your OnCommand handler (this also works in Indy 9):

      procedure TFormMain.IdCmdTCPServer1loginCommand(ASender: TIdCommand);
      var
        ...
      begin
        ASender.PerformReply := False;
        ...
      end;
      
    3. set the server's CommandHandlers.PerformReplies property to false (Indy 10 only - it will set TIdCommand.PerformReply to false by default):

      IdCmdTCPServer1.CommandHandlers.PerformReplies := False;
      

    On the other hand, you should consider using the command handler responses the way they are designed to be used, eg:

    procedure TFormMain.IdCmdTCPServer1loginCommand(ASender: TIdCommand);
    var
      Rights: String;
    begin
      if ASender.Params.Count = 2 then
      begin
        if BillingUserRegistered(ASender.Params[0], ASender.Params[1], Rights) then
        begin
          ...
          ASender.Reply.SetReply('SUCCESS', Rights);
        end
        else
          ASender.Reply.SetReply('ERROR', 'Login failed!');
      end
      else
        ASender.Reply.SetReply('ERROR', 'Wrong number of parameters!');
    end;
    

    I would even go as far as saying that you should set the TIdCommandHandler.NormalReply.Code property to SUCCESS and the TIdCommandHandler.ExceptionReply.Code property to ERROR, and then you can do this inside your OnCommand handler:

    procedure TFormMain.IdCmdTCPServer1loginCommand(ASender: TIdCommand);
    var
      Rights: String;
    begin
      if ASender.Params.Count <> 2 then
        raise Exception.Create('Wrong number of parameters!');
    
      if not BillingUserRegistered(ASender.Params[0], ASender.Params[1], Rights) then
        raise Exception.Create('Login failed!');
    
      ...
    
      ASender.Text.Text := Rights;
    end;
    

    With that said, any of these approaches should work fine without changing your existing client code. However, in Indy 10, I would suggest using SendCmd() instead of WriteLn()/ReadLn() directly:

    function TfrmLogin.VerifyUserNameAndPassword(username, password: String): Boolean;
    var
      response: String;
    begin
      response := frmMain.IdTCPClient1.SendCmd('login ' + username + ' ' + password);
      if response = 'SUCCESS' then
      begin
        rights := frmMain.IdTCPClient1.LastCmdResult.Text.Text;
        ...
      end else begin
        // error message in frmMain.IdTCPClient1.LastCmdResult.Text.Text ...
      end;
    end;
    

    Alternatively, you can let SendCmd() raise an exception if it does not receive a SUCCESS reply:

    function TfrmLogin.VerifyUserNameAndPassword(username, password: String): Boolean;
    begin
      try
        frmMain.IdTCPClient1.SendCmd('login ' + username + ' ' + password, 'SUCCESS');
      except
        on E: EIdReplyRFCError do begin
          // error message in E.Message ...
          ...
          Exit;
        end;
      end;
    
      rights := frmMain.IdTCPClient1.LastCmdResult.Text.Text;
      ...
    end;
    

    SendCmd() does exist in Indy 9, but it only supports numeric-based response codes, which you are not using. As you can see above, SendCmd() in Indy 10 supports string-based response codes as well as numeric ones.


    On a side note: in your server code, the OnCommand handler runs in a worker thread, so your use of ListBox1.Items.AddObject() is not thread-safe. Any access to the UI must be synchronized with the main UI thread, using techniques like TThread.Synchronize(), TThread.Queue(), TIdSync, TIdNotify, etc, eg:

    procedure TFormMain.IdCmdTCPServer1loginCommand(ASender: TIdCommand);
    var
      Rights: String;
      myClient: TClientData;
    begin
      if ASender.Params.Count = 2 then
      begin
        if BillingUserRegistered(ASender.Params[0], ASender.Params[1], Rights) then
        begin
          myClient := TClientData(ASender.Context.Data);
          if myClient = nil then
          begin
            myClient := TClientData.Create;
            ASender.Context.Data := myClient;
          end;
    
          myClient.ID := Now;
          myClient.ClientName := ASender.Params[0];
    
          myClient.ClientHost := GStack.HostByAddress(ASender.Context.Binding.PeerIP, ASender.Context.Binding.IPVersion);
          // In Indy 9, this would be:
          // myClient.ClientHost := GStack.WSGetHostByAddr(ASender.Thread.Connection.Socket.PeerIP);
          // NOT ASender.Thread.Connection.LocalName!
    
          TThread.Queue(nil,
            procedure
            begin
              ListBox1.Items.AddObject(
                PadR(myClient.ClientName,12,' ') + '=' + FormatDateTime('yyyy-mm-dd hh:nn:ss', myClient.ID),
                TDateTimeO.Create(myClient.ID));
            end
          );
    
          ASender.Reply.SetReply('SUCCESS', Rights);
        end
        else
          ASender.Reply.SetReply('ERROR', 'Login failed!');
      end
      else
        ASender.Reply.SetReply('ERROR', 'Wrong number of parameters!');
    end;
    

    Make sure your BillingUserRegistered() function is similarly thread-safe, if it is not already.