The TimeService sample service ("03 TimeService.exe"), shown in Listing 3-1 at the end of this section, includes all the necessary components for building a service. The source code and resource files for the application are in the 03TimeService directory on the companion CD. This very simple service returns the server machine's date and time when a client connects to the server. The service does assume that you are somewhat familiar with named pipes and I/O completion ports (discussed in Chapter 2).
If you examine the _tWinMain function, you'll see that this service has the ability to install and remove itself from the SCM's database depending on whether "-install" or "-remove" is passed as a command-line argument. Once you build the service, run it once from the command line, passing "-install". When you no longer want to keep the service on your machine, run the executable from the command line, passing "-remove" as an argument. I'll discuss the details of the functions used for adding services to and deleting services from the SCM database in the next chapter.
The most important aspect of _tWinMain to notice is that an array of SERVICE_TABLE_ENTRY structures is initialized with two members: one for the service and one with NULL entries to identify the last service. The address of this service table array is passed to StartServiceCtrlDispatcher, which creates a thread for the service. This new thread begins execution starting with the TimeServiceMain function. Note that StartServiceCtrlDispatcher will not return to the _tWinMain function until the TimeServiceMain function exits and its thread terminates.
The TimeServiceMain function implements the actual code to process the client requests. It starts by creating an I/O completion port. The service thread sits in a loop waiting for requests to enter the completion port. Two types of requests are possible: a client has connected to the pipe and wants the machine's date and time information; or the service needs to process an action request such as pause, continue, or stop.
Once the completion port is created, I initialize a global CServiceStatus object, g_ssTime. The CServiceStatus C++ class is my own creation and greatly simplifies the reporting of service status updates. This class is derived from the Windows SERVICE_STATUS structure and is basically a thin level of abstraction that places some logic over how the member variables are updated. CServiceStatus has a CGate class object as a member, which is used to synchronize the TimeHandlerEx thread and the TimeServiceMain thread, guaranteeing that the ServiceMain thread processes a single action request at a time.
The CServiceStatus's Initialize method internally calls RegisterServiceCtrlHandlerEx to notify the SCM of the service's HandlerEx function (TimeHandlerEx), and it passes the address of the I/O completion port C++ class object, iocp, to the handler function in its pvContext parameter. The Initialize method also sets the dwServiceType member, which never changes during the lifetime of the service.
Next, I call the AcceptControls method, which sets the dwControlsAccepted member. The AcceptControls method can be called periodically while the service runs to accept or reject the desired controls. The TimeService accepts stop, pause, continue, and shutdown codes during its lifetime.
You'll notice that my TimeHandlerEx function passes control codes to the service thread by calling the CIOCP's PostStatus method, which internally calls PostQueuedCompletionStatus using the handle of the I/O completion port. A completion key of CK_SERVICECONTROL is specified to indicate to the ServiceMain thread that it is waking up because of a service action request. The TimeHandlerEx function then returns as quickly as possible. The service's thread is responsible for waking up, processing the code, and then waiting for more client requests (if appropriate).
Back inside the TimeServiceMain function, a do-while loop starts. Inside this loop, I check the CompKey variable to see what action the service needs to respond to next. Since this variable is initialized to CK_SERVICECONTROL and the dwControl variable is initialized to SERVICE_CONTROL_CONTINUE, the service's first order of business is to create the named pipe that the client application will use to make requests of the service. This pipe is then associated with the completion port by using a completion key of CK_PIPE, and an asynchronous call to ConnectNamedPipe is made. The service now reports to the SCM that it is up and running by calling the g_ssTime object's ReportUltimateState method, which internally calls SetServiceStatus to report SERVICE_RUNNING.
The service calls the iocp object's GetStatus method (which internally calls GetQueuedCompletionStatus). This causes the service thread to sleep until an event appears in the completion port. If a service control code appears (because TimeHandlerEx called PostQueuedCompletionStatus), the service thread wakes, processes the control code (as appropriate), and then reports to the SCM again that the operation is complete. Note that the TimeHandlerEx function is responsible for reporting the pending state of the action, and the TimeServiceMain function is responsible for reporting the service's final execution state (which is the third method I discussed earlier in the section "Dealing with Interthread Communication Issues").
When the service thread wakes because GetQueuedCompletionStatus returns a completion key of CK_PIPE, a client has connected to the pipe. At this point, the service gets the system time and calls WriteFile to send the time to the client. Then the service disconnects the client and issues another asynchronous call to ConnectNamedPipe so that another client can connect.
When the service thread wakes with a SERVICE_CONTROL_STOP or SERVICE_CONTROL_SHUTDOWN code, it closes the pipe and terminates. This causes the completion port to close, and the TimeServiceMain function returns, killing the service thread. At this point, the StartServiceCtrlDispatcher returns to the _tWinMain function, which also returns, killing the process.
After you build the service, you must execute the program with the "_install" command-line switch to install the service in the SCM's database. Be sure to include quotes around the executable name since it includes a space, "03 TimeService.exe". Also, you'll want to use the Services snap-in to start and administer the "Programming Server-Side Applications Time" service.
Listing 3-1. The TimeService sample service
|
|
Gate.h /****************************************************************************** Module: Gate.h Notices: Copyright (c) 2000 Jeffrey Richter Purpose: This class creates a normally open gate that only one thread can pass through at a time. ******************************************************************************/ #pragma once // Include this header file once per compilation unit /////////////////////////////////////////////////////////////////////////////// #include "..\CmnHdr.h" /* See Appendix A. */ /////////////////////////////////////////////////////////////////////////////// class CGate { public: CGate(BOOL fInitiallyUp = TRUE, PCTSTR pszName = NULL) { m_hevt = ::CreateEvent(NULL, FALSE, fInitiallyUp, pszName); } ~CGate() { ::CloseHandle(m_hevt); } DWORD WaitToEnterGate(DWORD dwTimeout = INFINITE, BOOL fAlertable = FALSE) { return(::WaitForSingleObjectEx(m_hevt, dwTimeout, fAlertable)); } VOID LiftGate() { ::SetEvent(m_hevt); } private: HANDLE m_hevt; }; ///////////////////////////////// End of File ///////////////////////////////// |