Win32 Programming Features for Windows NT and Windows 95 by Marshall Brain

Services

Every operating system needs a way to execute background tasks that run continuously regardless of who is using the machine. These background tasks can perform various services important to the system or its users. For example, a messaging system might monitor the network and display a dialog box whenever it receives a message from another machine. An application that sends and receives faxes needs to start up at boot time and then continuously monitor the fax modem for fax machines dialing in. A home or office security program, or code that controls a piece of test equipment, may need to poll sensors periodically and respond to them when appropriate. All of these tasks require CPU time to perform their jobs, but should not affect a user working at the keyboard because they require so little of the total CPU power available.

In MS-DOS, background tasks like these are handled by Terminate and Stay Resident (TSR) programs. These programs are started in the autoexec.bat file. In UNIX, background tasks are handled by Daemons. At the very end of a UNIX machine's boot process you can see the operating system start up things like the Cron and Finger daemons before the system lets the first user log in. In Windows NT, background tasks are called services. Services start automatically when NT boots and remain running in the background regardless of who is logged in.

Windows NT services are implemented as normal executables, but follow a very specific protocol internally that allows them to interact properly with the Service Control Manager (SCM). In this article, you will learn how to create and install simple Win32 services in Windows NT. Once you understand simple services, it is easy to build your own because all services, no matter how complicated, must contain the same basic SCM interface code. Once the requirements of the SCM are met however, there is no real difference between the executable for a service and a regular program.

A good working knowledge of NT services is important to both programmers and system administrators. Programmers obviously benefit because they can create their own services. The benefit to administrators is more subtle, but equally important. Background tasks, in general, can be dangerous. Both MS-DOS and Macintosh systems make such good viral hosts because, through their lack of security, they allow any person or program to create background tasks at any time. Windows NT and UNIX systems are secure, so only an administrator can add background tasks to the system. However, if the administrator adds a destructive background task, then it is free to do its damage. When administrators understand the mechanisms and privileges available to Windows NT services, it is possible for them to be more selective in installing potentially harmful background tasks.

Basic Concepts

Services come in two different varieties. Driver services use device driver protocols to interface NT to specific pieces of hardware. Win32 services, on the other hand, implement general background tasks using the normal Win32 API. This article focuses on Win32 services because of their general utility and ease of creation. Any NT programmer with the normal NT SDK (or Visual C++) and administrative access to an NT machine can implement and install his or her own Win32 services. Any time that you want to create any type of program that starts at boot time and runs continuously as a background task in Windows NT, you will want to use a Win32 service.

Services expose themselves in NT's user interface through the Control Panel. There you will find a Services Applet that displays a list of all available Win32 services. This applet lets you start, stop, pause and resume services. A second dialog, accessed by pressing the Startup button in the Services Applet, lets you change the startup behavior as well as the default account used by the service. A service can start automatically at boot time, it can be totally disabled, or it can be set to start manually. When starting a service manually, a user can supply startup parameters. You need to be logged in as the administrator or a power user to do anything with the Services applet.

Windows NT ships with a number of pre-installed services that handle such things as network messaging, command scheduling with the "at" command, and distributed RPC naming. When you create your own services, you must perform a separate installation step to insert them into the list managed by the services applet. The installation process adds information about a new service--its name, the name of its executable, its startup type, etc.--into the registry so that the SCM knows about the new service the next time the machine boots.

Creating a New Service

A program that acts as a service is a normal EXE file, but it must meet special requirements so that it interfaces properly with the SCM. Microsoft has carefully choreographed the flow of function calls, and you must follow that plan closely or the service will not work. The requirements are listed below. You will find descriptions of the functions discussed here in the Win32 programmer reference manuals, or in the on-line help files for Win32 in the SDK or Visual C++:

  • The service's code must have a normal main or WinMain function. This function should immediately call the StartServiceCrtlDispatcher function. By calling this function, you give the SCM a pointer to a ServiceMain function to call when it wants to start the service.
  • The SCM calls the ServiceMain function when it wants to start the service. For example, if the administrator presses the Start button in the Services applet, then the SCM will execute the ServiceMain function in a separate thread. ServiceMain should call the RegisterServiceCtrlHandler function, which registers a Handler function with the SCM for it to call with control requests. You can name the Handler function anything you like, but it is listed in the documentation under Handler. The RegisterServiceCtrlHandler function returns a handle that the service can use when it wants to send status messages to the SCM.
  • The ServiceMain function must also start the thread that does the actual work of the service itself. The ServiceMain function should not return until it is time for the service to stop. When it returns, the service has stopped.
  • The Handler function contains a switch statement that parses control requests received from the SCM. By default the SCM can send any of the following control constants:
    • SERVICE_CONTROL_STOP - Tells the service to stop.
    • SERVICE_CONTROL_PAUSE - Tells the service to pause.
    • SERVICE_CONTROL_CONTINUE - Tells the service to resume.
    • SERVICE_CONTROL_INTERROGATE - Tells the service to immediately report its status.
    • SERVICE_CONTROL_SHUTDOWN - Tells the service that shutdown is imminent.

