Five Steps to Writing Windows Services in C

hen I had to write my first NT service, I went to MSDN and looked for samples. There, I found “Creating a Simple Win32 Service in C++, an article by Nigel Thompson with an example written in C++. Though the article explained the process quite well, I still felt I was missing important information. I wanted to understand what functions are called, by what framework, and when, but C++ didn’t make this easy. The object-oriented approach was convenient, but since calls for low-level Win32 functions are encapsulated by class, it is insufficient for learning about the essence of the Services. This is why I feel C is more suitable for the first steps in writing services or for implementing services that perform simple background tasks. C++ is worth using only after you gain a more thorough understanding of the subject. When I had to leave the place I worked and I had to transfer my knowledge to another person, it was very easy to explain the essence of NT services using my examples written in C.

A service is a console application that runs in the background and performs tasks that don’t require user interaction. The Windows NT/2000/XP operating systems offer special support for service programs. The installed services can be configured through the Services applet, available from the Control Panel in Windows NT or from Control Panel | Administrative Tools in Windows 2000/XP. Services can be configured to start automatically when operating system starts, so you dont have to start each of them manually after a system reboot.

This article explains how to create a service that periodically queries the amount of available physical memory and writes the result to a text file. The next sections guide you through the process of building, installing, and implementing the service.

Step 1: The main Function and Global Definitions
First, include the required header files. The program invokes Win32 functions (windows.h) and writes to file on disk (stdio.h):

#include #include 

Next, define two constants:

#define SLEEP_TIME 5000#define LOGFILE "C:\MyServices\memstatus.txt"

SLEEP_TIME specifies the time period in milliseconds between two consecutive queries for available memory. Use this constant in Step 2, when writing the worker loop of your service.LOGFILE defines the path to the log file for the memory queries results that you output using the WriteToLog function:

