Charles Steinhardt
Charles has been getting many questions about making an application an NT Service. In this month's column, he introduces the Service Control Manager and shows you how to set up your application as a Service from within the IDE.
What is the difference between a Service and an EXE in ATL AppWizard?
J. Gonzalez
Why can't I display a MessageBox from a service?
T. Nyguyen
I've written an NT service, part of which performs some important database updates. When I use Control Panel | Services, the Service Control Manager then emerges, allowing me to stop the service. I need to do some cleanup work right before the service shuts down and sometimes I don't want to stop the service at all. But, I can't figure out how to prevent or delay shutdown. Would the code AppWizard automatically generates for services in ATL
help me out?
Steve Gagig
"Defeat doesn't finish a man— quitting does. A man is not finished when he's defeated. He's finished when he quits."—Richard M. Nixon
Obviously, Nixon never had to write an NT service. In fact, simple technology like the erase button on a tape recorder seems to have stunned him. Should this aspect of an NT service not be written or configured properly, your whole system will appear as a "deer caught in the headlights."
Typically, NT services are console apps. ATL makes writing NT services so simple that I'll also use this opportunity to discuss how you can use the basic AppWizard-generated ATL code to create an NT service and a provide a solution to your question using ATL. Though this might appear to be an ATL discussion, in actuality, it's also a generic NT service question and doesn't contain as obvious an answer as one would expect.
The simplest definition of an NT service is "an executable program that's controlled, started, stopped, and paused with the service control manager (SCM)." Mystically named, the SCM is the software interface used to control services. The SCM can also configure startup parameters and security-oriented issues. A service looks and smells just like a regular EXE program. The only difference is that this executable needs a few special particulars in order to interact with the SCM.
The SCM is found in the Control Panel, as shown in Figure 1. The interface is simple, with a list of services and their status. You control the services with buttons on the right-hand side of the dialog box. (See Figure 2.)
Figure 1: Double-click on Services in the Control Panel to start the SCM
Figure 2: The SCM gives you control over all of the services on your system
It's easy to create a basic NT service with AppWizard. Use the ATL COM Wizard, and in Step 1, choose Service as your server type, as in Figure 3.
Figure 3: The ATL Object Wizard can create a starter Service for you
Hit the "Finish" button, and the AppWizard generates some globals as well as the one basic C++ class called CServiceModule needed for your service. More mystifying names, eh? At this point you can build the skeleton service, but you'll find that simply compiling and linking using the default settings isn't enough for your Service to be listed in the SCM list. I'll show you how to accomplish that with a simple example.
My test program for this article is named TestService.exe, and it's available in the Subscriber Downloads at www.pinpub.com/vcd. If you build an ATL Service with the same name, you can follow along. AppWizard generates some self-registration code and adds it to the Custom Build tab in Project Settings. Figure 4 shows you this tab and the extra code.
Figure 4: Self-registration code appears on the Custom Build tab
AppWizard generated a tWinMain function that will let you register your application as a server, a service, or both when you run it stand-alone. You provide command-line options indicating the kind of registration you require. The first custom build command registers your application as a server:
"$(TargetPath)" /RegServer
If you've not met the Custom Build tab before, this line might need translation: It substitutes the full name of the executable for "$(TargetPath)," so this line actually executes your program with a command-line parameter of "/RegServer," which registers it as a server, every time you successfully build the project. Any interfaces, IIDs, CLSIDs, and other typelibrary info that your executable supports will be updated in your registry so that they're available to client applications. But, this isn't the same as registering your application as a "service" with the SCM. You can register your application as a service by running it from the command line, like this:
C:\TestService\Debug\TestService.exe –Service
This command line registration can quickly become inconvenient during testing, particularly if you're switching back and forth between debug and release versions of your service. It would be much better to use the IDE's custom build capabilities so that when you compile the service in debug, release, or any other format, the latest build will be registered and used by client applications. Add these lines to the code on the custom build tab:
"$(TargetPath)" –Service
echo Service registration done!
Compile and link this service. If you start the SCM by bringing upControl Panel and then Services, you'll see the service applications listed in alphabetical order. If you haven't used the SCM before, you can easily experiment starting and stopping your simple service. Much like a "Hello World" application, the service itself doesn't "do" anything, but with the SCM you can start and stop it as often as you'd like. Are you feeling that incredible rush of power yet?
Service basicsRemember that class AppWizard made for you, CServiceModule? Most of the members are the same as you'd find in any Win32 service. For example, CServiceModule::ServiceMain is the entry point for a service. When the SCM requests a service to be started, the SCM sends a start request to a control dispatcher. This control dispatcher creates a new thread to execute the CServiceModule::ServiceMain function for the specified service. Requests from the SCM to services are very time-sensitive. Therefore, whenever dealing with any requests from the SCM, a service utilizes the SetServiceStatus function, which responds to the SCM with the current progress and any further "wait time" the service needs. For example, if you need to perform any special initialization in CServiceModule::ServiceMain that would take longer than one second, you should call SetServiceStatus, specifying SERVICE_START_PENDING flag along with a time you feel it would require to finish. This article isn't about tuning services, so I won't delve into methodologies for proper service development. Suffice it to say that initialization might be better handled by spawning threads in combination with the CServiceModule::ServiceMain function.
Once the service is initialized, the service must register a function—a callback, if you will—to process any other requests from the SCM. This is called a "handler" function. Hence, the default-generated code in CServiceModule::ServiceMain calls the RegisterServiceCtrlHandler, which registers the CServiceModule::Handler function to handle all control requests for the service. Gosh! When will those Microsofties ever learn to stop using those enigmatic names?
Every service has a control handler. Here's the AppWizard-generated code for CServiceModule::Handler:
inline void CServiceModule::Handler(DWORD dwOpcode)
{
switch (dwOpcode)
{
case SERVICE_CONTROL_STOP:
SetServiceStatus(SERVICE_STOP_PENDING);
PostThreadMessage(dwThreadID, WM_QUIT, 0, 0);
break;
case SERVICE_CONTROL_PAUSE:
break;
case SERVICE_CONTROL_CONTINUE:
break;
case SERVICE_CONTROL_INTERROGATE:
break;
case SERVICE_CONTROL_SHUTDOWN:
break;
default:
LogEvent(_T("Bad service request"));
}
}
The SCM sends commands to CServiceModule::Handler specifying either a user-defined code or, most typically, one of the standard codes below:
The control handler must return from any of these requests within 30 seconds, or the SCM will return an error. If your service needs more time while the service is executing the control handler, it would be best to create a thread to perform the extra processing. In that case, you'd spawn a new thread, then call SetServiceStatus(SERVICE_STOP_PENDING) and return. This prevents the service from confining the control dispatcher. Reviewing the default code in the handler, you can observe that it does something very similar. Under SERVICE_CONTROL_STOP, it calls SetServiceStatus() and posts a message to the service itself to quit.
Within this Handler function, we'll place the solution to the third question at the beginning of this column. When your server is passed a SERVICE_CONTROL_STOP request, inside CServiceModule::Handler is where you'll add your code to tell the SCM that it's closing or not closing.
I simplify my solution by not spawning a thread, but I complicate matters by displaying a messagebox requesting user input. If the user answers "Yes," I'll close the service. If the user answers "no," then the service will continue to run.
Add the following to your SERVICE_CONTROL_STOP code in CServiceModule::Handler:
case SERVICE_CONTROL_STOP:
{
SetServiceStatus(SERVICE_STOP_PENDING);
if (::MessageBox(NULL, "Do you want to quit? ",
"Important Title",
MB_SERVICE_NOTIFICATION | MB_YESNO)
== IDYES)
{
SetServiceStatus(SERVICE_STOP_PENDING);
PostThreadMessage(dwThreadID, WM_QUIT, 0, 0);
}
else
{
SetServiceStatus(SERVICE_RUNNING);
}
break;
}
Notice how the first action I take is to call SetServiceStatus(SERVICE_STOP_PENDING). I perform this as soon as possible. I'm telling the SCM, "Okay, enough already, wait, I'll be there soon." Next, I use the MessageBox API to display the question with Yes/No buttons. It's easy to miss the MessageBox flags I used.
I combine the familiar MB_YESNO with MB_SERVICE_NOTIFICATION. This is very important. It tells the function that the caller is a service notifying the user of an event. The function displays a message box on the current active desktop, even if there's no user logged on to the computer. When using this flag, you must set the hWnd parameter to NULL. This is so the message box can appear on a desktop other than the desktop corresponding to the hWnd. If the user refuses to shut down the service, my code responds with SetServiceStatus(SERVICE_RUNNING), and I don't post a message to quit.
That's all the code that's required. However, there's one very important SCM setting that must be changed for any registered service that will be displaying UI. You must follow these steps for this solution to work:
You can leave the account set to the System (administrator) accounts or a specific account. There's a potential problem if you do choose a specific user account and that user isn't logged on or isn't present to respond to the MessageBox that appears on their desktop. The server would appear to "lock up" until a response is given to that MessageBox. You might even need to reboot the server at that point. It's safer to leave it to anyone with a system account.
Download sample code here.
Charles Steinhardt, an MFC/C++ consultant in New York City, provides advanced Windows system consulting, including troubleshooting, for local and international clients. 70353.2604@compuserve.com.