Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Self Contained NT Service Class: No Derivations Required

4.66/5 (17 votes)
8 Aug 2007CPOL8 min read 1   1.2K  
A class to create a NT service with a few lines of code

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:

C++
// this is the service main
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);
}
// this is the application main
void wmain(DWORD argc, LPWSTR* argv)
{
    // create an access point to the service Framework
    nt_service&  service = nt_service::instance(L"my_test_service");

    // register "my_service_main" to be executed as the service main method
    service.register_service_main( my_service_main );

    // set the service to accept stop controls. Do nothing when it happens
    service.accept_control( SERVICE_ACCEPT_STOP );

    // set the service to accept shutdown commands.
    // Call my_shutdown_fcn() when it happens
    service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );

    // All set, start the service
    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:

  1. 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.
  2. 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.
  3. Keep the class interface clean and intuitive.

Finally, the class can be set to support UNICODE (wchar_t) and non UNICODE (char) strings.

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:

C++
// creates an access point to the service class
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:

C++
// register "my_service_main" to be executed as the 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:

C++
// set the service to accept stop controls. Do nothing when it happens
service.accept_control( SERVICE_ACCEPT_STOP );

To accept a control and register a function to be called when your service receives that control:

C++
// set the service to accept shutdown control and do something when received
service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );

Finally, to start your service, you call:

C++
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:

C++
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:

C++
// stops the service, reporting -1 as exit code.
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:

C++
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:

  1. 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.
  2. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)