Click here to Skip to main content
Click here to Skip to main content

A Simple C++ Class Framework for Services

, 10 Jun 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple C++ class framework for writing Windows services

Introduction

Windows services are background programs that form the backbone of many software solutions. They can be started when the OS is started before user logon occurs and can continue to run for the lifetime of the operating system. Service programs are the programs that run as services which are managed by the OS through the component Service Control Manager(SCM). Service programs have a different structure and control flow than normal user initiated programs and this often poses a challenge for many early developers, who consequently resort to copying the sample code of a service program provided in the platform SDK to write their own services. And quite often, this process is repeated for every service program, not an ideal software engineering practice. This article explains a simple C++ class that abstracts the structure and behavior of a typical service program thereby allowing code re-use without resorting to the copy-paste cycle.

Background

As mentioned in the previous section, as service programs have a different lifetime and control flow, its structure is a little different from normal user initiated programs. Microsoft has a specific set of API that the service programs needs to interface with to register itself as a service with the SCM. An object oriented framework for this is provided in C#, but there isn't an equivalent one in C++. There is a popular class in CodeProject by PJ Naughter, CNTService, that provides a similar framework, but it is coupled with MFC. Some service programs may prefer not to have the added baggage of MFC and in certain cases corporate policies might restrict usage of libraries such as MFC.

This article describes one such lean framework written in pure C++, which includes code for some common patterns that are employed in a typical Windows service program. The class and its methods are deliberately kept to a minimum and has helped me write many service programs over the past 10+ years. It is aimed at the novice programmer, though experienced developers might also find it useful.

Using the Code

The class framework essentially consists of two files:

  • conslsvc.h
  • logfmwk.h

The former declares the template class TConsoleService<>, which implements the service program logic and is the base class from which the client class representing the service program can be derived. The template accepts a single type argument, a text logging class used for the builtin logging mechanism. A global instance of this derived class forms your service program. logfmwrk.h declares a couple of classes that are used by TConsoleService<> to provide a simple text file logging mechanism. Note that this logging is different from the Windows Event Log framework. Text logging infrastructure is included as many services that I have worked on has required some sort of simple text logging mechanism.

The framework provides the following major features:

  • A simple service program that has no additional dependencies other than Win32 and STL. Program can be built as a console Win32 program in Visual Studio.
  • A facility to allow the service program to be run from commandline as a regular program. This allows easy debugging cycles as the service program can be launched direct from Visual Studio and run as a console program.
  • An optional mechanism to write program status messages to a text log file. The log file supports automatic rollover, on a per session basis.

To use the code, in your program's main.cpp file, declare a class as below:

class MyServicePrgram : public TConsoleService<FileLogger>
{
    typedef TConsoleService<FileLogger> baseClass;

public:
    MyServicePrgram():
        : baseClass(L"myservice")
    {}
    virtual DWORD run()
    {
        // Do your own service initialization here
        // If initialiation is a lengthy process (>30 seconds), 
        // do it in a  worker thread which can be spawned here. 

        // Base class waits for the quit event, a Win32 event.
        // If you want you can bypass this and have your own
        // wait routine which can also wait on other handles,
        // using, for example, WaitForMultipleObjects().
        DWORD dwRet = baseClass::run();

        // Service has been terminated. Do your service's 
        // deintialization here.
        // Again remember to use worker threads if this takes
        // more than 30 seconds.

        // a return value that will be propagated back to SCM
        return dwRet;
    }
};

// The ConsoleService class non-const static member
ConsoleService* ConsoleService::s_pProgram = 0;

// Declare an instance of the service program. Note that there 
// can be only one instance of TConsoleService<> object per module!
MyServicePrgram _service;

// in the main function call the ConsoleService<>::start() method
extern "C" int WINAPI _tmain( int argc, TCHAR* argv[] )
{
    return _service.start();
}

The derived class, MyServiceProgram in this case, instance is your service program. From the program main, transfer control to the service program by calling the TConsoleService<>::start() method on the derived class instance. TConsoleService<> takes care of the necessary SCM plumbing of registering itself and the associated control handler.

Control Handlers

