Creating a server system

Hi all,

Please excuse my lack of knowledge if I ask these questions wrongly as I’m not that sure what I’m asking

Using Delphi 12

So generally the outcome I’m seeking is,

I wish to create a server system that can reside on a server PC box (running as win service) and have a separate server admin application that can log into the server for administration purposes.

The overall service system interface connection components must have connection pooling as there will be many user connecting to it from web sites plus there will be many

  1. PLC’s
  2. IP Cameras

uploading/downloading data from the server system.

In the first instance if I can create a server system that can accommodate a minimum of 100 connections (but it needs to be easily scalable upwards) then that would be a grand start.

The server system must also include a FireDAC connection to a firebird database for saving data and sending data back based upon user requests( ie stored procedures and the like)

Data to and from the PLC to the firebird database I would like to be via TCP/IP packets (still not sure how to do this but in time we will)

I also need the server system to provide DNS IP address’s to the PLC’s and IP Cameras

The intended server system will have a fixed IP Address so I’m assuming that the PLC’s and IP cameras log in and request the DNS to provided IP Address via Jason request data

I’m using UniGUI as the bases of the web site pages so the server system needs to interact with UniGUI.

If anyone can provide guidance to the above or would like to work with me on this server system I would be very much appreciative.

Example code would be awesome.

Kind Regards
Grant
M : 0412 926 995
E : grant@countrytradies.net

1 Like

Hi Grant,

I’ve only had a little play with the trial of these… but these components by our Symposium 2025 prize sponsor Mitov, may be of some assistance to some of the things that you need.

Hi Grant

Debugging a service application can be difficult so I generally create a console application which creates and manages a single object which in turn creates and manages my DB Server, my Web server, my control application, etc.

These server objects typically operate under their own “Listener” threads interacting with the main object only for logging and errors.

Once you have this application running and debugged then create your service application and

  • In ServiceCreate - Create the Control object, Perhaps add a timer to monitor its status and log changes
  • In ServiceStart Log the start
  • In ServiceContinue Check the status of the Control Object and recreate if necessary
  • In ServiceStop Free and nil the Control Object;

At any time you can revert to the console application to improve or debug functionality.

A cut down version of a Console Application

Var
  ServerObject: TServerMonitorTCpViaIndy;
  sl,s:string;
  StatusCount,Timercount :integer;
  