It is also possible to create custom constants (with values between 128 and 255) and send them through the SCM to the service.

When you create an EXE that contains the main, ServiceMain, and Handler functions described above, as well as a function that contains the thread for the service itself, you have a complete service. The figure below summarizes the interactions between these different functions and the SCM:

{width=”432” height=”456”}

Listing 1 shows the simplest service possible. This service simply beeps. By default it beeps every two seconds. Optionally you can modify the beep interval with startup parameters. This service is complete in that it will appropriately respond to the SCM for every control signal possible. Because of that, this program can act as a good template for creating your own services.

The main function calls StartServiceCtrlDispatcher to register the ServiceMain function. The registration is performed using an array of SERVICE_TABLE_ENTRY structures. In this case, the program contains just one service, so there is only one entry in the table. However, it is possible for there to be several services in a single EXE file, and in that case the table identifies the appropriate ServiceMain function for each. It is possible to put initialization code in the main function prior to the call to StartServiceCtrlDispatcher, but this code must complete in less than 30 seconds. If it takes longer, the SCM aborts the service on the assumption that something went wrong.

The ServiceMain function gets called when the SCM wants to start the service either during the boot process or because of a manual start. ServiceMain always contains the following steps:

  1. It immediately calls RegisterServiceCtrlHandler to register the Handler function with the SCM as this service's Handler function.
  2. It then calls the SendStatusToSCM function. It does this to notify the SCM of progress. The fourth parameter is a "click count" value, which increments each time the program updates the status. The SCM and other programs can look at the click count and see that progress is being made during initialization. The last parameter is a "wait hint", which tells the SCM how long (in milliseconds) it should expect to wait before the click count next gets updated.
  3. ServiceMain next creates an event that will be used at the bottom of the function to prevent it from returning until the SCM issues a STOP request.
  4. Next, ServiceMain checks for startup parameters. These parameters can be passed in by the user during a manual start, using the Startup Parameters line in the Service applet. Any parameters come into the ServiceMain function in an argv-style array.
  5. If your service needs to perform other initialization tasks, they should go here just prior to the call to InitService.
  6. The ServiceMain function next calls the InitService function, which will start the thread that does the actual work of the service. If this call succeeds, then ServiceMain lets the SCM know that the service has successfully started.
  7. ServiceMain now calls WaitForSingleObject, which waits efficiently for the terminateEvent event object to be set. This object gets set in the Handler function. Once it does get set, ServiceMain calls the terminate function to clean up, and then returns to stop the service.

As you can see, there is not a lot of flexibility in this function. With the exception of step 5, you must perform each of the tasks mentioned in the order mentioned for the service to start properly.

The terminate function cleans up any open handles, and sends a status message to the SCM to tell it that the service is stopped.

The SCM calls the Handler function whenever it wants to pause, resume, interrogate or stop the service. To stop the service, the handler sets terminateEvent. By doing this, it causes ServiceMain, which is executing as a separate thread, to terminate and return. Once ServiceMain returns the service is stopped.

The SendStatusToSCM function consolidates all of the statements necessary to send the service's current status to the SCM.

The InitService function gets called by ServiceMain when it needs to start the service's thread. This function calls CreateThread to create a new thread for the service.

The ServiceThread function contains the actual work that is to be performed by the service. In this case, the thread consists of an infinite loop that beeps and then sleeps for a pre-determined interval. When creating your own services, you can place any code that you like in this thread, calling either Win32 functions or your own functions.

Installing and Removing Services

In order to use the beep service described in the previous section, you have to install it. Installation makes the SCM aware of the service, and causes the SCM to add it to the list of services that appears in the Services applet of the Control Panel. The code shown in Listing 2 demonstrates how to install a service.

Listing 2 starts by opening a connection to the SCM using the OpenSCManager function. In the call to OpenSCManager, you must specify what you want to do so that the SCM can validate that activity. If the account you are logged in under does not have sufficient privilege, then the call will return NULL.

