In this article, I introduce the concept of a service in the Win32 environment. I provide and discuss a C++ class, NTService, that encapsulates the basics of building an NT service. This class allows you to concentrate on only the application-specific needs of a service application, and not on the mechanics of creating a service.
About NT Services
Windows NT supports an application type known as a service. A Win32 service is conceptually similar to a Unix daemon. You can use services to enhance or add features to the operating system. One example of a Windows NT service is the Windows print spooler. The Service Control Manager (SCM) starts, stops, and controls services, which can be started either automatically at system boot time or manually by the user.
In NT, you can access the SCM user interface by selecting the Services icon from the Windows Control Panel. All services under NT must conform to the interface requirements of the SCM. A service must be installed into the SCM database before the SCM will recognize and register the service. Services have three function entry points: main, ServiceMain, and ServiceControl (sometimes called the ControlHandler). main registers the service with the SCM. Once the service is registered, the SCM starts the service by calling ServiceMain. ServiceMain must register the control handler so the SCM can retrieve status information and send control requests.
The NTService Class
I created the NTService class because I needed the ability to quickly create NT service programs, and to provide programmers with a way to create service programs without having to learn the details about how NT services operate within the Windows environment. I developed my NT service programs using Visual C++ v5.0.
To create the project, use File | New and the project tab to create a new Win32 console application. Since services can be started before any users are logged on to the system, user interface issues are immaterial. Therefore, I recommend avoiding MFC and other GUI libraries when possible to keep the service code small.
The NTService class provides all of the core functionality needed for a Windows NT service. ntservice.h (Listing 1) is the header file for the NTService class, and ntservice.cpp (Listing 2) is the C++ implementation file. The NTService class is a generic base class which cannot be instantiated by itself because of the pure virtual function Run. The NTService class also provides ten protected functions that are designed to be optionally overridden by the programmer in the child class. A new service program is created by deriving a child class that provides a Run member function from the NTService class. Run is a protected member function that performs only the actions needed to deliver the desired service capability.
The NTService class handles ServiceMain registration, the ServiceControl entry points, and any other generic service chores. It also provides methods to install and uninstall the service from the SCM.
The main Function
Like traditional C++ programs, but unlike most Windows programs, a service uses main instead of WinMain. To start the service, the SCM enters main, which in turn must register the service and identify its primary entry point, ServiceMain. ServiceMain must first register the entry point for ServiceControl. The SCM uses the service control function to monitor the status of the service and to suspend, resume, or stop the service. Listing 3 shows a portion of main.cpp.
For most applications, you'll need to make only two changes to main.cpp. Change the value of the SERVICE_NAME constant to the name of the service you're creating, and invoke the constructor of the derived class in the call to new. main.cpp consists of three functions: ServiceMain, ServiceControl, and main. main first creates an instance of the MyService class and assigns it to the global pointer ntService. The constructor for the NTService class takes three arguments: the service name, ServiceMain's address, and ServiceControl's address. These two functions provide the SCM with its interface to the derived service class. ServiceMain and ServiceControl must conform to the interface required by the SCM. They also must have access to the instance of the service class's ntService that was created in main. This is why they were implemented as globally visible functions. They could have been static member functions of the NTService class, but I felt I would have more flexibility to add multiple service support later if I implemented it this way.
main parses the command-line arguments and performs one of three actions. main will install or uninstall the service if a -i or a -u flag appear in the command-line argument, respectively. Install and Uninstall are member functions of NTService. If no arguments are present, then main assumes it is being called by the SCM and calls ntService->Startup.
Starting and Running the Service
An NT service is started by calling the Windows StartServiceCtrlDispatcher and passing it a dispatch table with the service name and the address of its ServiceMain. The NTService class does this in its Startup member function. The SCM then immediately calls the ServiceMain that was provided in the dispatch table (see Listing 1). My ServiceMain just calls the ntService->Service member function of my NTService class. ServiceMain receives two arguments from the SCM. Though these correspond to argc and argv from the command line, these arguments were not available to the command line. Instead, they come from the startup parameters entry from the SCM.
The first thing the Service member function does is register ServiceControl by calling RegisterServiceCtrlHandler and giving the service name and the function pointer that were used to create the NTService instance. The dwCurrentState member of the SERVICE_STATUS member (mStat) was set to
SERVICE_START_PENDING by the constructor. Once the SCM has the pointer to ServiceControl, it will retrieve this status value. NTService::Service calls Init, passing the argc and argv values. Init is stubbed in the NTService class but should be overridden to provide any initialization needed by the actual service instance. If the service was successfully initialized, the status word will be changed to SERVICE_RUNNING and the overloaded Run will be called. At this point the service is running.
To examine or change the service's state, the SCM calls the SeviceControl function that was registered when the service was started. In my case, it calls ServiceControl in main.cpp, which in turn calls NTService::Control. Control can respond to any of the five commands listed in the table, or to any user command. User commands should have values that are greater than that of SERVICE_CONTROL_SHUTDOWN. The SCM does not send either user commands or the shutdown command. Only Windows can send SERVICE_CONTROL_SHUTDOWN, when Windows is preparing to shut down, and only Service Control programs can send user commands.
The control commands that a service recognizes are established when the service control function is registered. The NTService class initializes this in its constructor by assigning a value to mStat.dwControlsAccepted. The NTService class defaults to recognizing all of the commands. You can change this in the derived class's constructor. All services must respond to the interrogate command. NTService::Control itself should never be overridden. The Control member is implemented by using a switch statement that invokes functions for each command. Control also makes appropriate changes to the service status. The control member functions are listed in Table 1. Stubs are provided in NTService for any functions that require no action.
Applying the NTService Class
Creating a new service is simply a matter of creating a class derived from the NTService class that implements the Run member function, and changing main.cpp to give the service the correct name and to invoke the new operator for the derived class. Other functions are provided that can be overloaded for specific purposes. An example of an implementation of a new service (MyService) can be found in myserve.h (Listing 4) and myserve.cpp (Listing 5). MyService is a simple service that uses Windows mail slots for interprocess communications. Anything written to the mail slot that MyService creates is copied to the file outfile.txt in the c:\temp subdirectory. One use for this might be as the base of a global message logging service.
The MyService Constructor
The MyService constructor has little to do. If the new service will not accept all of the controls, then the constructor can change the values by changing mstat's contents using SetAcceptedControls. MyService accepts only the SERVICE_CONTROL_STOP command, so it makes this change in its constructor.
The Init Function
The NTService::Init member is stubbed in the NTService class, but should be overloaded to provide any program-specific initialization needed for the service. Init receives the argc and argv arguments that the SCM passes to ServiceMain. The service status is set to SERVICE_START_PENDING by NTService::Service before the Init member is called, and is changed to SERVICE_RUNNING before calling Run if Init completes successfully. Init should process the argc and argv parameters and read any registry values that are needed to support the application. If initialization is going to be lengthy, the status word has provision for checkpoints and wait hints to be provided to the SCM. You can change these by calling the ChangeStatus member function with the appropriate values.
MyService uses the Windows function Createmailslot to create the mail slot that will collect messages from other processes. It then creates the output file in the c:\temp subdirectory. The filenames are hard-coded for simplicity. Normally, I would not hard-code the names of the mail slot or the output file, but would instead read them from the registry. I would also read the registry values using the Init method.
Service Control Functions
There are six functions for dealing with service control actions (see Table 1). In my example, I have implemented only OnStop. OnStop should be implemented for all services. MyService::OnStop simply changes the state of Run's controlling loop variable. Run then performs any file closing and other cleanup chores needed to gracefully stop the service. OnPause could be implemented by setting a member variable that Run would poll. Run would then poll the variable and sleep until OnContinue caused the variable state to change again. I have not implemented OnShutdown because the stub version in the NTService class calls OnStop as the default implementation. Nothing additional is needed for OnInquire so it is not overloaded. Also, I do not have any user commands in this example; therefore, OnUserControl is not implemented. The NTService::Control method handles service status changes that are needed to inform the SCM, so this does not need to be done by the service control functions.
The Run Function
Run is a pure virtual function in the NTService class, and so must always be implemented. Run is the meat of any service, and does all the work. Effectively, Run is a loop that continues to loop until the service is stopped. The m_running variable in the MyService class controls the loop in the MyService service. MyService::Run examines the mailslot to determine if data is available. If data is available it reads the data and copies it to the output file, continuing until OnStop sets m_running to false.
Error and Event Logging
Unlike Unix, Windows NT does not have the concept of a master console. Services cannot count on a console being available to receive output or error messages (unless the service opens one). However, Windows NT does provide event logging capability. All applications can access the event log to record significant information, and the event log can be easily viewed from any machine on the network. Using the event log, however, requires that you develop a series of messages in advance and compile them using the message compiler, which can be a complex process. I have created a simplified universal message file and an application log class, TAppLog, which simplifies the use of the event log for applications that do not have extensive event reporting requirements.
The TAppLog class can control the level of messages that are logged and it can echo messages to the debugger. This class is shown in tapplog.h (Listing 6), tapplog.cpp (Listing 7), and mcmsg.mc (Listing 8). Compiling mcmsg.mc with the Windows message compiler creates mcmsg.rc and mcmsg.h. mcmsg.rc should be linked when the executable is built and Mcmsg.h should be included in at least one file. I have included the file in main.cpp.
The MyService class includes the pointer m_log to an instance of the TAppLog class. The pointer is initialized in the MyService constructor and the object is freed by the MyService destructor. TAppLog also provides an Install function, which is called from MyService::InstallAid and creates the needed registry entries for the TappLog class to work properly. An Uninstall function is stubbed for a future enhancement.
TAppLog provides two versions of the LogEvent method. The first argument of each is a log category that is one of LOG_ERROR, LOG_WARNING, or LOG_INFORM. These are mapped to generate the appropriate code for the Windows ReportEvent function. One version of LogEvent then takes arguments in the same fashion as printf. The other version takes a valist (variable argument list). The valist version is what allows the LogEvent function to be provided as a member of the MyService class. MyService also implements an Error member function which automatically cracks the Windows message number and logs the error message to the event log.
A service is run by the SCM and cannot be started from the debugger in the IDE. The service can be debugged with the debugger by inserting a DebugBreak statement in the code. The code will break into the debugger when the DebugBreak statement is reached. Be aware that the SCM will time out if the service does not report back its status in a timely fashion. It may also be possible to debug some services as a console application. This requires avoiding or skipping over any calls to the SCM and modifying main to call ServiceMain and ServiceControl as needed for testing. I have been able to do all of my debugging by logging debug messages to the event log.
Installing and Removing a Service
Before a service can be started it must be installed into the SCM's database. The NTService::Install method does this. It first checks to see if the service is already installed. If so, it returns true. If the service needs to be installed, it gets a handle to the SCM and requests that the service be added. By default the service is installed for "demand start," meaning the service is not automatically started by the operating system, but instead starts when the user requests it by using the Services applet from the Control Panel.
Function InstallAid can be overloaded to add registry entries or perform any other installation tasks. It is stubbed to do nothing in NTService. You can also use the InstallAid method to change the effect of the defaulted arguments to the Windows CreateService function by calling the Windows ChangeServiceConfig function. A service can be removed from the SCM database by calling the NTService::UnInstall method. UnInstall calls UnInstallAid, which should be overloaded to clean up anything that was done by the overloaded InstallAid function. UnInstallAid is stubbed to do nothing in NTService.
Services should be configured to store any initialization parameters in the registry. A Control Panel applet also should be created to easily manipulate and change these values. This example is designed to emphasize the mechanics of building a service, and the issues of proper initialization and storing registry values is left for another exercise. For example, the mail-slot and the output filenames should be read in from values stored in the registry. There is a defined place in the registry for the parameter values needed by a service. It is:
HKEY_LOCAL_MACHINE\ SYSTEM\ CurrentControlSet\ Services\ <<ServiceName>>\ Parameters
Default values for any parameters should be written by the overloaded InstallAid method. The registry entries should be removed by the UninstallAid method. The values should be read by the Init method when the service is started.
Services in NT exist to extend the capability of the operating system. Like daemons in Unix, NT services can be started automatically when the operating system is started, or on demand by the user or by an application program that talks to the Service Control Manager. The SCM starts and stops the service and forwards custom commands from application programs. An NT service program consists of three primary functions or entry points: main, ServiceMain, and ServiceControl. The NTService class can be used to create a new NT service by writing as few as three functions: Init, Run, and OnStop. This service can install and uninstall itself to and from the SCM database. The example MyService class implements a simple logging service by using mail slots.
James M. Bell has a Ph.D. in mathematics from Auburn University. He was a member of the mathematics faculty at Furman University for 12 years and for the last 14 years has been a mathematician in the Operations Research Department of Miliken & Company. Part of his job is as system administrator for the department's Unix network. James may be reached at [email protected]