int WriteToLog(char* str){   FILE* log;   log = fopen(LOGFILE, "a+");   if (log == NULL)      return -1;   fprintf(log, "%s
", str);   fclose(log);   return 0;}

Declare several global variables to share their values across multiple functions of your program. Also, make the forward definitions of functions prototypes:

SERVICE_STATUS          ServiceStatus; SERVICE_STATUS_HANDLE   hStatus;  void  ServiceMain(int argc, char** argv); void  ControlHandler(DWORD request); int InitService();

Now, that the preparation work is done, you can start coding. Service programs are a subset of console applications. Therefore, you begin by defining the main function, which is the entry point to the program. In case of services, the code for main will be surprisingly short, since it just creates the dispatch table and starts the control dispatcher:

void main() {    SERVICE_TABLE_ENTRY ServiceTable[2];   ServiceTable[0].lpServiceName = "MemoryStatus";   ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;   ServiceTable[1].lpServiceName = NULL;   ServiceTable[1].lpServiceProc = NULL;   // Start the control dispatcher thread for our service   StartServiceCtrlDispatcher(ServiceTable);  }

A program may contain several services. Each of them must be listed in the special dispatch table (this program defines ServiceTable array for this purpose). Each entry in this table is in the SERVICE_TABLE_ENTRY structure, which has two fields:

  • lpServiceName: a pointer to a string with a service name; must be specified when multiple services are defined.
  • lpServiceProc: a pointer to a service main function (entry point of a service).

The last entry of the dispatch table must have NULL pointers for the service name and service main function fields. In this case, the program only hosts a single service, so defining the service name is optional.

The Services Control Manager (SCM) is the process that manages all the services in the system. When the SCM starts a service, it waits for the main thread of a process to call the StartServiceCtrlDispatcher function. Pass the dispatch table to StartServiceCtrlDispatcher. This transforms the main thread of the calling process into the control dispatcher. The dispatcher starts a new thread that runs the ServiceMain function of each service in the dispatch table (in this case, only one service). The dispatcher also monitors the execution of all the services in the program. The dispatcher then passes the control requests from the SCM to the service.

NOTE: If the StartServiceCtrlDispatcher function is not called for 30 seconds, an error is reported. To avoid this, initialize a service inside the ServiceMain function (as in the example) or in a separate thread rather than in the main function. The service described in this article does not require such precautions.

The StartServiceCtrlDispatcher call returns after all of the services in the dispatch table have finished executing (for example, after a user stops them from using the Service applet), or when an error occurs. The main process then terminates.

Step 2: The ServiceMain Function
Listing 1 shows the code for ServiceMain. This function is the entry point of a service. It runs in a separate thread, which is created by the control dispatcher. ServiceMain should register the control handler for a service as soon as possible. Do this by calling the RegisterServiceCtrlHadler function. You will pass two arguments to this function: the service name and the pointer to the ControlHandlerfunction.

This instructs the control dispatcher to invoke the ControlHandler function to handle SCM control requests. After registering the control handler, obtain the status handle (hStatus). Use hStatus to report the service’s status to the SCM by calling the SetServiceStatus function.

In Listing 1 shows how to initialize the ServiceStatus structure specifying the service characteristics and its current state. Each ServiceStatus structure fields has a purpose:

  • dwServiceType: indicates the type of service. Author Win32 service; assign the SERVICE_WIN32 value.
  • dwCurrentState: specifies the current state of the service. Since initialization of a service has not been finished at this point, set the SERVICE_START_PENDING status.
  • dwControlsAccepted: this field will inform the SCM which fields the service accepts. In this case, allow STOP and SHUTDOWN requests. Handling control requests is discussed in Step 3.
  • dwWin32ExitCode and dwServiceSpecificExitCode: these fields are useful when you are terminating the service and want to report the detailed exit code. Since, you will initialize the service and will not exit, assign 0 values.
  • dwCheckPoint and dwWaitHint: these fields indicate the progress of a service when it performs an initialization longer than 30 seconds . This service has a very short initialization procedure, so assign 0 values to both fields.

Report the service’s status to the SCM by calling the SetServiceStatus function. This supplies the hStatus handle and the ServiceStatus structure. Notice that ServiceStatus is global, so you can use it across multiple functions. In the ServiceMain function, you assign values to several structure fields that will remain unchanged during the whole run of service, such as dwServiceType.

After reporting the status of the service, you can proceed to the initialization. Do this by calling the InitService function. This function simply adds the Monitoring started. string to the log file. Here is the code:

// Service initializationint InitService() {    int result;   result = WriteToLog("Monitoring started.");   return(result); }

In ServiceMain, test the return value of the InitService function. If there was an initialization error (in this case, it may happen if writing to the log failed), set the service status to stopped and exit the ServiceMain:

error = InitService(); if (error) {   // Initialization failed; we stop the service   ServiceStatus.dwCurrentState = SERVICE_STOPPED;    ServiceStatus.dwWin32ExitCode = -1;    SetServiceStatus(hStatus, &ServiceStatus);    // exit ServiceMain   return; }

If initialization was successful, report the running status to the SCM:

   // We report the running status to SCM.    ServiceStatus.dwCurrentState =       SERVICE_RUNNING;    SetServiceStatus (hStatus, &ServiceStatus);

Next, start the worker loop. Every five seconds, query the available physical memory and write the result to the log file.

As seen in Listing 1, the loop continues until the current state of the service is SERVICE_RUNNING or until an error writing to the log file occurs. The state may be altered by the ControlHandler function in response to a SCM control request.

Step 3: Handling Control Requests
In Step 2, you used the ServiceMain function to register the control handler function. The control handler is very similar to the window callback function that processes various Windows messages. It checks what request was sent by the SCM and acts accordingly.

Each time you call the SetServiceStatus function, you must specify that the service accepts the STOP and SHUTDOWN requests. Listing 2 shows how to handle them in the ControlHandler function.

The STOP request is sent when the SCM stops the service. For example, if the user manually stops the service from the Services applet, the SHUTDOWN request is issued to all running services by the SCM, when the machine shuts down. Both cases are handled identically:

  • Writing to the log file Monitoring is stopped.
  • The SERVICE_STOPPED state is reported to the SCM.

Since the ServiceStatus structure is global to the whole program, the worker loop in ServiceMain stops after the current state changes and the service thread terminates. There are other control requests, such as PAUSE and CONTINUE, that are not handled in this example.

The control handler function must report the service status, even if that status remains the same, every time the SCM sends a control request. Hence, SetServiceStatus is called in response to all requests.

Figure 1: The Services applet displaying the MemoryStatus service.

Step 4: Installing and Configuring a Service
The program is ready and may be compiled into an exe file. I created MemoryStatus.exe and copied it into C:MyServices folder on my computer. To install this service on your operating system, you’ll need to use the SC.EXE executable, which comes with the Win32 Platform SDK tools. You will use this utility to install and remove the service. The other control operations will be done through the Services applet.

Here is the command-line to install your MemoryStatus service:

sc create MemoryStatus binpath= c:MyServicesMemoryStatus.exe

Issue the create command. This specifies the service name and path to the binary file (notice the space between binpath= and the path). You can now use the Services applet to control the service (see Figure 1).

Start and stop the service using the toolbar of the applet.

Figure 2: The Properties window for the MemoryStatus service

The startup type of MemoryStatus is manual, which means that it will start only on-demand. Right-click on the service and choose Properties from the context menu, to get the properties window for the service (Figure 2). This is where you can alter the startup type along with other settings. You can also start/stop the service from the General tab.

To remove the service from the system, execute the following command:

sc delete MemoryStatus

Specify the delete option and the service name. The service will be marked for deletion and will be completely removed after the next restart.

Step 5: Testing Your Service
Start the MemoryStatus service from the Services applet. If there are no initialization errors, the execution should start successfully. Wait for a while, then stop the service. Examine the service’s output in the memstatus.txt file under the C:MyServices folder. On my computer, I obtained the following:

Monitoring started.273469440273379328273133568273084416Monitoring stopped.

To test the behavior of the MemoryStatus service in case of errors, you can make the memstatus.txt file read-only. In this setting, the service will not start.

Remove the read-only attribute, start the service, and reapply read-only. The service should stop its execution because writing to the log fails. If you refresh the contents of the Services applet, you will see that service status is stopped.

On to Bigger and Better Servcies
Understanding the basic concepts of Win32 services, allows you to better use C++ for designing a wrapper class. A wrapper class hides the calls to the low-level Win32 functions and provides a comfortable generic interface. Alter the MemoryStatus program code to create your own services that fit your needs! To perform more complicated tasks than one demonstrated in this article, you can create multithreaded services to divide the job between several worker threads and monitor their execution from the ServiceMain function.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Related Posts