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:
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()
{
DWORD dwRet = baseClass::run();
return dwRet;
}
};
ConsoleService* ConsoleService::s_pProgram = 0;
MyServicePrgram _service;
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()
{
status_.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN;
DWORD dwRet = ConsoleService::run();
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.