21

Delphi Windows Service Design

I've never created a windows service but have been reading everything I've found. All the articles or examples I've run across are very basic in implementation and limited in their scope. Haven't seen anything that goes beyond this or that address specific scenarios. So, I have all the theory I'm probably going to find, and now I'm ready to dive into this project. I like to layout my ideas and get some feedback on what people think. I'll describe what I need from the application and how I intend to build it. I'd appreciate comments from anyone who has experience building windows services and any advice they would care to share.

[SCENARIO] Right now I have an application (I'll call this UPDATEAPPLICATION) that provides updates to all our other applications. In order to run any of our applications you first have to run this UPDATEAPPLICATION program and pass it a parameter of the desired application. The UPDATEAPPLICATION calls a WebService that returns XML information as to whether the desired application has any updates.

If there is an update, the UPDATEAPPLICATION downloads the update in EXE or ZIP format, and replaces the appropriate files to update the targeted application. Afterwards the UPDATEAPPLICATION does a ShellExecute to start the desired application and then the UPDATEAPPLICATION closes.

It's a fairly basic process that has worked well over the years. The UPDATEAPPLICATION program is a Delphi application, our other applications are mixed: Delphi, VB6, MS Access, .NET.

[THE PROBLEM] With the move to Vista and Windows 7, the security has changed dramatically. Because of the nature of the UPDATEAPPLICATION UAC won't allow the application to run under without Admin acces or UAC completely turned off. We are in the process of upgrading many of our applications to .NET and during this process I'd like the applications as well as the UPDATEAPPLICATION be UAC compliant. From what I've researched the only way to do this is by creating the UPDATEAPPLICATION as Windows Service. So, essentially, I need to duplicate the functionality of the UPDATEAPPLICATION into a Windows Service architecture.

[MY DESIGN] I'm using DelphiXE2. My design will consist of 3 parts to form a single solution: a Windows Service, a small tray Application to interact with the Windows Service, and my redesigned applications that will send messages to the Windows Service.

  1. My Windows Service (which I will call the UPDATESERVICE) will run as a Windows Service and create a TCP server to listen for requests.
  2. The tray application (which I will call TRAYAPP) will use TCP Client to configure/manage the UPDATESERVICE.
  3. My USERAPPLICATION, when started, will send a TCP message to UPDATESERVICE that's says "THIS APPLICATION" has started.

[UPDATESERVICE] Will listen for messages. If it receives a message that a USERAPPLICATION has started will it will call the web service to see if there are updates. If there are, the user will be notified to close the application and allow the UPDATESERVICE to update the application. The UPDATESERVICE will download the appropriate files and update the application.

Now that I've explained the basics of what I'm trying to do, I can ask my specific questions I need answered. These all have to do with how I should build my Windows Service. I also plan on using OmniThread for my thread management.

When my service starts, I need to create the TCP Server.

  1. Should the TCP Service be created on it's own thread?
  2. If the TCP Service is it's own thread, how do I keep the thread alive? Otherwise, I can start the TCP Service but I'm not sure what code I would use within the TCP Service unit to keep the thread running?
  3. What Windows Services event should create the TCP Service? OnExecute? OnStart? OnCreate? After all I've read it's unclear what event should be used.
  4. When the TCP Service receives a message to do something, should the work be executed within TCP Service thread or a new thread spawned off the main UPDATESERVICE? For example:
    • if the TCP Service gets a message to check for an update using HTTP should the TCP Service thread spawn a new thread to do this work
    • Or, should the TCP Service thread send a message to the UPDATESERVICE to spawn a new thread to do this work
    • Does it even matter?
  5. Is it possible to Start/Stop/Register/Unregister a windows service in Delphi Code?

This is all my questions. There probably isn't a right/wrong answer for this but simply a preference based on experience. If you've built services with Delphi you probably have some input that I would find useful. If you have a project that is more robust then a basic "start a service and sleep" and are willing to share it - even if I doesn't run or just psuedo code - I'm sure this would be invaluable. Thanks for reading my long-winded question. If you can think of a better way to go about this please share your thoughts. I'll add that several of our applications can be downloaded and run by the general public, so I don't have complete control over the expected environments. Any advice/comments/help would be appreciated.

Darren -
  • 285
  • 1
  • 3
  • 7

1 Answers1

31

fast answers:

1&3) Yes. As a rule of thumb do not implement the OnExecute service event. Spawn your own thread from the OnStart service event. The thread can be terminated when you receive the OnStop service event.