Control commands are commands that are sent by the SCM to the service program to indicate various events occurring within the OS. These also include commands to tell the service to stop or pause itself. Each of these control commands are mapped to an appropriately named virtual method in the base class. Derived class can override these methods and when the command is received from SCM, the derived method will be invoked. Following are the control command methods that are supported at present:

  • onStop() - SERVICE_CONTROL_STOP
  • onPause() - SERVICE_CONTROL_PAUSE
  • onContinue() - SERVICE_CONTROL_RESUME
  • onInterrogate() - SERVICE_CONTROL_INTERROGATE
  • onShutdown() - SERVICE_CONTROL_SHUTDOWN
  • onDeviceEvent() - SERVICE_CONTROL_DEVICEEVENT
  • onHaredwareProfileChange() - SERVICE_CONTROL_HARDWAREPROFILECHANGE
  • onSessionChange() - SERVICE_CONTROL_SESSIONCHANGE
  • onPowerEvent() - SERVICE_CONTROL_POWEREVENT
  • onPreShutdown() - SERVICE_CONTROL_PRESHUTDOWN
  • onUnknownRequest() - for all other control codes not covered by the above

Take note that some control codes (and therefore their corresponding handler methods) are only available in certain versions of Windows. In the base class source, these are conditionally declared using the _WIN32_WINNT version macro. You may refer to the MSDN documentation for details on the control codes and the meaning of the control function arguments for each. Also, Microsoft has been adding new control commands with almost every major release of Windows. With Windows 7 & 8, new control commands have been added to the above list. If there's a new command that you need to handle and is not supported by the list above, you can extend the switch block to add your own handler.

The design of the class might suggest that to accept the additional control codes, all that is required is to override the relevant method of the TConsoleService<> base class. While this is true for certain controls commands, for others, you need to do a little bit more. This is because SCM only sends certain control commands if the service program expresses its interest in those commands. The service program does this by specifying the required flags in SERVICE_STATUS.dwControlsAccepted member. By default, the service program is registered to only accept the SERVICE_CONTROL_STOP control command from SCM.

An example case is the onShutdown() notification. To receive shutdown notification, service program has to specify SERVICE_ACCEPT_SHUTDOWN flag in the SERVICE_STATUS.dwControlsAccepted member. An ideal way to do this is to override the ConsoleService::run() method and specify the additional flags just before switching the service state to STATE_RUNNING. The following code excerpt shows how this can be done.

DWORD run()
{
    // specify flags for additional control commands we accept
    status_.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN;

    // call base class method to block on the quit event
    DWORD dwRet = ConsoleService::run();

    // service is stopping, do additional unwind logic here
    return dwRet;
}

Note that SERVICE_ACCEPT_STOP flag will automatically be added in TConsoleService<>::setServiceStatus() when the service switches out of its SERVICE_START_PENDING state. This makes the TConsoleService<>::setServiceStatus() seem a bit unwieldy, but it's a simple solution to the clean design requirement that until service enters the SERVICE_STATE_RUNNING state, it should not be able to accept any additional control commands.

Text Logging

As mentioned before, the class framework also includes a built-in mechanism to write messages to a text log file, which is outside of the Win32 event log mechanism. This logging mechanism is optional and is controlled through the class template argument. There are two logging classes that are provided for this purpose -- NullLogger and FileLogger. The former class directs all log messages to null device (in other words, discards all log messages) whereas the latter writes the messages to a text file. By default, this text file takes the same name as the service name that is specified as the TConsoleService<> constructor argument, but with .log extension. This can however be changed by overriding the TConsoleService<>::getLogFilename() method. Depending on your service needs, you may use either of these two classes, or if you want to redirect log messages to an entirely different medium, you can create your own specialization of the Logger class and supply it as the class template argument to TConsoleService<>.

The logging framework consists of two classes:

  • Logger - responsible for the actual job of logging the messages. An abstract base class, that needs to be specialized for each log output medium.
  • LogWriter - responsible for composing the log message and sending it to the attached logger. Clients use this class to compose and write log messages.

There is no magic here -- trusted and proven log framework design that is simple and easy to use.

LogWriter is the primary interface for writing log messages. Each LogWriter instance can be assigned a text tag (specified as a constructor argument) and all messages written using that instance will be prefixed with this text tag. This allows messages belonging to be a component to be easily identified & grouped together. The class also provides a C++ stream based interface for writing messages. Stream based interface is a typesafe alternative to traditional printf(...) style logging and consequently yields better runtime safety.

The framework design is based on the approach that each class in the service will instantiate a LogWriter and use that instance to write messages to the log file. This allows separate tags to be assigned to each instance which helps identify the internal module which generates the log message. Typically, I assign the class name or something very close as the tag. For example, in the attached sample code, all messages written using lw_ will be prefixed with the tag "svcapp".

