http://www.tolderlund.eu/delphi/service/service.htm
Last updated 03-10-2012.
This tutorial is not finished, it is a work in progress.
Warning: Use of any files and information from this tutorial is at your own risk.
In this tutorial the following topics will be covered:
Create a service
Install and Uninstall the service application
Make the service do something
Debugging the service application
Using the TService.LogMessage method
Sample code for a service application
Links
FAQ
The Tutorial assumes you have Delphi 7, but it should work the same in other Delphi versions.
Create a service
How do we create Windows Services in Delphi?
Well, that's actually easy to do in Delphi. Select the menu items File, New, Other and select "Service Application" and click OK.
Note that if you have the Standard edition of Delphi "Service Application" may not be available. You need at least the Professional edition.
You now have the framework for a service application which includes a TService class.
The TService class is where we do our stuff and it has a number of properties which you can see in the Object Inspector.
Among the properties you will see a Name, DisplayName, ServiceStartName and Password property.
Name property:
Enter a good descriptive name for your service in the Name property.
Do not just leave the name as Service1, but choose a more descriptive name such as "CompanynameSqlDatabaseSpecialSomething" (no blanks in the name).
Why is this Name property so important?
It's important to choose a good name, because when installing the service this name is automatically used to create a key in the registry under
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services.
So be sure to use a name which will not be used by other services, otherwise you might end up with a nasty registry key name conflict with other services.
Also we will later use the Name property ourself to create a key in the registry under
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application.
More about this key later.
DisplayName property:
Enter a user friendly and descriptive name such as "Companyname SQL Database Special Something" (feel free to use spaces in the name).
The DisplayName is used for displaying when you use the Control Panel, Administration, Services applet.
ServiceStartName property:
You can specify an account name in this property and password in the Password property to specify which account the service should log on as.
That requires that you know at design time the account name and password, and who knows that?
Just leave ServiceStartName and Password empty. You can always enter an account name and password in the service properties after the service has been installed.
A service runs as a specific user and that means that a service has the same access permissions to different things such as folders as the account under which the service runs. Many services runs as "Local System" unless you specify a specific username when installing the service. For many things "Local System" is sufficient, however if the service needs to have access to things such as a folder on a network drive/share (which are normally user/password protected) you may need to specify an account which has been granted access to the network share.
I'll assume that you have created the service framework as shown above. We haven't written a single line of code ourself yet, that will come shortly.
Let's first install the service and see if it can run.
First save the project (File, Save All) in a folder on your local harddrive. Windows won't run a service application if it's located on a network drive.
For this tutorial save the unit as MyServiceUnit.pas and the project as MyService.dpr. Compile or Build the project.
Install and Uninstall the service application
Note that you need administrator rights to install and uninstall service applications, because it is necessary to write or delete registry entries in HKEY_LOCAL_MACHINE, and that requires permissions that normal or restricted users do not have.
To install the service application you open a command prompt and type:
MyService.exe /install
You will see a confirmation dialog when the service has been successfully installed or an error message if it failed. It can fail if you do not have sufficient rights.
If you do not want to see the confirmation dialog you can add the /silent switch like this:
MyService.exe /install /silent
MyService.exe /uninstallYou will see a confirmation dialog when the service has been successfully uninstalled or an error message if it failed. It can fail if you do not have sufficient rights.
MyService.exe /uninstall /silent
procedure TMyTestServiceApp.ServiceAfterInstall(Sender: TService); var Reg: TRegistry; begin Reg := TRegistry.Create(KEY_READ or KEY_WRITE); try Reg.RootKey := HKEY_LOCAL_MACHINE; if Reg.OpenKey('\SYSTEM\CurrentControlSet\Services\' + Name, false) then begin Reg.WriteString('Description', 'This is a description for my fine Service Application.'); Reg.CloseKey; end; finally Reg.Free; end; end;Remember to add Registry to the uses clause.
procedure TCompanySqlDatabaseSpecialSomething.ServiceExecute( Sender: TService); const SecBetweenRuns = 10; var Count: Integer; begin Count := 0; while not Terminated do begin Inc(Count); if Count >= SecBetweenRuns then begin Count := 0; { place your service code here } { this is where the action happens } SomeProcedureInAnotherUnit; end; Sleep(1000); ServiceThread.ProcessRequests(False); end; end;
private { Private declarations } MyServiceThread: TMyServiceThread;Now you need to create and fill in the OnStart and OnStop events like this:
procedure TCompanySqlDatabaseSpecialSomething.ServiceStart( Sender: TService; var Started: Boolean); begin { Create an instance of the secondary thread where your service code is placed } MyServiceThread := TMyServiceThread.Create; { Set misc. properties you need (if any) in your thread } //MyServiceThread.Property1 := whatever; // and so on MyServiceThread.Resume; end; procedure TCompanySqlDatabaseSpecialSomething.ServiceStop(Sender: TService; var Stopped: Boolean); begin MyServiceThread.Terminate; end;What happens here is that the secondary thread is created and started in the service's OnStart event.
unit MyServiceThreadUnit; { This thread frees itself when it terminates } interface uses Windows, Messages, SysUtils, Classes, Graphics; type TMyServiceThread = class(TThread) private { Private declarations } protected procedure Execute; override; public constructor Create; end; implementation { Important: Methods and properties of objects in visual components can only be used in a method called using Synchronize, for example, Synchronize(UpdateCaption); and UpdateCaption could look like, procedure TMyServiceThread.UpdateCaption; begin Form1.Caption := 'Updated in a thread'; end; } { TMyServiceThread } constructor TMyServiceThread.Create; // Create the thread Suspended so that properties can be set before resuming the thread. begin FreeOnTerminate := True; inherited Create(True); end; procedure TMyServiceThread.Execute; const SecBetweenRuns = 10; var Count: Integer; begin { Place thread code here } while not Terminated do // loop around until we should stop begin Inc(Count); if Count >= SecBetweenRuns then begin Count := 0; { place your service code here } { this is where the action happens } SomeProcedureInAnotherUnit; end; Sleep(1000); end; end; end.Using the OnStart method to start a secondary thread has it's advantages and drawbacks.
constructor TMyServiceThread.Create; // Create the thread Suspended so that properties can be set before resuming the thread. begin FreeOnTerminate := False; inherited Create(True); end;And like this:
type TCompanySqlDatabaseSpecialSomething = class(TService) procedure ServiceStart(Sender: TService; var Started: Boolean); procedure ServiceStop(Sender: TService; var Stopped: Boolean); procedure ServiceShutdown(Sender: TService); private { Private declarations } MyServiceThread: TMyServiceThread; procedure ServiceStopShutdown; public function GetServiceController: TServiceController; override; { Public declarations } end; procedure TCompanySqlDatabaseSpecialSomething.ServiceStart(Sender: TService; var Started: Boolean); begin // Allocate resources here that you need when the service is running { Create an instance of the secondary thread where your service code is placed } MyServiceThread := TMyServiceThread.Create; { Set misc. properties you need (if any) in your thread } //MyServiceThread.Property1 := whatever; // and so on MyServiceThread.Resume; end; procedure TCompanySqlDatabaseSpecialSomething.ServiceStop(Sender: TService; var Stopped: Boolean); begin ServiceStopShutdown; end; procedure TCompanySqlDatabaseSpecialSomething.ServiceShutdown( Sender: TService); begin ServiceStopShutdown; end; procedure TCompanySqlDatabaseSpecialSomething.ServiceStopShutdown; begin // Deallocate resources here if Assigned(MyServiceThread) then begin // The TService must WaitFor the thread to finish (and free it) // otherwise the thread is simply killed when the TService ends. MyServiceThread.Terminate; MyServiceThread.WaitFor; FreeAndNil(MyServiceThread); end; end;
Debugging the service application Quote from http://info.borland.com/techpubs/delphi/delphi5/dg/buildap.htmlAccording to this: http://groups.google.dk/group/borland.public.delphi.nativeapi.win32/msg/13df743b00f57603?dmode=source&hl=da Go to http://www.wilsonc.demon.co.uk/delphi.htm and download 'NT Low Level Utilities' and use TDebugServiceApplication in unitDebugService.pas. Apparently Colin Wilson uses it for debugging only.Debugging services
The simplest way to debug your service application is to attach to the process when the service is running. To do this, choose Run|Attach To Process and select the service application from the list of available processes.
In some cases, this may fail, due to insufficient rights. If that happens, you can use the Service Control Manager to enable your service to work with the debugger:
- First create a key called Image File Execution Options in the following registry location:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion- Create a subkey with the same name as your service (for example, MYSERV.EXE). To this subkey, add a value of type REG_SZ, named Debugger. Use the full path to the debugger as the string value.
- In the Services control panel applet, select your service, click Startup and check Allow Service to Interact with Desktop.
LogMessage('Your message goes here SUCC', EVENTLOG_SUCCESS, 0, 1); LogMessage('Your message goes here INFO', EVENTLOG_INFORMATION_TYPE, 0, 2); LogMessage('Your message goes here WARN', EVENTLOG_WARNING_TYPE, 0, 3); LogMessage('Your message goes here ERRO', EVENTLOG_ERROR_TYPE, 0, 4);Use the message type as you think appropriate.
The description for Event ID ( 0 ) in Source ( MyService.exe ) cannot be found. The local computer may not have the necessary registry information or message DLL files to display messages from a remote computer. The following information is part of the event: Your message goes here.As you can see there is a lot of nonsense text in front of the message we specified in LogMessage.
; /* ------------------------------------------------------------------------- ; HEADER SECTION ;*/ SeverityNames=(Success=0x0: STATUS_SEVERITY_SUCCESS Informational=0x1: STATUS_SEVERITY_INFORMATIONAL Warning=0x2: STATUS_SEVERITY_WARNING Error=0x3: STATUS_SEVERITY_ERROR ) FacilityNames=(System=0x0: FACILITY_SYSTEM Runtime=0x2: FACILITY_RUNTIME Stubs=0x3: FACILITY_STUBS Io=0x4: FACILITY_IO_ERROR_CODE ) LanguageNames=(English=0x409:MSG00409) ;LanguageNames=(German=0x407:MSG00407) ; ;/* ------------------------------------------------------------------------- ; MESSAGE DEFINITION SECTION ;*/ MessageIdTypedef=WORD ;/* ; The message in the LogMessage call is shown in event log. ; LogMessage('Your message goes here', EVENTLOG_SUCCESS, 0, 1); ; LogMessage('Your message goes here', EVENTLOG_INFORMATION_TYPE, 0, 2); ; LogMessage('Your message goes here', EVENTLOG_WARNING_TYPE, 0, 3); ; LogMessage('Your message goes here', EVENTLOG_ERROR_TYPE, 0, 4); ; The message in the LogMessage call is not shown in event log. ; LogMessage('Your message goes here SUCC', EVENTLOG_SUCCESS, 0, 5); ; LogMessage('Your message goes here INFO', EVENTLOG_INFORMATION_TYPE, 0, 6); ; LogMessage('Your message goes here WARN', EVENTLOG_WARNING_TYPE, 0, 7); ; LogMessage('Your message goes here ERRO', EVENTLOG_ERROR_TYPE, 0, 8); ;*/ MessageId=0x1 Severity=Success Facility=Application SymbolicName=CATEGORY_SUCCESS Language=English %1 . MessageId=0x2 Severity=Success Facility=Application SymbolicName=CATEGORY_INFORMATION Language=English %1 . MessageId=0x3 Severity=Success Facility=Application SymbolicName=CATEGORY_WARNING Language=English %1 . MessageId=0x4 Severity=Success Facility=Application SymbolicName=CATEGORY_ERROR Language=English %1 . MessageId=0x5 Severity=Success Facility=Application SymbolicName=CATEGORY_SUCCESS Language=English Here is id5 success message . MessageId=0x6 Severity=Success Facility=Application SymbolicName=CATEGORY_INFORMATION Language=English Here is id6 information message . MessageId=0x7 Severity=Success Facility=Application SymbolicName=CATEGORY_WARNING Language=English Here is id5 warning message . MessageId=0x8 Severity=Success Facility=Application SymbolicName=CATEGORY_ERROR Language=English Here is id5 error message . ;/* ; For some reason Severity <> Success doesn't work properly ??? ;MessageId=0x6 ;Severity=Informational ;Facility=Application ;SymbolicName=CATEGORY_INFORMATION ;Language=English ;Here is id6 information message ;. ; ;MessageId=0x7 ;Severity=Warning ;Facility=Application ;SymbolicName=CATEGORY_WARNING ;Language=English ;Here is id5 warning message ;. ; ;MessageId=0x8 ;Severity=Error ;Facility=Application ;SymbolicName=CATEGORY_ERROR ;Language=English ;Here is id5 error message ;. ;*/You must write the lines exactly as shown here, otherwise you will get an error when you compile it. The message compiler is very picky.
"%VS71COMNTOOLS%Bin\mc.exe" MyServiceMessageResource.mcVS71COMNTOOLS is an environment variable created when Visual Studio is installed. It points to the "C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\" directory.
"%VS71COMNTOOLS%Bin\rc.exe" MyServiceMessageResource.rcFor the Delphi resource compiler issue the command
brcc32.exe MyServiceMessageResource.rcNow we have a MyServiceMessageResource.res file which we can include in the project with the line
{$R MyServiceMessageResource.res}
// Create registry entries so that the event viewer show messages properly when we use the LogMessage method. Key := '\SYSTEM\CurrentControlSet\Services\Eventlog\Application\' + Self.Name; Reg := TRegistry.Create(KEY_READ or KEY_WRITE); try Reg.RootKey := HKEY_LOCAL_MACHINE; if Reg.OpenKey(Key, True) then begin Reg.WriteString('EventMessageFile', ParamStr(0)); Reg.WriteInteger('TypesSupported', 7); Reg.CloseKey; end; finally Reg.Free; end;In the ServiceAfterUninstall method add these lines:
var Reg: TRegistry; Key: string; begin // Delete registry entries for event viewer. Key := '\SYSTEM\CurrentControlSet\Services\Eventlog\Application\' + Self.Name; Reg := TRegistry.Create(KEY_READ or KEY_WRITE); try Reg.RootKey := HKEY_LOCAL_MACHINE; if Reg.KeyExists(Key) then Reg.DeleteKey(Key); finally Reg.Free; end;Now when we use the LogMessage method the event viewer will only show our message, without all the other junk.