The call to CreateService actually installs the new service. It uses the pointer to the SCM returned by OpenSCManager, the name, label and EXE file specified on the command line, along with a set of standard parameters to fill in all of the other values. The use of SERVICE_WIN32_OWN_PROCESS indicates that the service's EXE file contains just one service, and SERVICE_DEMAND_START indicates that the service is manual start rather than automatic start. A typical invocation of the install program at the command line is shown below:

       install BeepService "Beeper" c:\winnt\beep.exe

The first parameter specifies the name of the service used internally by the SCM. This name is later used to remove the service. The second parameter specifies the label used to display the service in the Services applet. The third parameter gives the fully qualified path to the service's executable. After you install the service, start it using the Services applet in the Control Panel. Look up any error codes in the on-line help file for the Win32 API.

To remove a service, you follow the steps shown in Listing 3. It starts by opening a connection to the SCM, and then opens a connection to the service using the OpenService function. Listing 3 next queries the service to find out if it is currently stopped. If it is not, it stops it. The DeleteService function removes the service from the Services applet in the Control Panel. A typical invocation of the removal program shown in Listing 3 would look like this:

       remove BeepService

If desired, you can immediately reinstall the service.

Conclusion

Services are an essential part of Windows NT because they allow you to extend the operating system. Using the code in Listing 1 as a template, you will find that it is extremely easy to create new services of your own.


Listing 1