class MyService : public TConsoleService<FileLogger> {
    typedef TConsoleService<FileLogger> baseClass;
public:
    MyService()
        : baseClass(L"myservice")
        , m_lw(L"svcapp", getLogger())
    {}
...
private:
    LogWriter lw_;
};

The stream interface to writing messages is provided through the following methods:

class LogWriter {
    ...
public:
    template<class charT>
    TSafeWriter<charT> getStream(int level);

    TSafeWriter<wchar_t> getStreamW(int level); 

    TSafeWriter<char> getStreamA(int level); 
    ...
};

TSafeWriter<> is a specialization of std::basic_ostringstream<> and therefore supports all the methods and manipulators supported by std::ostringstream. The solitary parameter to getStream and its variants is the classic logging level attached to the message. All LogWriter instances maintain a reference to the app wide Logger instance and the Logger instance stores the current logging level. Incoming messages at a higher level are not written to the destination medium. A few standard predefined logging levels are declared as Logger class constants. But being a pure integer, the client class is free to use whatever best fits its needs.

With this in place, log messages can be written as:

lw_.getStreamW(LOG_LEVEL_INFO) << L"Error registering device notification, rc: " << ::GetLastError() << L"\r\n";

Note that TSafeWriter<> is declared with a private constructors. This is a deliberate design decision to prevent clients from storing the object in an l-value. This is required as the messages written to the log stream are buffered and are written out to the output medium from the returned object's destructor. With the above code, construction happens at the beginning of the statement line and the object destruction occurs when the entire statement is executed and before the execution of the next statement line. Without this in place, clients can write code like:

{
   TSafeWriter<wchar_t> ls = getStreamW(100);
   ...
   ls << "Log message 1\r\n";
   ...
   ls << "Log message 2\r\n"; 
   ...
}

While there's nothing wrong with the above code, the messages will only be written to the output medium when ls goes out of scope. This poses multiple problems.

Firstly, in a multi threaded system, the execution sequence may be such that between output of "Log message 1" and "Log message 2", the CPU context switched to another thread which also prints its own log messages. Since the messages won't be written until the local variable is destroyed, the sequence of writing the messages would not reflect the sequence of program thread's execution. This can still happen while the log message in a single statement is being composed. But since we treat each log statement as an atomic piece of information, log messages will be written out in their natural thread execution sequence. Secondly, the program could hit a critical error between the print of message 1 and message 2, causing it to abort. In this scenario, the exact statement where the error occurred is not clear from the log messages as none of the messages in the block have been written out yet.

Adopting a log stream object that has a single statement lifetime gets around these problems quite nicely. If you're wondering how getStream() can return a TSafeWriter<> instance, it's declared as a friend of TSafeWriter<>, so has the necessary access rights to TSafeWriter<> constructor and therefore can instantiate it.

Points of Interest

In the original design, TConsoleService<>, was a pure class where the logging system was hard-coded. That is, for every service program created by it, a log file would be automatically created. However, as the class usage scenarios evolved, certain services did not require a log file. Even creation of an empty file was not acceptable. So this design was changed where the user can specify whether a log file is to be created or not (as a constructor argument). Even this was found lacking, as logging was still restricted to preset mediums. So eventually this pattern was abstracted out and the service framework was designed as a template class such that the required logging pattern can be specified as a template argument. This is another example that shows how the templates feature of C++ provides an efficient approach to code re-use.

History

  • 10 June 2014 - Initial release
  • 17 June 2014 - Added the demo solution that builds a sample service using the framework. This file did not get uploaded correctly in the initial version.

License

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

Share

About the Author

Hari Mahadevan
Software Developer IBM
Taiwan Taiwan
Experienced developer having worked on drivers, daemons and applications -- essentially the entire breadth of components in both Windows & Linux. My choice programming language is C++, though I dabble with Python every now and then. Very comfortable with Microsoft technologies and enjoy tackling complex problems.
 
When not coding, I do a bit of photography, read stuff and love making my hands greasy.
 
I live in Taipei, work for IBM as a Technical Lead and am fluent in Chinese.
 
The views and opinions expressed in this article are mine and do not necessarily reflect the policy or position of my employer.
Follow on   Google+   LinkedIn

Comments and Discussions

 
QuestionHow to download the source code of this project? thx PinmemberAiven.Wang13-Jun-14 22:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411023.1 | Last Updated 11 Jun 2014
Article Copyright 2014 by Hari Mahadevan
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid