3

I would just like a tip about a situation.

I created a Windows service that does the task management of my application.

The service connect to the database (Firebird) and call a component that does the task management.

The process works fine, however, in Windows 10 the service does not start automatically after the computer is restarted. In other versions of Windows everything works perfectly. In testing, I have identified that if I comment on the method that calls the execution of the tasks, the service usually starts on Windows 10.

Procedure TDmTaskService.ServiceExecute(Sender: TService);
Begin
  Inherited;

  While Not Terminated Do
  Begin
    //Process;
    Sleep(3000);
    ServiceThread.ProcessRequests(False);
  End;

End;

The problem is that nothing exception is generated in component or service.

By analyzing the Windows Event Monitor, I have identified that the error that occurred with my service is Timeout, in which case the service was unable to connect to the service manager within the time limit. No more exceptions are generated.

Would anyone have any about Windows Services made in Delphi that connect to database?

Example of my source code:

**Base class:**

unit UnTaskServiceDmBase;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs;

type
  TDmTaskServicosBase = class(TService)
  private
    { Private declarations }
  public
    function GetServiceController: TServiceController; override;
    { Public declarations }
  end;

var
  DmTaskServiceBase: TDmTaskServicosBase;

implementation

{$R *.DFM}

procedure ServiceController(CtrlCode: DWord); stdcall;
begin
  DmJBServicosBase.Controller(CtrlCode);
end;

function TDmTaskServicosBase.GetServiceController: TServiceController;
begin
  Result := ServiceController;
end;

end.    

**Service Class:**    

Unit UnTaskServiceDm;

    Interface

    Uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs,

      UnJBTask,
      UnJBReturnTypes,
      UnJBUtilsFilesLog,
      UnTaskServiceDmConfig,
      UnTaskServiceDmConnection,
      ExtCtrls,
      IniFiles;

    Type
      TDmTaskService = Class(TDmTaskServicosBase)
        Procedure ServiceExecute(Sender: TService);
        Procedure ServiceCreate(Sender: TObject);
        Procedure ServiceStop(Sender: TService; Var Stopped: Boolean);
      Private
        FTaskServiceConfig: TDmTaskServiceConfig;
        FStatus: TResultStatus;
        FDmConnection: TDmTaskServiceConnection;
        FJBTask: TJBTask;
        FLog: TJBUtilsFilesLog;

        Procedure ExecuteTasksSchedule;
        Procedure UpdateServiceInformation;
        Procedure Process;
        Procedure UpdateConnection;
      Public
        Function GetServiceController: TServiceController; Override;
      End;


    Implementation

    {$R *.DFM}

    Procedure ServiceController(CtrlCode: DWord); Stdcall;
    Begin
      DmTaskService.Controller(CtrlCode);
    End;

    Procedure TDmTaskService.UpdateConnection;
    Begin

      Try
        FDmConnection.SqcCon.Connected := False;
        FDmConnection.SqcCon.Connected := True;

        FLog.Adicionar('Conexão com banco restabelecida.');
        FLog.FinalizarLog;
      Except

        On E: Exception Do
        Begin
          FLog.Adicionar('Erro ao restabelecer conexão com o banco de dados.' +
            sLineBreak + sLineBreak + E.Message);
          FLog.FinalizarLog;
        End;

      End;

    End;

    Procedure TDmTaskService.UpdateServiceInformation;
    Begin
      Inherited;

      Try

        Try
          FTaskServiceConfig.Load;

          FLog.Adicionar('Dados registro serviço.');
          FLog.Adicionar('Nome: ' + FTaskServiceConfig.ServiceName);
          FLog.Adicionar('Descrição: ' + FTaskServiceConfig.ServiceDescription);

          If (FTaskServiceConfig.ServiceName <> EmptyStr) And
            (FTaskServiceConfig.ServiceDescription <> EmptyStr) Then
          Begin
            Name := FTaskServiceConfig.ServiceName ;
            DisplayName := FTaskServiceConfig.ServiceDescription;
          End;

          FTaskServiceConfig.Close;

        Except

          On E: Exception Do
          Begin
            FLog.Adicionar('Erro adicionar dados registro serviço.');
            FLog.Adicionar('Erro ocorrido: ' + sLineBreak + sLineBreak + E.Message);
          End;

        End;

      Finally
        FLog.Adicionar('Name: ' + Name);
        FLog.Adicionar('DisplayName: ' + DisplayName);
        FLog.FinalizarLog;
      End;

    End;

    Procedure TDmTaskService.Process;
    Begin

      Try

        If FDmConnection.SqcCon.Connected Then
        Begin

            ExecuteTasksSchedule;

        End
        Else
          UpdateConnection;

      Except

        On E: Exception Do
        Begin

          FLog.Adicionar('Ocorreu um erro ao checar as tarefas.' + sLineBreak +
            'Erro ocorrido: ' + sLineBreak + E.Message);
          FLog.FinalizarLog;

          UpdateConnection;

        End;

      End;

    End;

    Procedure TDmTaskService.ExecutarTarefasAgendadas;
    Begin

      If FJBTask.ExistTaskDelayed Then
      Begin

        Try
          FJBTask.ExecuteTasks;
        Except

          On E: Exception Do
          Begin
            FLog.Adicionar('Ocorreu um erro ao executar as tarefas agendadas.' +
              sLineBreak + 'Erro ocorrido: ' + sLineBreak + E.Message);
            FLog.FinalizarLog;

            UpdateConnection;
          End;

        End;

      End;

    End;

    Function TDmTaskService.GetServiceController: TServiceController;
    Begin
      Result := ServiceController;
    End;

    Procedure TDmTaskService.ServiceCreate(Sender: TObject);
    Begin
      Inherited;

      Try
        FLog := TJBUtilsFilesLog.Create;
        FLog.ArquivoLog := IncludeTrailingPathDelimiter(FLog.LogFolder) + 'TaksService.log';

        FDmConnection := TDmTaskServiceConexao.Create(Self);
        FDmConnection.Log := FLog;

        FJBTask := TJBTarefa.Create(Self);
        FJBTask.SQLConnection := FDmConnection.SqcConexao;

        FTaskServiceConfig := TDmTaskServiceConfig.Create(Self);
        FTaskServiceConfig.SQLConnection := FDmConnection.SqcConexao;

        FStatus := FDmConnection.ConfigurouConexao;

        If FStatus.ResultValue Then
        Begin
          UpdateServiceInformation;
        End
        Else
        Begin
          FLog.Adicionar(FStatus.MessageOut);
          FLog.FinalizarLog;
        End;

      Except

        On E: Exception Do
        Begin
          FLog.Adicionar('Não foi possível iniciar o serviço.' + sLineBreak +
            'Erro ocorrido: ' + sLineBreak + sLineBreak + E.Message);
          FLog.FinalizarLog;
          Abort;
        End;

      End;

    End;

    Procedure TDmTaskService.ServiceExecute(Sender: TService);
    Begin
      Inherited;

      While Not Terminated Do
      Begin
        Process;
        Sleep(3000);
        ServiceThread.ProcessRequests(False);
      End;

    End;

    Procedure TDmTaskService.ServiceStop(Sender: TService; Var Stopped: Boolean);
    Begin
      Inherited;

      If Assigned(FDmConnection) Then
      Begin

        FLog.Adicionar('Finalizando serviço.');
        FLog.Adicionar('Fechando conexão.');
        Try
          FDmConnection.SqcConexao.Close;
        Finally
          FLog.FinalizarLog;
        End;

      End;

    End;

    End.
Delphiman
  • 373
  • 4
  • 15
  • 1
    On Windows startup, assume your service starts *before* the database service has started, thus making your service fail to connect. When that happens, it raises an exception. If that exception isn't handled, the service will halt. *EDIT* I just looked closer and there is some exception handling which should catch this occasion. – Jerry Dodge Aug 23 '17 at 20:23
  • 1
    What is the question? – David Heffernan Aug 23 '17 at 20:25
  • 2
    On a side note, `Sleep(3000);` is begging for trouble - it's a huge no-no in services, and threads in general. At least do it in a loop which continuously checks if it's terminated, at bare minimum. – Jerry Dodge Aug 23 '17 at 20:28
  • 2
    I have a bad feeling you haven't provided all the code. Since everything has `Inherited;` in it, this tells me that your entire service data module is inherited from another one, which could have more code. Your problem is probably in that ancestor code. But, it's not inherited, it has `TService` directly on there, so why you have `Inherited;` all over, I don't know. – Jerry Dodge Aug 23 '17 at 20:30
  • @JerryDodge: "*Since everything has `Inherited;` in it, this tells me that your entire service data module is inherited from another one, which could have more code*" - it is not unusual for newbies who don't know any better to put `inherited` in event handlers, not knowing that doing so is basically a no-op. – Remy Lebeau Aug 23 '17 at 22:41
  • @JerryDodge. You're right, my service inherits from another class. But this base class has no code that could interfere with the situation. I just modified the class to best exemplify for you. – Delphiman Aug 24 '17 at 11:29
  • @RemyLebeau, sure, but it's not my case, I'll use the inherited statement. It was just an example that I wanted to display in a simpler way. – Delphiman Aug 24 '17 at 11:31

3 Answers3

9

By analyzing the Windows Event Monitor, I have identified that the error that occurred with my service is Timeout, in which case the service was unable to connect to the service manager within the time limit. No more exceptions are generated.

Do not connect to your database, or do any other lengthy operations, in the TService.OnCreate event. Such logic belongs in the TService.OnStart event instead. Or better, create a worker thread for it, and then start that thread in the TService.OnStart event and terminate it in the TService.On(Stop|Shutdown) events.

When the SCM starts your service process, it waits for only a short period of time for the new process to call StartServiceCtrlDispatcher(), which connects the process to the SCM so it can start receiving service requests. StartServiceCtrlDispatcher() is called by TServiceApplication.Run() after all TService objects have been fully constructed first. Since the OnCreate event is called while your process is trying to initialize itself, before StartServiceCtrlDispatcher() is called, any delay in service construction can cause the SCM to timeout and kill the process.

Also, you should get rid of your TService.OnExecute event handler completely. You shouldn't even be using that event at all, and what you currently have in it is no better than what TService already does internally when OnExecute is not assigned any handler.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 1
    Indeed, proper service design should never do work in the main service thread. In fact I always write every service in a thread within a test app first, then migrate it over to the service. I have a template I use which allows me to simply start/stop the thread in the start/stop of the service. It would be nice if Borland had never made the `OnExecute` event available in the first place :-) – Jerry Dodge Aug 23 '17 at 23:34
  • If you say OnExecute should not be used at all, whats the alternative? and whats wrong with OnExecute? – Alec Aug 24 '17 at 05:50
  • 1
    @Fero: the alternative is to do what I already explained - create a thread to do your work, and start it in the `OnStart` event and stop it in the `On(Stop|Shutdown)` event. The `OnExecute` event should not be used because it is very easy to mess up. Too many newbies get it wrong and then wonder why their services don't work. `TService` handles SCM requests just fine without `OnExecute` assigned, so let it. – Remy Lebeau Aug 24 '17 at 06:03
  • 1
    @Fero, to expand on Remy's answer, [here](https://stackoverflow.com/a/10538102/800214) you can find how to correclty implement a service in Delphi – whosrdaddy Aug 24 '17 at 06:52
  • @RemyLebeau, Remy, thanks for the tips, always very important. I used the OnCreate event to connect because my service can have multiple instances, that is, I can have the same service, connected to different databases. So I need to give it a different name before I start it. This name is in the database. Before showing you I had tried several possibilities arriving at the service I have now, which works without problems, but does not start automatically in Windows 10. I will reformulate the process based on your tips and examples and confirm the return here. – Delphiman Aug 24 '17 at 11:43
  • @Delphiman do not store the service name in a database. It doesn't belong there. Delay all database activity to the `OnStart` event or later. The best place to store the service name is in the service's registered Registry key as an added command line parameter to the executable path so it gets passed to the service process when the SCM is starting it. There are plenty of examples of how to do this for `TService`. – Remy Lebeau Aug 24 '17 at 16:23
  • @RemyLebeau, perfect, thanks for your tip.I'll arrange for the change. – Delphiman Aug 24 '17 at 21:15
