How To: Creating Windows NT Services

Posted: (EET/GMT+2)

 

How To: Creating Windows NT Services

Level: NT, WinSock Advanced


When creating special applications, it is often necessary to have them running all the time. On a workstation, this usually not a problem, because you can for example put the app to your StartUp group. But, on a server, especially on a remote one, this is more difficult. Of course, you could create a normal app, and keep it running by logging in, and then locking the computer. However, this is inconvinient, and besides a security risk. The solution to these problems is a service, which do only run on Windows NT, not on 95.


What are services?

Shortly said services are normal executable files, which keep running even if nobody is logged on to the computer. Services are controlled through the Control Panel's Services applet, where you can start and stop individual services, as well as change other settings.

Technically services need only call a few Win32 Service API functions, and conform to certain rules, which are simple enough. The service can then do almous whatever it wants, for example silently wait for Internet connections, like our example service does. Note that services are almost never interactive -- for example all interaction with the desktop is prohibited by default.


The three service functions in theory

To succesfully create a service, three routines must be made available in the executable. This first is the entry point, which is the main block of the code. In Delphi apps, this is the code seen at the bottom of the file when viewing the project source.

Secondly, a ServiceMain function must be written, although you can name this just the way you want, of course. Nonetheless, this function is the actualy work horse of the service, and it is always called in the context of a different thread than the two other functions. Thus, you have to make sure you don't improperly mess with global variables etc.

Thirly, a control handler function must be exposed. This function handles the messages sent by the Service Control Manager (SCM) application, which usually is the Control Panel Services applet. This function gets called when the user for example requests to stop your service.


The three service functions in practice

Lets start with the main function. Below you can see the code used, without any debugging stuff:

    Var
      STEs : Array[1..2] of TServiceTableEntry;
    
    begin { entry point -- the "main" function }
      With STEs[1] do Begin
        lpServiceName := 'MySampleServ';
        lpServiceProc := @MyServiceProc;
      End;
      With STEs[2] do Begin
        lpServiceName := nil;
        lpServiceProc := nil;
      End;
      Check(StartServiceCtrlDispatcher(STEs[1]),'StartServiceCtrlDispatcher');
    end.
    
Here, we must fill two service table entries. These entries tell which services this executable exposes. Our demo exposes only a single service, but could expose more. After filling the entries, the StartServiceCtrlDispatcher API function is called to inform the SCM what services this EXE has. This must be done immediately without any non-necessary delays.

As you recall, the second function is the ServiceMain function, which is called MyServiceProc in our example service. The code looks like this:

    Procedure MyServiceProc(ArgCount : Integer; Args : PPCharArray); StdCall;
    Var
      Status : TServiceStatus;
      WSData : TWSAData;
    
    Begin
      { Register the CtrlHandler procedure }
      StatusHandle := RegisterServiceCtrlHandler('MySampleServ',@MyCtrlHandler);
      { Startup - make it quick }
      WSAStartup($0101,WSData);
      With Status do Begin
        FillChar(Status,SizeOf(Status),0);
        dwServiceType := Service_Win32_Own_Process;
        dwCurrentState := Service_Running;
        dwControlsAccepted := Service_Accept_Stop;
      End;
      { Report status to Service Control Manager (SCM) }
      Check(SetServiceStatus(StatusHandle,Status),'SetServiceStatus MyServiceProc');
      { Do our work... }
      Repeat
        CreateListenSocket;
        WaitForConnection;
        HandleConnection;
      Until False;
    End;
    
Here, we must first register the third function exposed, that is the CtrlHandler. Then, we must report to the SCM, that our service has started, and that we are ready to run. Note that according to specs, we should report immediately after registering the CtrlHandler, that we have a start operation pending. However, WSAStartup won't take that long, so I've omitted the call. Also, to keep things simple, our service only accepts the Stop command. For example, one could expand the list to Pause and Resume, but often those are very similar to start/stop, and thus not worth implementing here.