Begin
    ServerObject := TServerMonitorTCpViaIndy.Create(nil);
     Try
      while ServerObject.IsActive do
      begin
        if Timercount > 30 then
        begin
          Timercount := 0;
          inc(StatusCount);
          sl := ServerObject.IsTCPAppServer.ReadLogMessage;
          if sl <> '' then
            writeln(sl)
          else
            writeln('ping ' + FormatDateTime('nn:ss', Now));

          if StatusCount > 30 then
          begin
            writeln(#13#10);
            writeln(#13#10);
            s := ServerObject.ReportSomething;
            writeln(s);
            writeln(#13#10);
            writeln(#13#10);
            s := ServerObject.IsTCPAppServer.CurrentAddressDetails;
            writeln(s);
            StatusCount := 0;
          end;
        end;
        // Sleep(30000);
        Sleep(1000);
        inc(Timercount);
      end;
    Finally
      ServerObject.Free;
    End;
  except
    on E: Exception do
    begin
      writeln(E.ClassName, ': ', E.Message);
      Sleep(60000);
    end;
  end;
  Sleep(10000);
end.

An example of the Control Object definition.

  TServerMonitorTCpViaIndy = class(Tobject)
  private
    FIsTCPAppServer: TIsIndyApplicationServer;
    FTsting:TIsMonitorTCPAppServer;
    FWebTestServer: TTestInnovaWebServerObj;
    // FLogFile: TLogFile;
    FOnCloseRequest: TSrvMonEvent;
    FAvailablelogs, FAvailablelogHints: TStringList;
    FIsActive, FInCreateTestServer, FInLogMessage: Boolean;
    FTCPMonitorPort: word;
    fDbFilename: string;
    FServiceNameDb, FServiceNameIndyDb, FServiceNameApache: string;
    FLogProc: TAnsiLogProcedure;
    FNextStatusLogTime: TDateTime;
    procedure LoadIniFile;
    procedure SetIsActive(const Value: Boolean);
    function GetIsActive: Boolean;
    Procedure LogStatusWhenRequired;
  public
    Constructor Create(AOnCloseRequest: TSrvMonEvent);
    Destructor Destroy; Override;
    Function IsTCPAppServer: TIsIndyApplicationServer;
    Function TestPoll: AnsiString;
    Function WebTestServer: TTestInnovaWebServerObj;
    Function ServiceLogFileName: AnsiString;
    Procedure LogAMessage(const AMessage: AnsiString);
    Function AddLogOrTextFile(AFileName, AHint: String): Boolean;
    Property IsActive: Boolean read GetIsActive write SetIsActive;
    Property LogProc: TAnsiLogProcedure Read FLogProc Write FLogProc;
  end;

And its Destructor

  destructor TServerMonitorTCpViaIndy.Destroy;
begin
  try
    LogAMessage(Formatdatetime('dd/mm  dddd hh:nn:ss', now) +
      ' Closing Server');
    if Assigned(FOnCloseRequest) then
      FOnCloseRequest(Self);
    FOnCloseRequest := nil;
    FreeAndNil(FIsTCPAppServer);
    FreeAndNil(FWebTestServer);
    FreeAndNil(FTsting);
    // FreeAndNil(FLogFile);
  finally
    inherited;
  end;
end;

My servers generally inherit from TIdTcpServer

TIsIndyApplicationServer = class(TIdTcpServer)

Allocating my own functions to OnConnect, OnExecute, OnContextCreated;
And Extending TIdServerContext

  TIsIndyTCPServerContext = class(TIdServerContext)
  Private
    FTcpRef: TISIndyTCPSvrSession;
    FDestroying: Boolean;
    Function RawTcpRef: TISIndyTCPSvrSession;
  Public
    Destructor Destroy; override;
    Function TcpRef: TISIndyTCPSvrSession;
    Procedure ReleaseOldRef;
  end;


constructor TIsIndyApplicationServer.Create(AOwner: TComponent);
begin
  FListLock := TCriticalSection.Create;
  FBusyLock := TCriticalSection.Create;
  Inherited;
  ContextClass := TIsIndyTCPServerContext;
  OnConnect := IsIdTCPSrvrConnect;
  OnExecute := IsIdTCPSvrSessionExecute;
  OnContextCreated := IsIdTCPSrvrContextCreated;
  LoadServerIniData;
end;

procedure TIsIndyApplicationServer.IsIdTCPSvrSessionExecute
  (AContext: TIdContext);
Var
  LContext: TIsIndyTCPServerContext;
  LConnection: TIdTCPConnection;
  TcpCtx: TISIndyTCPSvrSession;
  s: String;
begin
  if AContext = nil then
    exit;
  Try
    LContext := LocalContext(AContext);
    TcpCtx := nil;
    if LContext = nil then
      ISIndyUtilsException(Self, '#IsIdTCPSvrSessionExecute Nil Local Context')
    else
      TcpCtx := LContext.RawTcpRef;

    if TcpCtx = nil then
    begin
      ISIndyUtilsException(Self, '#IsIdTCPSvrSessionExecute Nil TcpCtx');
      exit;
    end;

    LConnection := LContext.Connection;
    if TcpCtx.FConnection <> LConnection then
    begin
      if LConnection = nil then
        ISIndyUtilsException(Self,
          '#IsIdTCPSvrSessionExecute Comtext Nil Coonection')
      else
        ISIndyUtilsException(Self, '#IsIdTCPSvrSessionExecute Wrong TcpCtx ::' +
          TcpCtx.TextID);
      exit;
    end;

    if LConnection <> nil then
      // while LConnection.Connected do
      if LConnection.Connected then
      begin
        if FResetStartTime > 0.01 then
          if FResetStartTime < now then
            FResetStartTime := 0.0
          else
          begin
            ISIndyUtilsException(Self, '# Closing ResetStartTime=' +
              FormatDateTime('dd hh:nn:ss.zzz', FResetStartTime));
            LConnection.Disconnect;
            Sleep(1000);
            exit;
          end;
        if TcpCtx <> nil then
          If not TcpCtx.ProcessNextTransaction Then
          Begin
            if GblLogAllChlOpenClose then
            begin
              AddLogMessage(TcpCtx.TextID, 'End Run Channel');
              ISIndyUtilsException(Self, '#Failed ProcessNextTransaction');
            end;
            TcpCtx.CloseGracefully;
            Sleep(1000);
          end;
      end;
  Except
    On E: Exception do
    Begin
      Try
        AddLogMessage('Server IsIdTCPSvrSessionExecute Ex:', E.Message);
        if TcpCtx <> nil then
          TcpCtx.LogAMessage('Execute Error:' + E.Message);
        if LContext.Connection <> nil then
          if LContext.Connection.Connected then
            AContext.Connection.Disconnect;
      Except
        On ee: Exception do
        Begin
          Try
            AddLogMessage('Double Exeption:', ee.Message);
          Except
          end;
        end;
      end;
    End;
  End;
end;

procedure TIsIndyApplicationServer.IsIdTCPSrvrConnect(AContext: TIdContext);
Var
  LContext: TIsIndyTCPServerContext;
  ThisSession: TISIndyTCPSvrSession;
begin
  Try
    LContext := LocalContext(AContext);

    LContext.ReleaseOldRef;
    ThisSession := LContext.TcpRef;

    ThisSession.FServerObject := Self;
    Inc(FServerTcpSessions);
    ThisSession.OnLogMsg := AddLogMessage;
    ThisSession.OnAnsiStringAction := OnSessionAnsiStringAction;
    ThisSession.OnStringAction := OnSessionStringAction;
    ThisSession.OnSimpleRemoteAction := OnSessionSimpleRemoteAction;
    if cLogAll then
      AddLogMessage('Server', 'On Connect::' + IntToStr(FServerTcpSessions));

    if FResetStartTime > 0.01 then
      if FResetStartTime > now then
        FResetStartTime := 0.0
      else
        LContext.FTcpRef.CloseGracefully;
    // accept no calls in reset period;

    if FResetStartTime < 0.001 then
      If ThisSession.AcceptStart then
      Begin
        if GblLogAllChlOpenClose then
        begin
          ISIndyUtilsException(Self, LContext.FTcpRef.TextID + '# Run Channel');
          AddLogMessage(LContext.FTcpRef.TextID, 'Run Channel');
        end;
      end
      else
        LContext.FTcpRef.CloseGracefully;
  Except
    On E: Exception do
    Begin
      ISIndyUtilsException(Self, E, 'IsIdTCPSrvrConnect');
      AddLogMessage('Server', 'OnConnect Error:' + E.Message);
    end;
  end;
end;

procedure TIsIndyApplicationServer.IsIdTCPSrvrContextCreated
  (AContext: TIdContext);
begin
  Inc(GlobalContext);
end;

Hope this helps

2 Likes

Grant,

I use Colin Wilson’s uDebugService.pas unit when debugging Windows Services, It turns a service EXE into a debuggable console app. Now you can use the normal IDE debugger to set breakpoints, examine variable values, Etc. in real-time.

I have been using it for years and it still works great on modern IDEs. I made a minor tweak to the unit to support EurekaLog monitoring of threads in the Service. It won’t break if you don’t use EurekaLog.

Here you go:

(*======================================================================*
 | unitDebugService                                                     |
 |                                                                      |
 | TDebugServiceApplication allows you to run and debug a service like  |
 | regular application                                                  |
 |                                                                      |
 | The contents of this file are subject to the Mozilla Public License  |
 | Version 1.1 (the "License"); you may not use this file except in     |
 | compliance with the License. You may obtain a copy of the License    |
 | at http://www.mozilla.org/MPL/                                       |
 |                                                                      |
 | Software distributed under the License is distributed on an "AS IS"  |
 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
 | the License for the specific language governing rights and           |
 | limitations under the License.                                       |
 |                                                                      |
 | Copyright © Colin Wilson 2002  All Rights Reserved
 |                                                                      |
 | Version  Date        By    Description                               |
 | -------  ----------  ----  ------------------------------------------|
 | 1.0      19/09/2002  CPWW  Original                                  |
 *======================================================================*)

(*
The question was asked... "How to Debug a Service?"

Although I have never found the need to debug an IW Service, a quick test confirms that the following approach works for IW as well.  I typically use the code EXTENSIVELY to debug my many services.

Step A:
Include the attached uDebugService.pas (by Colin Wilson circa 2002) in the project

Step B:
Include the following code in the project source:

begin
   if (paramCount > 0) and (SameText (ParamStr (1), '-DEBUG')) then
   begin
      FreeAndNil (Application);
      Application := TDebugServiceApplication.Create(nil);
   end;
   IWRun;
end.

Step C:
Include the following units in the project
  SvcMgr,
  SysUtils,

Step D:
Lastly, as per Step B, include -DEBUG in the Debugging Parameters of the compiler.

Step E
Run project.  I have found this uDebugService to work with EVERY service I have ever built.  Thanks all goes to Colin Wilson (where ever his blessed soul may be !) (http://www.wilsonc.demon.co.uk/index.htm)

This is a tool EVERY ONE should have in their bag of tricks. 
*)

unit uDebugService;

interface

uses
    Classes,
    Consts,
    {$IFDEF EUREKALOG}
    EBase,
    {$ENDIF}
    Forms,
    Messages,
    SvcMgr,
    SysUtils,
    Windows;

type

    //---------------------------------------------------------------------
    // TDebugServiceApplication class
    TDebugServiceApplication = class(TServiceApplication)
    private
        procedure OnExceptionHandler(Sender : TObject; E : Exception);
    public
        procedure Run; override;
        destructor Destroy; override;
        procedure TerminateThreads(all : Boolean);
    end;

    //---------------------------------------------------------------------
    // TDebugServiceThread class
    TDebugServiceThread = class(TThread)
    private
        fService: TService;
        procedure ProcessRequests(WaitForMessage : Boolean);
    protected
        procedure Execute; override;
    public
        constructor Create(AService : TService);
    end;

implementation

{ TDebugServiceApplication }

(*----------------------------------------------------------------------*
 | procedure TDebugServiceApplication.OnExceptionHandler                |
 |                                                                      |
 | Handler for VCL exceptions                                           |
 |                                                                      |
 | Parameters:                                                          |
 |   Sender: TObject; E: Exception                                      |
 *----------------------------------------------------------------------*)
destructor TDebugServiceApplication.Destroy;
begin

    try
        inherited;
    except
        MessageBeep($ffff);
    end;
end;


procedure TDebugServiceApplication.OnExceptionHandler(Sender : TObject; E : Exception);
begin
    DoHandleException(E);
end;


(*----------------------------------------------------------------------*
 | procedure TDebugServiceApplication.Run                               |
 |                                                                      |
 | Run the service
 *----------------------------------------------------------------------*)
procedure TDebugServiceApplication.Run;
var
    I :       Integer;
    service : TService;
    thread :  TThread;
begin
    //    Forms.Application.OnException := OnExceptionHandler;
    try

        // Create a TDebugServiceThread for each of the services

        for I := 0 to ComponentCount - 1 do begin
            if Components[I] is TService then begin
                service     := TService(Components[I]);
                thread      := TDebugServiceThread.Create(service);
                thread.Resume;
                service.Tag := Integer(thread);
            end;
        end;

        // Run the 'service'

        while not Forms.Application.Terminated do
            Forms.Application.HandleMessage;

        // Terminate each TDebugServiceThread

        TerminateThreads(True)

    finally
    end;
end;


{ TDebugServiceThread }

(*----------------------------------------------------------------------*
 | constructor TDebugServiceThread.Create                               |
 |                                                                      |
 | Constructor for TDebugServiceThread                                  |
 *----------------------------------------------------------------------*)
constructor TDebugServiceThread.Create(AService : TService);
begin
    fService := AService;
    inherited Create(True);
end;


(*----------------------------------------------------------------------*
 | procedure TDebugServiceThread.Execute                                |
 |                                                                      |
 | 'Execute' method fot TDebugServiceThread.  Process messages          |
 *----------------------------------------------------------------------*)
procedure TDebugServiceThread.Execute;
var
    msg :     TMsg;
    Started : Boolean;
begin
    {$IFDEF EUREKALOG}
    SetEurekaLogStateInThread(GetCurrentThreadID, IsEurekaLogActive);
    NameThread('ServiceThread', GetCurrentThreadID);
    {$ENDIF}
    PeekMessage(msg, 0, WM_USER, WM_USER, PM_NOREMOVE); { Create message queue }
    try
        Started := True;
        if Assigned(FService.OnStart) then
            FService.OnStart(FService, Started);
        if not Started then begin
            PostMessage(Forms.Application.Handle, WM_QUIT, 0, 0);
            ProcessRequests(True);
            Exit;
        end;
        try
            if Assigned(FService.OnExecute) then
                FService.OnExecute(FService)
            else
                ProcessRequests(True);
            ProcessRequests(False);
        except
            ON E: Exception do
                FService.LogMessage(Format(SServiceFailed, [SExecute, E.Message]));
        end;
    except
        ON E: Exception do begin
            FService.LogMessage(Format(SServiceFailed, [SStart, E.Message]));
            PostMessage(Forms.Application.Handle, WM_QUIT, 0, 0);
        end
    end;
end;


(*----------------------------------------------------------------------*
 | procedure TDebugServiceThread.ProcessRequests                        |
 |                                                                      |
 | 'ProcessRequests' method.  do a message loop.                        |
 *----------------------------------------------------------------------*)
procedure TDebugServiceThread.ProcessRequests(WaitForMessage : Boolean);
var
    msg :           TMsg;
    Rslt, stopped : Boolean;
begin
    while True do begin
        if Terminated and WaitForMessage then
            Break;
        if WaitForMessage then
            Rslt := GetMessage(msg, 0, 0, 0)
        else
            Rslt := PeekMessage(msg, 0, 0, 0, PM_REMOVE);

        if not Rslt then begin   // No message received, or WM_QUIT
            if not WaitForMessage then
                Break;

            // WM_QUIT received.  Terminate loop - if we're allowed
            stopped := True;

            if Assigned(fService.OnStop) then
                fService.OnStop(fService, stopped);
            if stopped then
                Break;
        end
        else
            DispatchMessage(msg);
    end;
end;


procedure TDebugServiceApplication.TerminateThreads(all : Boolean);
var
    I, N :    Integer;
    service : TService;
    thread :  TThread;
begin
    if all then
        N := 0
    else
        N := 1;

    for I := ComponentCount - 1 downto N do begin
        if Components[I] is TService then begin
            service := TService(Components[I]);
            thread  := TThread(service.Tag);
            if Assigned(thread) then begin
                PostThreadMessage(thread.ThreadID, WM_QUIT, 0, 0);
                thread.WaitFor;
                FreeAndNil(thread);
            end;
            service.Tag := 0;
        end;
    end;
end;

end.

1 Like

I agree with Roger, but compile the same project as a debuggable console application or release it as a standard windows service, without any 3rdParty units or complex structures:

program XXXX;
{$ifdef DEBUG}
{$APPTYPE CONSOLE}
{$endif}

uses
ShareMem, Vcl.SvcMgr, …

begin
{$IFDEF DEBUG}
try
// In debug mode the server acts as a console application.
WriteLn(‘MyServiceApp DEBUG mode. Press enter to exit.’);

Service := TService.Create(nil);

// Simulate service start.
Service.ServiceStart( Service , Status);

// Keep the console box running (ServerContainer1 code runs in the background)
ReadLn;
Service.ServiceStop( Service, Status);
// On exit, destroy the service object.
FreeAndNil(Service);
except
on E: Exception do
begin
WriteLn(E.ClassName, ': ', E.Message);
WriteLn(‘Press enter to exit.’);
ReadLn;
end;
end;
{$ELSE}
// Run as a true windows service (release).
if not Application.DelayInitialize or Application.Installing then
Application.Initialize;
Application.CreateForm(TService, Service);
Application.Run;
{$ENDIF}

end.

The exact same “Service” is used by the service or console app. You can debug it in debug mode, or release it and run as service.

Regards,
Cosmin

1 Like