1

In your service code : - you can try to add Dependencies on your Firebird Service - you can increase WaitHint

if it still not work : you can start as automatic but "Delayed"

Hugues Van Landeghem
  • 6,755
  • 3
  • 34
  • 59
  • 1
    The other answer is the correct fix. This is just a work-around, which isn't even guaranteed to work. – Jerry Dodge Aug 23 '17 at 23:38
  • @HuguesVanLandeghem, In my case, I added Firebird as a dependent service, but still, it still did not work. The detail is that the service only does not start automatically in Windows 10. In the rest of Windows it works without problems. I'll test with your WaitHint tip and confirm whether it worked or not. I can not use the start delayed option because it does not work on Windows 2000, 2003 and XP versions. Which in that case is part of our customers. – Delphiman Aug 24 '17 at 11:35
  • @HuguesVanLandeghem, Just incrementing WaitHint did not work. Just to give you a comeback. The solution was the answer I just posted. – Delphiman Aug 24 '17 at 13:51
  • @Delphiman I think the word you mean to use is "feedback". "comeback" is a negative thing, usually a form of retaliation. – Jerry Dodge Aug 24 '17 at 13:52
  • @JerryDodge, you are right. Sorry, my English is. I meant feedback. – Delphiman Aug 24 '17 at 15:29
1

I found it otherwise to solve, however, I thank everyone for the tips, because in a timely manner you will make improvements to my service.

The solution was to extend the service startup timeout through the Windows ServicesPipeTimeout registry key.

For my case it worked perfectly. I increased the value of ServicesPipeTimeout to 120000 (2 minutes). By default the value is 30000 (30 seconds) or less.

To manual Edit:

1) Open the Windows Regedit App; 2) Locate and then click the following registry subkey:   - HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control In the panel values, locate the ServicesPipeTimeout entry.

** Note **: 
If the ServicesPipeTimeout entry does not exist, you must create it. To do 
this, follow these steps:

 - On the Edit menu, point to New, and then click DWORD Value.  - Type ServicesPipeTimeout, and then press ENTER. 3) Right-click ServicesPipeTimeout, and then click Modify. 4) Click Decimal, type 120000, and then click OK. ** 120000 miliseconds = 2 minutes 5) Restart the computer.

In Delphi (Sample registry value):

Procedure TForm3.JBButton3Click(Sender: TObject);
Const
  CKeyConfigTimeout = 'SYSTEM\CurrentControlSet\Control';
  CValueConfigTimeout = 'ServicesPipeTimeout';

Var
  LReg: TRegistry;

Begin

  LReg := TRegistry.Create;
  Try
    LReg.RootKey := HKEY_LOCAL_MACHINE;
    LReg.OpenKey(CKeyConfigTimeout, False);
    LReg.WriteInteger(CValueConfigTimeout, 120000);
  Finally
    LReg.CloseKey;
    FreeAndNil(LReg);
  End;

End;

Note: The delphi application with the registry update code needs to run in administrator mode for Windows Vista / Server or Superior versions;

Delphiman
  • 373
  • 4
  • 15
  • This is just another workaround that is not guaranteed to work. The proper fix is to redesign the service to stop doing anything at startup that can delay the service from connecting to the SCM so it wont timeout in the first place. – Remy Lebeau Aug 24 '17 at 16:27
  • @RemyLebeau, I agree with you. This change in timeout (ServicesPipeTimeout) will allow me to gain time to refactor the process. It will be a temporary solution. Thank you very much for the valuable tips. – Delphiman Aug 24 '17 at 21:19