Delphi Services

Top  Previous  Next

http://www.freepascal.org/~michael/articles/services/services.pdf

 

entry point for the service (it is called SimpleServiceCtrlHandler).  It will return a handle

to the service, which can be used in the

SetServiceStatus

call.

After this is done, the named pipe is created on which clients can connect to the service. If

the application is running as a service, the status will subsequently be set to running, and

reported to the SCM.

After this is done, the actual work of the application is started.  It enters a loop, in which

it waits for clients to connect to the named pipe.   It does does this using a IOComple-

tionPort:  This is a special port which an application can request from windows, and by

which windows can notify the application that a certain IO operation is completed.  In our

case,  the  operation  is  a  client  which  makes  a  connection  on  the  named  pipe.   The  port

was created with the

CreateIoCompletionPort

at the start of the service entry. The

cmdPipe

constant used in that call tells windows that when someone connects to the pipe,

the

GetQueuedCompletionStatus

call  should  receive  the

cmdPipe

value  in  the

Command

argument:

GetQueuedCompletionStatus(ControlPort,BytesTransferred,Command,po,INFINITE);

If Command=cmdPipe then

HandlePipe;

The

INFINITE

argument  tells  Windows  that  the

GetQueuedCompletionStatus

call should wait forever before returning. When the call returns, the value stored in

Command

is checked: if it is

cmdPipe

, a client has connected to the named pipe, and the connection

to the pipe is handled in the

HandlePipe

call.

The loop is run until the service is stopped, or forever if the application is run as a normal

application. More information about the

CreateIoCompletionPort

and

GetQueuedCompletionStatus

will not be given here, as that would lead too far. The interested reader can consult the Mi-

crosoft API documentation for more information.

When the service needs to be stopped or paused, the SCM will send a control code to the

control entry point that was registered by the service.  The service control entry procedure

should accept a single argument: A

DWord

value, which is the control code being sent by

the SCM. As the control entry point is called by the SCM, it should have

stdcall

calling

conventions.   The  control  entry  point  should  do  whatever  is  necessary  for  the  received

control code, and report the service status as soon as possible with the SCM. Note that since

there is only one argument to the control entry point, there is no way to know for which

service the control code is sent, in case multiple services are provided by the application:

each service must have its own control entry point.

The control entry point for the simple service looks as follows:

Procedure SimpleServiceCtrlHandler (Opcode : DWord); StdCall;

begin

Case Opcode of

SERVICE_CONTROL_PAUSE :

begin

ClosePipe;

CurrentStatus.dwCurrentState:=SERVICE_PAUSED;

end;

SERVICE_CONTROL_STOP :

begin

ClosePipe;

CurrentStatus.dwCurrentState:=SERVICE_STOPPED;

end;

12

SERVICE_CONTROL_CONTINUE :

begin

CreatePipe;

CurrentStatus.dwCurrentState:=SERVICE_RUNNING;

end;

SERVICE_CONTROL_INTERROGATE : ;

else

ServiceError(SErrUnknownCode,[Opcode]);

end;

SetServiceStatus(ServiceStatusHandle,CurrentStatus);

// Notify main thread that control code came in, so it can take action.

PostQueuedCompletionStatus(ControlPort,0,cmdControl,NiL);

end;

As can be seen, it is a simple routine: Depending on the control code received, the pipe is

closed or created, and the new status of the service is stored in the global

CurrentStatus

record, after which the status is reported to the SCM. Then, the main loop is notified that

there was a change in status:  the

PostQueuedCompletionStatus

will queue a IO

completion event to the IOCompletionPort, with code

cmdControl

.  This will cause the

GetQueuedCompletionStatus

call in the main program loop to return. If the service

is being stopped, the main loop will then exit, and the service will stop. In any other case,

the main loop will simply repeat the

GetQueuedCompletionStatus

, waiting for a

connection on the pipe or till another control code arrives.

The rest of the code in the simple service is simply concerned with the handling of the pipe:

creating a pipe, closing it down or handling a client connection on the pipe.  The code is

quite simple, and the interested reader is referred to the source code which is available on

the CD-ROM accompagnying this issue.

To test the simple service, it can be installed with the service manager program developed

above, using the name ’SimpleService’, and giving it the

/RUN

command-line option. The

CD-ROM also contains a

simpleclient

client program which will connect to the service

and show the time it received from the service.

7    Services in Delphi

Borland has wrapped the API calls and functionality to write services in 2 classes, shipped

with Delphi’s VCL. They are contained in the

SvcMgr

unit. The classes are:

TServiceApplication

is a class which performs the same function as the

TApplication

provides for a normal GUI application.  It has all functionality to register services,