Code that implements the simplest possible NT service

    //***************************************************************
    // From the book "Win32 System Services: The Heart of Windows NT"
    // by Marshall Brain
    // Published by Prentice Hall
    //
    // This code implements the simplest possible service.
    // It beeps every 2 seconds, or at a user specified interval.
    //***************************************************************
     
    // beepserv.cpp
     
    #include <windows.h> 
    #include <stdio.h> 
    #include <iostream.h> 
    #include <stdlib.h> 
     
    #define DEFAULT_BEEP_DELAY 2000
     
    // Global variables
     
    // The name of the service
    char *SERVICE_NAME = "BeepService";
    // Event used to hold ServiceMain from completing
    HANDLE terminateEvent = NULL;
    // Handle used to communicate status info with
    // the SCM. Created by RegisterServiceCtrlHandler
    SERVICE_STATUS_HANDLE serviceStatusHandle;
    // The beep interval in ms.
    int beepDelay = DEFAULT_BEEP_DELAY;
    // Flags holding current state of service
    BOOL pauseService = FALSE;
    BOOL runningService = FALSE;
    // Thread for the actual work
    HANDLE threadHandle = 0;
     
    void ErrorHandler(char *s, DWORD err)
    {
       cout <<  s <<  endl;
       cout <<  "Error number: " <<  err <<  endl;
       ExitProcess(err);
    }
     
    // This function consolidates the activities of 
    // updating the service status with
    // SetServiceStatus
    BOOL SendStatusToSCM (DWORD dwCurrentState,
       DWORD dwWin32ExitCode, 
       DWORD dwServiceSpecificExitCode,
       DWORD dwCheckPoint,
       DWORD dwWaitHint)
    {
       BOOL success;
       SERVICE_STATUS serviceStatus;
     
       // Fill in all of the SERVICE_STATUS fields
       serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
       serviceStatus.dwCurrentState = dwCurrentState;
     
       // If in the process of doing something, then accept
       // no control events, else accept anything
       if (dwCurrentState == SERVICE_START_PENDING)
           serviceStatus.dwControlsAccepted = 0;
       else
           serviceStatus.dwControlsAccepted = 
               SERVICE_ACCEPT_STOP |
               SERVICE_ACCEPT_PAUSE_CONTINUE |
               SERVICE_ACCEPT_SHUTDOWN;
     
       // if a specific exit code is defined, set up
       // the win32 exit code properly
       if (dwServiceSpecificExitCode == 0)
           serviceStatus.dwWin32ExitCode = dwWin32ExitCode;
       else
           serviceStatus.dwWin32ExitCode = 
               ERROR_SERVICE_SPECIFIC_ERROR;
       serviceStatus.dwServiceSpecificExitCode =
           dwServiceSpecificExitCode;
     
       serviceStatus.dwCheckPoint = dwCheckPoint;
       serviceStatus.dwWaitHint = dwWaitHint;
     
       // Pass the status record to the SCM
       success = SetServiceStatus (serviceStatusHandle, 
           &serviceStatus);
       return success;
    }
     
    DWORD ServiceThread(LPDWORD param)
    {
       while (1)
       {
           Beep(200,200);
           Sleep(beepDelay);
       }
       return 0;
    }
     
    // Initializes the service by starting its thread
    BOOL InitService()
    {
       DWORD id;
     
       // Start the service's thread
       threadHandle = CreateThread(0, 0,
           (LPTHREAD_START_ROUTINE) ServiceThread,
           0, 0, &id);
     
       if (threadHandle==0)
           return FALSE;
       else
       {
           runningService = TRUE;
           return TRUE;
       }
    }
     
    // Dispatches events received from the SCM
    VOID Handler (DWORD controlCode) 
    {
       DWORD currentState = 0;
       BOOL success;
     
       switch(controlCode)
       {
           // There is no START option because
           // ServiceMain gets called on a start
     
           // Stop the service
           case SERVICE_CONTROL_STOP:
               // Tell the SCM what's happening
               success = SendStatusToSCM(SERVICE_STOP_PENDING,
                   NO_ERROR, 0, 1, 5000);
               runningService=FALSE;
               // Set the event that is holding ServiceMain
               // so that ServiceMain can return
               SetEvent(terminateEvent);
               return;
     
           // Pause the service
           case SERVICE_CONTROL_PAUSE:
               if (runningService && !pauseService)
               {
                   // Tell the SCM what's happening
                   success = SendStatusToSCM(
                       SERVICE_PAUSE_PENDING,
                       NO_ERROR, 0, 1, 1000);
                   pauseService = TRUE;
                   SuspendThread(threadHandle);
                   currentState = SERVICE_PAUSED;
               }
               break;
     
           // Resume from a pause
           case SERVICE_CONTROL_CONTINUE:
               if (runningService && pauseService)
               {
                   // Tell the SCM what's happening
                   success = SendStatusToSCM(
                       SERVICE_CONTINUE_PENDING,
                       NO_ERROR, 0, 1, 1000);
                   pauseService=FALSE;
                   ResumeThread(threadHandle);
                   currentState = SERVICE_RUNNING;
               }
               break;
     
           // Update current status
           case SERVICE_CONTROL_INTERROGATE:
               // it will fall to bottom and send status
               break;
     
           // Do nothing in a shutdown. Could do cleanup
           // here but it must be very quick.
           case SERVICE_CONTROL_SHUTDOWN:
               return;
     
           default:
               break;
       }
       SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);
    }
     
    // Handle an error from ServiceMain by cleaning up
    // and telling SCM that the service didn't start.
    VOID terminate(DWORD error)
    {
       // if terminateEvent has been created, close it.
       if (terminateEvent) CloseHandle(terminateEvent);
     
       // Send a message to the scm to tell about stopage
       if (serviceStatusHandle)
           SendStatusToSCM(SERVICE_STOPPED, error,
               0, 0, 0);
     
       // If the thread has started, kill it off
       if (threadHandle) CloseHandle(threadHandle);
     
       // Do not need to close serviceStatusHandle
    }
     
    // ServiceMain is called when the SCM wants to
    // start the service. When it returns, the service
    // has stopped. It therefore waits on an event
    // just before the end of the function, and
    // that event gets set when it is time to stop. 
    // It also returns on any error because the
    // service cannot start if there is an eror.
    VOID ServiceMain(DWORD argc, LPTSTR *argv) 
    {
       BOOL success;
     
       // immediately call Registration function
       serviceStatusHandle =
           RegisterServiceCtrlHandler(
               SERVICE_NAME, (LPHANDLER_FUNCTION)Handler);
       if (!serviceStatusHandle) {terminate(GetLastError()); return;}
     
       // Notify SCM of progress
       success = SendStatusToSCM(SERVICE_START_PENDING,
           NO_ERROR, 0, 1, 5000);
       if (!success) {terminate(GetLastError()); return;}
     
       // create the termination event
       terminateEvent = CreateEvent (0, TRUE, FALSE, 0);
       if (!terminateEvent) {terminate(GetLastError()); return;}
     
       // Notify SCM of progress
       success = SendStatusToSCM(SERVICE_START_PENDING,
           NO_ERROR, 0, 2, 1000);
       if (!success) {terminate(GetLastError()); return;}
     
       // Check for startup params
       if (argc == 2)
       {
           int temp = atoi(argv[1]);
           if (temp < 1000)
               beepDelay = DEFAULT_BEEP_DELAY;
           else
               beepDelay = temp;
       }
     
       // Notify SCM of progress
       success = SendStatusToSCM(SERVICE_START_PENDING,
           NO_ERROR, 0, 3, 5000);
       if (!success) {terminate(GetLastError()); return;}
     
       // Start the service itself
       success = InitService();
       if (!success) {terminate(GetLastError()); return;}
     
       // The service is now running. 
       // Notify SCM of progress
       success = SendStatusToSCM(SERVICE_RUNNING,
           NO_ERROR, 0, 0, 0);
       if (!success) {terminate(GetLastError()); return;}
     
       // Wait for stop signal, and then terminate
       WaitForSingleObject (terminateEvent, INFINITE);
     
       terminate(0);
    }
     
    VOID main(VOID)
    {
       SERVICE_TABLE_ENTRY serviceTable[] = 
       { 
       { SERVICE_NAME,
           (LPSERVICE_MAIN_FUNCTION) ServiceMain},
       { NULL, NULL }
       };
       BOOL success;
     
       // Register with the SCM
       success = 
           StartServiceCtrlDispatcher(serviceTable);
       if (!success)
           ErrorHandler("In StartServiceCtrlDispatcher",
               GetLastError());
    }

Listing 2

Code that installs an NT service

    //***************************************************************
    // From the book "Win32 System Services: The Heart of Windows NT"
    // by Marshall Brain
    // Published by Prentice Hall
    //
    // This code installs a service.
    //***************************************************************
     
    // install.cpp
     
    #include <windows.h> 
    #include <iostream.h> 
     
    void ErrorHandler(char *s, DWORD err)
    {
       cout <<  s <<  endl;
       cout <<  "Error number: " <<  err <<  endl;
       ExitProcess(err);
    }
     
    void main(int argc, char *argv[])
    {
       SC_HANDLE newService, scm;
     
       if (argc != 4)
       {
           cout <<  "Usage:\n";
           cout <<  "   install service_name \
    service_label executable\n";
           cout <<  "           service_name is the \
    name used internally by the SCM\n";
           cout <<  "           service_label is the \
    name that appears in the Services applet\n";
           cout <<  "              (for multiple \
    words, put them in double quotes)\n";
           cout <<  "           executable is the \
    full path to the EXE\n\n";
           return;
       }
     
       // open a connection to the SCM
       scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);
       if (!scm) ErrorHandler("In OpenScManager",
           GetLastError());
     
       // Install the new service
       newService = CreateService(
           scm, argv[1], // eg "beep_srv"
           argv[2],      // eg "Beep Service"
           SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
           SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
           argv[3],      // eg "c:\winnt\xxx.exe"
           0, 0, 0, 0, 0);
       if (!newService) ErrorHandler("In CreateService",
           GetLastError());
       else cout <<  "Service installed\n";
     
       // clean up
       CloseServiceHandle(newService);
       CloseServiceHandle(scm);
    }

------------------------------------------------------------------------

### Listing 3

Code that removes an NT service

```c
    //***************************************************************
    // From the book "Win32 System Services: The Heart of Windows NT"
    // by Marshall Brain
    // Published by Prentice Hall
    //
    // This code removes a service from the Services applet in the
    // Control Panel.
    //***************************************************************
     
    // remove.cpp
     
    #include <windows.h> 
    #include <iostream.h> 
     
    void ErrorHandler(char *s, DWORD err)
    {  
       cout << s <<  endl;
       cout <<  "Error number: " <<  err <<  endl;
       ExitProcess(err); 
    }
     
    void main(int argc, char *argv[])
    {
       SC_HANDLE service, scm;
       BOOL success;
       SERVICE_STATUS status;
     
       if (argc != 2)
       {
           cout <<  "Usage:\n   remove service_name\n";
           return;
       }
       
       // Open a connection to the SCM
       scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);
       if (!scm) ErrorHandler("In OpenScManager",
           GetLastError());
     
       // Get the service's handle
       service = OpenService(scm, argv[1],
           SERVICE_ALL_ACCESS | DELETE);
       if (!service) ErrorHandler("In OpenService",
           GetLastError());
       
       // Stop the service if necessary
       success = QueryServiceStatus(service, &status);
       if (!success) ErrorHandler("In QueryServiceStatus",
           GetLastError());
       if (status.dwCurrentState != SERVICE_STOPPED)
       {
           cout <<  "Stopping service...\n";
           success = ControlService(service,
               SERVICE_CONTROL_STOP, &status);
           if (!success) ErrorHandler("In ControlService",
               GetLastError());
       }
       
       // Remove the service
       success = DeleteService(service);
       if (success) cout <<  "Service removed\n";
       else ErrorHandler("In DeleteService",
           GetLastError());
     
       // Clean up
       CloseServiceHandle(service);
       CloseServiceHandle(scm);
    }