BTW, the sample service opens the TCP port 620. When a connection arrives, it displays the disk space on all available disk drives, and closes the connection. After installing the service on your computer, run the command "telnet localhost 620" and see what happens. I won't describe the WinSock part here -- that's why I've leveled this How To as "WinSock Advanced".


The Control Handler

The code for our Control Handler (named MyCtrlHandler) is:

    Procedure MyCtrlHandler(OpCode : Integer); StdCall;
    Var Status : TServiceStatus;
    Begin
      Case OpCode of
        Service_Control_Stop        : Begin
                                        { Do operations required to stop here }
                                        With Status do Begin
                                          FillChar(Status,SizeOf(Status),0);
                                          dwServiceType := Service_Win32_Own_Process;
                                          dwCurrentState := Service_Stopped;
                                          dwControlsAccepted := Service_Accept_Stop;
                                        End;
                                        { Report status to Service Control Manager (SCM) }
                                        Check(SetServiceStatus(StatusHandle,Status),'SetServiceStatus Stop');
                                      End;
        Service_Control_Interrogate : Begin { report our current status }
                                        With Status do Begin
                                          FillChar(Status,SizeOf(Status),0);
                                          dwServiceType := Service_Win32_Own_Process;
                                          dwCurrentState := Service_Running;
                                          dwControlsAccepted := Service_Accept_Stop;
                                        End;
                                        Check(SetServiceStatus(StatusHandle,Status),'SetServiceStatus Intr.');
                                      End;
        End;
      End;
    End;
    
The first and most important message ("opcode") we handle is the Stop message. After receiving this message, we must stop executing. In our sample service, we won't do anything to clean things up, because the worker thread is simply terminated. Of course, this leaves WinSock handles open, but NT is robust enough to take care of them.

The Interrogate message is only used to report our current state to the SCM. We return always the "running" state, but for more complex services (for example those accepting the pause command) you would need to take additional steps here.


Registering and running the sample service

After you have studied and successfully compiled the sample service, you need to install it. This requires updating the Windows Registry. The following is the Registry Editor code export of the registry keys required for operation.

    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MySampleServ]
    "Type"=dword:00000020
    "Start"=dword:00000003
    "ErrorControl"=dword:00000000
    "ImagePath"=hex(2):63,3a,5c,74,65,6d,70,5c,6e,74,73,65,72,76,63,65,2e,65,78,65,\
      00
    "DisplayName"="My Sample Disk Space Service"
    "DependOnService"=hex(7):52,50,43,53,53,00,4e,54,4c,4d,53,53,50,00,00
    "DependOnGroup"=hex(7):00
    "ObjectName"="LocalSystem"
    
As you can see, services are always registered at the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services key. When setting the registry, first launch RegEdit. Then, choose Import from the Registry menu, and import the "ntservce.reg" file. Then browse to the key, and update the image path to point to your location of the service. Don't change other parameters -- that is unnecessary and risky, if you don't know what you are doing.

After importing the registry, reboot your machine. This is necessary because the Service settings are only read from the registry when the system starts.

After restarting you computer, fire up Control Panel, and the double-click the Services icon. If you updated the registry correctly, you will see the "My Sample Disk Space Service" listed. Click the Start button. If everything is OK, the "Started" message appears next to the name. If you compiled the code with the DEBUG compiler directive, "type" the file "c:\ntservce.dbg" to see if everything is OK.

If everything appears running smoothly, try the ultimate "telnet localhost 620" test. If this passes, the service works! Next, you can simply stop the service, remove the registry keys and reboot. Then your system is back on its original state.

Conclusion

Having read this How To, you should have at least somekind of idea how services work on a Windows NT system. Of course, to keep this text short, I've omitted much information, but as always, the Win32 documentation is your friend. Look at the Help topics for the functions/data structures used, and you should quickly learn how the code works. After that, let your imagination fly, hack a few services, and add NT services to your CV!