2) you keep your thread alive like this (execute method):

while not Terminated do
begin
  // do something
end;

4) normally each client connection will live on it's own thread. (ie the TCP server spawns a new thread for each client). Use a well known stack like Indy or ICS. Concerning the HTTP update, you can do this in the spawned client connection thread.

5) yes, be aware that you need elevated rights to do this.

I have made quite a few services in my career and I always use the same skeleton for the service application up till now:

unit u_svc_main;

interface

uses
  // Own units
  u_globals, u_eventlog, u_MyThread, 
  // Third party units
  // Delphi units
  Windows, Messages, Registry, SysUtils, Classes, SvcMgr;

type
  TMyService = class(TService)
    procedure ServiceCreate(Sender: TObject);
    procedure ServiceAfterUninstall(Sender: TService);
    procedure ServiceAfterInstall(Sender: TService);
    procedure ServiceShutdown(Sender: TService);
    procedure ServiceStop(Sender: TService; var Stopped: Boolean);
    procedure ServiceStart(Sender: TService; var Started: Boolean);
  private
    { Private declarations }
    MyThread : TMyThread;
  public
    { Public declarations }
    function GetServiceController: TServiceController; override;
  end;

var MyService : TMyService;

implementation

{$R *.DFM}

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

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

procedure TMyService.ServiceCreate(Sender: TObject);
begin
  DisplayName := 'myservice';
end;

procedure TMyService.ServiceAfterInstall(Sender: TService);
var
  Reg        : TRegistry;
  ImagePath  : string;
begin
  // create needed registry entries after service installation
  Reg := TRegistry.Create;
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    // set service description
    if Reg.OpenKey(STR_REGKEY_SVC,False) then
    begin
      ImagePath := Reg.ReadString(STR_REGVAL_IMAGEPATH);
      Reg.WriteString(STR_REGVAL_DESCRIPTION, STR_INFO_SVC_DESC);
      Reg.CloseKey;
    end;
    // set message resource for eventlog
    if Reg.OpenKey(STR_REGKEY_EVENTMSG, True) then
    begin
      Reg.WriteString(STR_REGVAL_EVENTMESSAGEFILE, ImagePath);
      Reg.WriteInteger(STR_REGVAL_TYPESSUPPORTED, 7);
      Reg.CloseKey;
    end;
    // set installdir
    if ImagePath <> '' then
      if Reg.OpenKey(STR_REGKEY_FULL,True) then
      begin
        Reg.WriteString(STR_REGVAL_INSTALLDIR, ExtractFilePath(ImagePath));
        Reg.CloseKey;
      end;
  finally
    FreeAndNil(Reg);
  end;
end;

procedure TMyService.ServiceAfterUninstall(Sender: TService);
var
  Reg : TRegistry;
begin
  Reg := TRegistry.Create;
  try
    // delete self created registry keys
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    Reg.DeleteKey(STR_REGKEY_EVENTMSG);
  finally
    FreeAndNil(Reg);
  end;
end;

procedure TMyService.ServiceShutdown(Sender: TService);
var
  Stopped : boolean;
begin
  // is called when windows shuts down
  ServiceStop(Self, Stopped);
end;

procedure TMyService.ServiceStart(Sender: TService; var Started: Boolean);
begin
  Started := False;
  try
    MyThread := TMyThread.Create;
    MyThread.Resume;
    NTEventLog.Add(Eventlog_Success, STR_INFO_SVC_STARTED);
    Started := True;
  except
    on E : Exception do
    begin
      // add event in eventlog with reason why the service couldn't start
      NTEventLog.Add(Eventlog_Error_Type, Format(STR_INFO_SVC_STARTFAIL, [E.Message]));
    end;
  end;
end;

procedure TMyService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
  try
    Stopped := True; // always stop service, even if we had exceptions, this is to prevent "stuck" service (must reboot then)
    MyThread.Terminate;
    // give MyThread 60 seconds to terminate
    if WaitForSingleObject(MyThread.ThreadEvent, 60000) = WAIT_OBJECT_0 then
    begin
      FreeAndNil(MyThread);
      NTEventLog.Add(Eventlog_Success,STR_INFO_SVC_STOPPED);
    end;
  except
    on E : Exception do
    begin
      // add event in eventlog with reason why the service couldn't stop
      NTEventLog.Add(Eventlog_Error_Type, Format(STR_INFO_SVC_STOPFAIL, [E.Message]));
    end;
  end;