start services and respond to control codes. A global instance of this type is instanti-

ated in the

SvcMgr

initialization code, just as is done for its GUI counterpart in the

Forms

unit.

TService

is a

TDataModule

descendent which implements a service: it corresponds to

the

TForm

class in a normal GUI application. Each service application must contain

one or more descendents of the

TService

Class. It implements all functionality of

a single service: the main loop, events corresponding to the control codes that can be

sent by the SCM.

To create a service application, select ’New service Application’ from the ’New’ menu.

Delphi will then create a new project which contains 1 service.   Looking at the project

source, something similar to the following can be seen:

13

program diskclean;

uses

SvcMgr,

svcClean in ’svcClean.pas’ {CleanService: TService};

begin

Application.Initialize;

Application.Title := ’Disk cleaning service’;

Application.CreateForm(TCleanService, CleanService);

Application.Run;

end.

Which looks a lot like the code for a normal GUI application, with the exception that the

Application

object lives in the

SvcMgr

unit. Choosing

New service

from the

New

menu in Delphi will add new services to this list.

The

Run

method takes care of registering the services, unregistering them, or running the

services.  Only services registered with the

Application.CreateForm

method will

be installed or run.

A Delphi service application can install itself:  supplying the

/INSTALL

command-line

option will register all available services and exit after displaying a messagebox announc-

ing that the services have been installed.  The

/SILENT

option suppresses the message.

Likewise, the

/UNINSTALL

command-line option will unregister the services. The service

class has some handlers which can be used to respond to these events:

BeforeInstall

AfterInstall

BeforeUninstall

AfterUninstall

The meaning of these events should be clear from their name. The various other properties

such as

Name

,

DisplayName

,

ServiceType

and

StartType

of the

TService

class

should be set to an appropriate value, as they will be used when registering the service.

Giving other command-line options to the service application will actually run the service

application: In that case the

Run

method of the

TServiceApplication

class will start

a thread which registers the main entry point for all services using the

StartServiceCtrlDispatcher

call discussed in the previous section. One entry point is used for all services, and the op-

tions passed to the entry point are used to dispatch the call to the correct

TService

class

so it can do its work. After all service entry points were registered an event loop is started,

waiting for windows events to arrive. As soon as the thread stops, the application stops.

When the SCM starts a service, the call to the service entry point is dispatched to the

Main

method of the appropriate service instance.  Which service instance to use is determined

from the arguments to the entry point.

The

Main

method of the service is once more a big event loop, started in a separate thread

(available in the

ServiceThread

property of the service).  The loop is interspiced with

some event handlers and status reporting events. The following events exist:

OnStart

This method is called once when the service is started. The

Started

parameter

should be set to

True

if the service can be started succesfully.  Setting it to

False

will abort the service.

14

OnExecute

This is the main method of the service: here the service should do whatever it

is designed to do. It should regularly call

ServiceThread.ProcessRequests

:

this will process any windows events or incoming control codes.

When the SCM sends some control commands to the service, the service thread will call

one of the following event handlers:

OnPause

When  the  pause  command  was  received.   The  service  should  stop  what  it  is

doing, but should not exit.

OnContinue

When the continue command was received.

OnShutdown

When the service is being shut down.

OnStop

When the service is stopped. It is the

Each of these handlers is passed a boolean variable which should be set to

True

(default),

indicating that the command was correctly processed, or

False

to indicate that the com-

mand failed.  The service thread will then report the status (using the

ReportStatus

method) to the SCM.

As pointed out in the previous section, the service control entry point has only one argument

(the control code), making it impossible to use a single entry point for all services in the

application.  For this reason, the Delphi IDE inserts a control entry point for each service

that is created. The control entry point is inserted at the start of the implementation section

of the unit containing the service and looks similar to this:

procedure ServiceController(CtrlCode: DWord); stdcall;

begin

CleanService.Controller(CtrlCode);

end;

function TCleanService.GetServiceController: TServiceController;

begin

Result := ServiceController;

end;

The

GetServiceController

function is used by the

Main

function of the service to

obtain the control entry point which it registers with the SCM. The service entry point sim-

ply calls the

CleanService.Controller

method: Note that this makes dynamically

creating and running multiple instances of the same service class in code is not possible, as

the

CleanService

variable is then not valid. Obviously, this code should not be deleted

as the service will not be able to receive any control events.

8    A Disk cleaning service

To illustrate the concepts of a service application,  a disk cleaning service application is

developed: The service will sit in the background, and at a certain time will scan specified

directories on the hard disks for files matching certain extensions,  and if they exceed a

certain size, it will compress them and remove the original. The time to perform the scan,

