Introduction
The purpose of this article is to offer a simple and easy to use class to create a NT service. All its users are required to do is to write the service's own functions and pass them to the class. The following example shows how it works:
void WINAPI my_service_main(DWORD argc, char_* argv[])
{
static DWORD i = 0;
try
{
wofstream fout(L"C:\\output.txt",ios_base::app);
if ( fout.is_open() ){
fout << "service main counting: " << ++i << endl;
fout.close();
}
Sleep(5000);
}
catch(...){
nt_service::stop( -1 );
}
}
void my_shutdown_fcn(void)
{
Beep(1000,1000);
}
void wmain(DWORD argc, LPWSTR* argv)
{
nt_service& service = nt_service::instance(L"my_test_service");
service.register_service_main( my_service_main );
service.accept_control( SERVICE_ACCEPT_STOP );
service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );
service.start();
}
If you are not familiar with the process of creation of NT services, I suggest you read the entire article. If you are already familiar with NT services, just download the code and start playing with it! The class interface
is self explanatory.
Motivation
There are a lot of classes available on the net that encapsulate the API work necessary to create a simple service. Most of them are well written and full of cool features. But there's a problem with such classes (at least the ones I've found): The user is forced to derive a class from some supplied base class in order to create the NT service. There's nothing inherently wrong with that approach. The drawback here is the time the user wastes digging into the base class implementation to figure out the right way to do the derivation and to know what can and can't be done with that class. This kind of knowledge is boring to learn and easy to forget. Some months later when you need to create another service you probably will have to learn it again...
Background
The code in this article is written in plain C++ and uses some STL classes and Win32 API calls.
This article does not cover the basics on creating NT services. Also, I think that reading someone's C++ encapsulation is not the best starting point. For beginners, I recommend a very good article written by Yevgeny Menaker available here.
Implementation Details
The nt_service
class is implemented as a Meyer's singleton. A singleton is a special kind of class that can have only one instance in a program. To explain why it's a singleton, I have to go a little deeper into the service creation process. A service application can pass more than one service main function to the Service Control Manager (SCM). To support this leads to a more complex class, but I've never needed to create a service that deals with two or more service main threads. Therefore, I've implemented nt_service
class as a singleton to reinforce the following idea: with this class you can create only one service thread per service application, so you can't have more than one instance.
Also, I've followed some basic design principles when creating the class:
- Require the minimum possible amount of user code: This is achieved by requiring no class derivations. Its users can focus on writing only the service functions.
- Have no mandatory function calls and setups: This was not achieved since it is necessary to call the
nt_service::start
method to start the service. I gave up on this principle because it led to very odd and hard to read code. - Keep the class
interface
clean and intuitive.
Finally, the class can be set to support UNICODE (wchar_t
) and non UNICODE (char
) string
s.
Usage
The nt_service
class is a singleton. So its constructor, copy constructor and attribution (=) operator are kept private
. To create an access point to nt_service
class, you call:
nt_service& service = nt_service::instance(L"my_service");
"my_service"
is the name that the class will use when creating your service. It's not the name that appears in the Service Control Manager, it's only used internally. The service name shown by the SCM is set when you install the service. Refer to the section "Installing and Managing your Service" for details.
To register your service main function:
service.register_service_main( my_service_main );
my_service_main
must be of the type LPSERVICE_MAIN_FUNCTION
This constant is "typedefined" in winsvc.h as void WINAPI (*)(DWORD, LPWSTR *)
(this is the UNICODE version, non UNICODE uses LPSTR instead). Therefore your service main function has to be declared exactly like this.
There are three things your service can do when it receives a control from the SCM. It can reject the control (the default behavior), It can just accept the control and do nothing, or it can accept the control and schedule some function to be called when it receives the control.
The example below shows how to just accept some command:
service.accept_control( SERVICE_ACCEPT_STOP );
To accept a control and register a function to be called when your service receives that control:
service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );
Finally, to start your service, you call:
service.start();
You must call start()
as soon as possible. If the start()
function is not called for 30 seconds, the SCM terminates your service and an error is reported. If your service needs to perform some time consuming initialization, you should neither put the initialization code in the service main function nor in the main application function. Instead you should call:
service.register_init_function( my_init_fcn );
It's user's task to create and put the initialization code in the my_init_fcn
function.
Sometimes the user needs to stop the service from its service main function. The next example shows how to do that:
nt_service::stop( -1 );
The stop()
method got to be called with full qualification (nt_service::stop()
) because it's static
.
Installing and Managing your Service
To install services, I use the command line toll sc.exe. It is part of the Windows resource kit. Consider that your service application is called my_service.exe and that it's located at c:\. The following example shows how to create a service and call it my_test_service
:
>sc.exe create my_test_service binpath= c:\my_service.exe
That's the name used by the Service Control Manager to refer to your service. To start your service you call:
sc.exe start my_test_service
If you want your service to be started automatically at boot time, you have two ways to set it up: using the Service Control Manager (start->Control panel->Administrative tools->Services) or using the "start= auto" option when installing your service:
>sc.exe create my_test_service binpath= c:\my_service.exe start= auto
Hints and Pitfalls
Command Line Parameters
When writing service applications you have to deal with two different command lines: one of your application and other of your service main function. You can pass command line parameters to your application when installing it with sc.exe:
>sc.exe create my_test_service binpath= "c:\my_service.exe param1 param2 ..."
These parameters can be accessed though the argv[]
parameter of your application's main function. To pass parameters directly to the service main function, you have two ways: you can pass (or change) it in the service control manager (only if your service is already installed) or when starting the service using sc.exe:
>sc.exe start my_test_service param1 param2 .....
You can access these parameters through the argv[]
parameter of your service main function in the usual way.
Writing your Service Main
This is the piece of code that executes the user's service main inside the nt_service
class:
while ( service_status.dwCurrentState == SERVICE_RUNNING )
{
try
{
user_service_main( argc, argv );
}
catch(...)
{
nt_service::stop( -1 );
}
}
As shown in the code above, the user_service_main
function is executed with no interruption as long as the service is running. If you do not include in your service main some code to pause it or to put it in a wait state, it will be called again and again until you stop the service. Unless you are writing a service application called cpuloader.exe, it's not a good thing to do...
The ACCEPT and CONTROL Constants, and Why it's Easy to Confuse Them
There are two kinds of constants that the user has to deal with when using the nt_service
class: the SERVICE_ACCEPT_*
constants and SERVICE_CONTROL_*
constants.
The ACCEPT*
constants are used when you want to set your service to just accept some control. It's particularly useful to accept stop and pause-continue controls. If you do not set these controls to be accepted, your service can't be stopped nor paused. The ACCEPT*
constants are used only as parameters for the accept_control()
method.
The CONTROL*
constants are used when you want your service to perform some action when it receives a command. To do that, you call register_control_handler()
passing the control type (one of the CONTROL*
constants) and the callback function as parameters. Of course when you choose to handle a control, you expect that your service also accepts that control. And that's exactly what the class does.
The problem is that it's very easy to use a CONTROL*
constant where a ACCEPT*
constant is expected and vice versa. Their names and functions are similar. There's no simple correspondence between CONTROL*
constants and ACCEPT*
constants. Different controls are related to the same ACCEPT*
constant, and some don't have an accept counterpart at all. And that's why we need both.
So, if your service starts to behave strangely, make sure that you are using the ACCEPT*
and CONTROL*
constants in the right places. This is a subtle and common source of bugs.
Someone can point out that the CONTROL*
vs. ACCEPT*
constants problem can be solved using enumerations. That's true and I'm planning to do that in a next version. But I've decided to include the section above because I believe that this can help anyone that plays with NT services, using nt_services
or not.
Drawbacks and Known Issues
I have identified two problems with my class so far:
- The user functions are not directly supplied to the service control manager. Instead class methods are supplied and the user functions are called inside them. So, there's a performance penalty of one more function call each time the user's service main is called. The same happens at each request your service answers. For the most service applications this is completely negligible.
- It doesn't implement all the features that the Windows API offers to create a service. I've chosen to implement only the most common features to keep the class simple. Many of the features are really advanced and complicated, and I confess, I don't fully master them.
Another thing that might be considered a problem: The class does no output on error. The problem is that each developer has his or her own favorite way of logging errors: streams, log files, OutputDebugString
calls... Since it's a pretty simple subject I let an eventual user extend the class functionality to do that.
Finally, I'm not a native English speaker. So I apologize in advance for any misspellings and grammatical errors in this article.
History
- 06/18/2007 - First version posted
- 08/08/2007 - A design flaw corrected: lots of global variables removed, using
private static
members instead