end;

end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
whosrdaddy
  • 11,720
  • 4
  • 50
  • 99
  • 2
    Sweet. Thanks for taking the time to respond. This is the practical type of input I was looking for. Your skeleton code is fleshed out starting point that helps alot better than much of what I've found so far. Your other answers we're what I was expecting, too. just nice to hear if from someone who's been down that path. Thanks again. – Darren - May 10 '12 at 16:44
  • I am in a similar situation, creating my first windows service, and I found your reply here very helpful. Thanks for sharing it. I still have two questions: In ServiceAfterInstall, the registry entry being written just after the "// set installdir" comment - What is its purpose? - What are the correct REGKEY and REGVAL for that entry? – Neville Cook Aug 18 '13 at 01:03
  • 1
    @NevilleCook, STR_REGKEYFULL is the HKLM registry key for your application, something like HKLM\Software\MyCompany\MyApp, STR_REGVAL_INSTALLDIR would represent the Key "InstallDir" = the installation path of your service. You don't really need it, you can always read the ImagePath key of your service to determine installation path (like I do in AfterInstall). – whosrdaddy Aug 18 '13 at 06:38
  • @whosrdaddy, thanks for the reply. Now I know it is not for any specific purpose I can move forward with more confidence. – Neville Cook Aug 18 '13 at 13:57
  • @RemyLebeau, where does the content of `ImagePath` appears? – NaN Aug 19 '13 at 21:02
  • @EASI: I don't understand what you are asking. `ImagePath` is created by the SCM when the service is installed. The code could just as easily have used `ParamStr(0)` instead to get the path an filename of the calling process. – Remy Lebeau Aug 19 '13 at 22:37
  • Great answer which helps a lot. Thanks! – r4w8173 Nov 15 '16 at 08:27
  • Dear whosrdaddy can I please ask you, what would your service-skeleton-code be if the Service on itself its is not a Server (tcp, pop,ftp...), but only a client? Let us say it looks for files, loads them and send them somewhere. In the case above, I think a single thread would be enough? So can I use the Simple OnExecute method ? Furthermore, I see that on the OnStart method, you do wait only for 60 sec before Terminating the job. What I need is to wait for the job until it finishes - not to terminate it. And only then the Windows service shuts down. best regards Altin – altink Nov 28 '17 at 15:25
  • @altink, In that case I would look into using a scheduled task, no need for a service IMHO... – whosrdaddy Nov 28 '17 at 15:40
  • thank you very much @whosrdaddy for the prompt response. do you mean something out of Delphi? sorry, but trying to keep my post short I didn't mentioned that I need this done as a Windows service made with delphi TService regards Altin – altink Nov 28 '17 at 15:50
  • @altink: I mean you can make a normal delphi program and schedule it via task scheduler. Why do you want to use TService? (it does not make sense)? – whosrdaddy Nov 28 '17 at 17:36
  • thank you @whosrdaddy a normal Delphi program via task scheduler ? but will it require the user to be logged on ? I want it to be as a service to make it automatic and run at reboot without user intervention. Are you saying that I can make my program as a simple single form and call this via windows scheduler - working like any NT Service? Sorry - I have no good knowledge of "system" things in Delphi. – altink Nov 28 '17 at 18:56
  • @altink You can make a simple console application and schedule it via task scheduler (task scheduler can run applications even if you are not logged on) – whosrdaddy Nov 28 '17 at 19:52
  • thank you very much @whosrdadd but I have never done so far a console application, and it seems (could be wrong) that the complexity is almost the same as Service coding - which I have at least tested sometimes - but with non-production things. Furthermore I have seen that the .exe sometimes has problems with the AVs. The above made me go for a Service Thank you very much regards Altin – altink Nov 29 '17 at 12:33
  • @whosrdaddy, what if `MyThread` is not teminated after 60 sec? Would it be right to set `FreeOnTerminate := false` in `MyThread`'s constructor and then set `FreeOnTerminate := true` after 60 sec? – Old Skull Aug 22 '20 at 09:56
  • @OldSkull: don't do that, the 60 seconds is an arbitrary value which works in my case. If you have longer runnning threads you can opt to increase the timeout.Or Make sure that the execute loop of your thread checks the Terminated variable at regular intervals. – whosrdaddy Aug 22 '20 at 11:07