as well as the minimum size and extension of the files to be processed can be configured:

Global values and per-directory values can be specified.  As with all services created with

Delphi, it can be installed by running it once with the

/INSTALL

command-line option.

The service can be compiled using TurboPower’s (freely available) Abbrevia:  it will then

create a zip file for each file it must process. By default it will use the

ZLIb

unit which can

15

be found on the Delphi installation CD: this will create a simple compressed file.  Setting

the

USEABBREVIA

conditional define will compile using Abbrevia.

The

TCleanService

class in the

svcClean

implements the service. It contains a timer

(

TClean

) and event logging class

ELClean

, discussed in an earlier edition of

Toolbox

.

The

TService

class itself has a logging method, but it doesn’t offer the functionality of

the

TEventLog

class, hence it is not used.

When the service is started, it gets the time when it should clean the disk from the registry:

procedure TCleanService.ServiceStart(Sender: TService;

var Started: Boolean);

begin

Started:=InitTimer;

If Not Started then

PostError(SErrFailedToInitTimer);

end;

The

PostError

uses the

ELClean

component to log an error message. The

InitTimer

method retrieves the time to run from the registry, and activates the timer so a timing event

will arrive when it is time to clean the disk.

The

Execute

handler contains a simple loop:

procedure TCleanService.ServiceExecute(Sender: TService);

begin

While not Terminated do

ServiceThread.ProcessRequests(True);

end;

The

ServiceThread.ProcessRequests

call will wait for windows event messages

and  control  commands;  This  ensures  that  when  the  timer  event  occurs,  the  associated

OnTimer

event handler will be executed:

procedure TCleanService.TCleanTimer(Sender: TObject);

begin

If Assigned(FCleanThread) then

PostError(SErrCleanThreadRunning);

StartCleanThread;

// Reset timer.

If Not InitTimer then

PostError(SErrFailedToInitTimer);

end;

The actual cleaning will be done in a separate thread: It is created in the

StartCleanThread

method. After that, the timer is re-initialised to deliver an event on the next day, same time.

The

StartCleanThread

operation simply starts a thread, and assigns some status re-

porting callbacks:

procedure TCleanService.StartCleanThread;

begin

FCleanThread:=TCleanThread.Create(True);

FCleanThread.OnTerminate:=Self.CleanThreadFinished;

(FCleanThread as TCleanThread).OnErrorMessage:=PostError;

16

(FCleanThread as TCleanThread).OnInfoMessage:=PostInfo;

FCleanThread.Resume;

end;

The

TCleanThread

is implemented in the

thrClean

unit, and is discussed below. Note

that the

OnTerminate

event of the service is assigned. It serves to set the

FCleanThread

to

Nil

when the service finished its job:

procedure TCleanService.CleanThreadFinished(Sender : TObject);

begin

With FCleanThread as TCleanThread do

PostInfo(Format(SFinishedClean,[FTotalFiles,FormatDateTime(’hh:nn:ss’,FTotalTime)]));

FCleanThread:=Nil;

end;

At the same time, some cleaning statistics are logged: the

FTotalTime

and

FTotalFiles

fields of the CleanThread contain the total time the thread was working and the number of

cleaned files, respectively.

When the service is stopped, the

OnStop

event handler is called:

procedure TCleanService.ServiceStop(Sender: TService;var Stopped: Boolean);

begin

TClean.Enabled:=False;

If Assigned(FCleanThread) then

With FCleanThread do

begin

If Suspended then

Resume;

Terminate;

WaitFor;

end;

Stopped:=True;

end;

The timer is stopped, and if the clining thread happens to be running, it is terminated. After

the thread stopped executing, the handler reports success and exits.

This is everything that is needed to implement our service. In order to support pause/continue

commands, the

OnPause

/

OnContinue

handlers can be used:

procedure TCleanService.ServicePause(Sender: TService;

var Paused: Boolean);

begin

TClean.Enabled:=False;

If Assigned(FCleanThread) then

FCleanThread.Suspend;

end;

procedure TCleanService.ServiceContinue(Sender: TService;

var Continued: Boolean);

begin

If Assigned(FCleanThread) then

FCleanThread.Resume

17

else

InitTimer;

end;

The code of the event handlers speaks for itself.

When the user changes the time on which the service should clean the disk, and the service

is running, it should be notified of this, so it can reset the timer. To do this, a custom control

code can be sent to the service.  Strangely enough, Borland has not implemented an event

to deal with custom control codes:  there is no

TService

event associated with such a

command.  Instead, the

DoControlCode

method must be explicitly overridden.  For the

cleaning service, it would be implemented as follows:

function TCleanService.DoCustomControl(CtrlCode: DWord): Boolean;

begin

Result:=(CtrlCode=ConfigControlCode);

If Result and TClean.Enabled then

begin

InitTimer;

PostInfo(SConfigChanged);

end;

end;

The

ConfigControlCode

constant is defined in the

cfgClean

unit. As can be seen,

when this code is received, the timer is simply re-initialised, and a message recording this

fact is logged.

The cleaning thread (implemented in unit

thrClean

is actually a simple loop. It contains

the following methods (among others):

Execute

The main function of the thread. It initializes some variables, and then loops over

the list of directories listed in the registry.

RecursivelyCleanDir

Called for each directory. It will clean all files in the directory, and

recursively call itself to handle subdirectorie.

CleanDir

Called for each directory, it scans all files in the directory, and if the file matches

the search criteria, it is cleaned.

CleanFile

Called for each file that matches the search criteria.  It determines whether the

file needs to be cleaned and cleans it if needed.

CompressFile

This method actually compresses the file. Two implementations exist: one

using Abbrevia, one using an internal method, based on the

ZLib

unit, delivered on

the Delphi CD-ROM.

The interested reader is referred to the unit itself.  The service can easily be adapted by

changing the

CleanFile

and

CompressFile

methods. For instance, the files could be

moved to a special directory instead of compressing them. They could also be archived in

1 big archive file, instead of creating a zip file per file. They could be simply deleted. It is

simple code, easily adapted to suit a particular need.

The  CD-ROM  contains  also  the  code  for  a  managing  application  (called

dkmngr

):   it

allows  to  set  the  needed  registry  entries,  and  to  start  or  stop  the  service.   It  uses  the

TServiceManager

component  introduced  at  the  beginning  of  this  article  to  do  this.

A screenshot of the application can be seen in figure figure 2 on page 19.

18

Figure 2: Disk cleaning service control application

9    Creating a dual application

There may be occasions when a dual application should be made; or one which should also

run on Windows 95,98 or millenium, operating systems which do not allow for services.

For instance, it would be good to be able to run the disk cleaning service manually, and see

the output on the screen.  The simple service presented earlier in this article also could be

run as a service, or normally.  This explains why the disk cleaning code was implemented

separate thread instead of directly in the service object: The thread can be started from the

service, or from the main form of the ’normal’ application.

Implementing a dual application is not so hard,  it involves some simple changes to the

project source code. To demonstrate it, a form (

TManualCleanForm

, in unit

frmManualClean

)

was added to the the diskclean application, which contains a simple memo and a start but-

ton. Pressing the start button will start the cleaning thread, and the progress of the cleaning

progress will be shown in the memo. The form in action is also visible in figure figure 2 on

page 19.

The main program code of the project file must be changed as follows:

begin

Installing:=IsInstalling;

if Installing or StartService then

begin

SvcMgr.Application.Initialize;

SvcMgr.Application.Title := ’Disk cleaning service’;

svcMgr.Application.CreateForm(TCleanService, CleanService);

svcMgr.Application.Run;

19

function StartService: Boolean;

begin

Result := FindCmdLineSwitch(’RUN’,[’-’,’/’], True);

end;

To show how it can be done using Borland’s default implementation, the last option is used

in the diskclean application.  It can be done quite simply using the

TServiceManager

class presented earlier:

function StartService: Boolean;

Var

Info : TServiceDescriptor;

Buf : Array[0..255] of char;

N : String;

Size : DWord;

F : TExt;

begin

Result := False;

Try

With TServiceManager.Create(Nil) do

try

Connected:=True;

QueryServiceConfig(SCleanService,Info);

If CompareText(Info.UserName,’LocalSystem’)=0 then

Info.UserName:=’System’;

finally

Connected:=False;

Free;

end;

Size:=256;

GetUserName(@Buf[0],Size);

N:=Buf;

Result:=CompareText(N, Info.UserName)=0;

Except

end;

end;

As can be seen, the servicemanager class is used to fetch the username with which the appli-

cation should run as a service. If the username is ’LocalSystem’ (the default) then the name

is changed to ’System’, as that will be the name that is reported by the

GetUserName

call

(a nice anomaly in itself). After this, the actual username is fetched using the

GetUserName

Windows API call, and the two values are compared.  If they match, the application will

start as a service.

10    Conclusion

Writing a complete service application is not hard to do.   Using Delphi it is even more

easily accomplished. Service applications can be used in a variety of tasks: the application

presented here has its use - downloaded files and documents tend to clutter harddisks after

some time - but can easily be changed to perform its task a little different - or to perform

other tasks as well.

21