February 1998
Manipulate Windows NT Services by Writing a Service Control Program
Download Service2.exe (9KB)
Jeffrey Richter wrote Advanced Windows, Third Edition (Microsoft Press, 1997) and Windows 95: A Developers Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32 programming courses (www.solsem.com. He can be reached at www.JeffreyRichter.com.
|
Every day,
I come up with more reasons to use Windows NT® services. I expect other people will, too, as Windows NT proves itself as a solid operating system for the enterprise. My October 1997 article, "Design a Windows NT Service to Exploit Special Operating System Facilities," identified three components to the Windows NT Service architecture: the Service Control Manager (SCM), the service itself, and a service control program (SCP). The article went into great detail on the first two components but spent very little time discussing SCPs. This article explains SCPs. If you're writing a service, it's important to understand SCPs and what they are capable of because they are the applications that control your service.
Writing a Service Control Program
|
|
This function establishes a communication channel with the SCM on the machine specified by the lpMachineName parameter. Pass NULL to open the SCM on the local machine. The lpDatabaseName parameter identifies which database to open; you should always pass either SERVICES_ACTIVE_DATABASE or NULL for this parameter. The dwDesiredAccess parameter tells the function what you intend to do with the SCM database. Figure 1 indicates what accesses are available.
OpenSCManager returns an SC_HANDLE that you pass to other functions to manipulate the SCM's database. When you are finished accessing the SCM database, you must close the handle by passing it to CloseServiceHandle: |
|
Adding a Service to the SCM Database
|
|
As you can see, CreateService requires quite a few parameters13 to be exact. The hSCManager parameter is the handle returned from OpenSCManager. The next two parameters, lpServiceName and lpDisplayName, indicate the name of the service. Services have an internal name for programmers and a display name that is shown to users. The internal name, identified by lpServiceName, is the One the SCM uses to store the service information inside the registry. For example, the Directory Replicator service has an internal name of Replicator, and its service information can be found under the following registry subkey: |
|
CreateService's dwDesiredAccess parameter is useful because CreateService returns a handle to the newly installed service so you can manipulate it right away. This dwDesiredAccess parameter tells the SCM what you intend to do with the service. If you are just installing a service and do not intend to manipulate it after it's installed, simply pass zero for this parameter and then close the handle returned from CreateService immediately by calling CloseServiceHandle. Figure 2 shows what you can do to a service.
The dwServiceType parameter tells the system how many services are contained inside the executable file. Pass SERVICE_WIN32_OWN_PROCESS if the file contains a single service or SERVICE_WIN32_SHARE_PROCESS if the file contains two or more services. You can also combine the SERVICE_INTERACTIVE_PROCESS flag if you want the services in this process to be able to interact with the interactive user's desktop. The dwStartType parameter tells the system when the service should be started. If you want the service to start automatically when the system boots, specify SERVICE_AUTO_START. To have the service start only when a service dependent on it starts or to allow the user to manually start the service, pass SERVICE_DEMAND_ START. I'll talk more about service dependencies shortly. Finally, a value of SERVICE_DISABLED prevents the system from starting the service at all. A service is an important part of the system and, as such, the system needs to know what it should do if the service fails to start. This is the job of the dwErrorControl parameter. Passing a value of SERVICE_ERROR_IGNORE or SERVICE_ERROR_NORMAL tells the system to log the service's error in the system's event log and continue starting the system. The difference between the Se two codes is that SERVICE_ERROR_NORMAL will also have the system display a message box notifying the user that the service failed to start. Services that are demand-started should always specify SERVICE_IGNORE_ERROR. Values of SERVICE_ERROR_SEVERE and SERVICE_ ERROR_CRITICAL tell the system to abort startup if the service fails to start. When a service fails and one of the Se codes is specified, the system logs the error in the system's event log and then reboots automatically using the last-known good configuration. If, when the system is booting the last-known good configuration, the service fails and its error control is SERVICE_ERROR_SEVERE, the system continues to boot. The SERVICE_ERROR_CRITICAL code also tells the system to abort booting the last-known good configuration. CreateService's lpBinaryPathName parameter identifies the full path name of the executable that contains the service. Many service files are installed to the %SystemRoot%\system32 directory, but you can place a service executable anywhere in the file system. Now let's get to the issue of service dependencies. Loosely speaking, a service acts as part of the Operating system, and many services will not work properly unless they know that other parts of the system are up and running first. When the system boots, it follows an algorithm that dictates the Order in which services should start. Microsoft has divided the system services into a set of predefined groups, as shown in Figure 3. (You can also find this list in the registry under the HKEY_LOCAL_MACHINE\SYSTEM\ CurrentControlSet\Control\ServiceGroupOrder subkey.) The system iterates through the list, loading the device drivers and services that are part of each group. For example, the system loads all the device drivers and services that are part of the System Bus Extender group before loading the device drivers and services that are part of the SCSI miniport group. When you add a service to the SCM database, you can assign it to one of the predefined groups by passing the name of the group in CreateService's lpLoadOrderGroup parameter. If you write a service that shouldn't belong to any of the Se groups (the most common case), simply pass NULL for this parameter. The system starts services that are not in any group after all the Other device drivers and services have started. If you are adding a device driver to the SCM (as opposed to a service), you can get even greater granularity of load order when your particular device driver is started by specifying a tag ID. Services cannot take advantage of this additional granularity and must always pass NULL to CreateService's lpdwTagId parameter. In addition to telling the SCM that your service is part of a particular group, you can tell the SCM that your service requires other services or groups to be running before your service can run. For example, the Computer Browser service requires that the Workstation and Server services be running before it can work properly, and the Clipbook Server requires that the NetDDE service be running. Specifying which services your service depends on is actually much more useful than indicating that your service is part of a group. You use CreateService's lpDependencies parameter to tell the SCM database which services you depend on. If your service has no dependencies, simply pass NULL for this parameter. The lpDependencies parameter is a little unusual because you must pass the address of a double zero-terminated array of zero-separated names. In other words, lpDependencies must point to a block of memory that contains a set of zero-terminated strings with an extra zero character at the end of the buffer. To create a service that is dependent on the Workstation service (like the Alerter service), you would set lpDependencies as below before passing it to CreateService: |
|
To create a service that is dependent on the Workstation and NetBios services (like the Messenger service), you would set lpDependencies like this: |
|
A service can also be dependent on a group rather than a single service. The Server service, for instance, depends on the TDI group. Dependency on a group means that this service can run if at least one member of the group is running after an attempt to start all members of the group. To specify a group in the lpDependencies buffer, you must precede the group name with the special SC_GROUP_ IDENTIFIER character (defined in WINSVC.H): |
|
Therefore, to create a service that is dependent on the Workstation service and the TDI group, you set lpDependencies like this: |
|
When setting the lpDependencies value, you can specify as many services and groups as you like. Just remember to place a zero character between each service or group, a plus sign in front of all group names, and a terminating zero character just before the closing quote. CreateService's final two parameters, lpServiceStartName and lpPassword, allow you to specify an account under which the service will run. To have the service run under the System Account (the most common case), pass NULL for the Se two parameters. If the executable file contains more than one service, the system requires that you use the System Account. If you want the service to run under a user account, pass an account name in the form of "DomainName\UserName" for the lpServiceStartName parameter and pass the user account's password for the lpPassword parameter. If CreateService is successful in adding the service to the SCM's database, a non-NULL handle is returned. This handle is required by other functions in order to manipulate the service. Make sure to pass this handle to CloseServiceHandle when you're finished. If CreateService fails it returns NULL, and a call to GetLastError will return a value indicating the reason for failure. Here are the most common reasons for CreateService to fail:
Deleting a Service from the SCM Database
|
|
To this function, pass the handle returned from OpenSCManager, followed by the internal name of the service (this is the same value that was passed in CreateService's lpServiceName parameter), and DELETE for the desired access. Now that you have the handle to the specific service, you can delete the service by calling |
|
passing the handle returned from OpenService. This function does not actually delete the service right away; it simply marks the service for deletion. The SCM will delete the service only when the service stops running and after all handles to the service have been closed. I also write my services so that they can delete the Mselves from the SCM database. If "-remove" is passed as a command-line argument, I call a function like ServiceRemove (see Figure 5).
Controlling a Service
|
|
The hService parameter identifies the Opened service, and the dwNumServiceArgs and lpServiceArgVectors parameters indicate the set of arguments that you wish to pass to the service. Most services do not accept any parameters, so zero and NULL are usually passed for the Se last two arguments. Remember that starting a service may cause several services to start if your service is dependent on other services or groups. Here are some of the main reasons why StartService could fail:
|
|
Again, the hService parameter identifies the Opened service that you wish to control. The dwControl parameter indicates what you wish the service to do and can be any one of the following values: SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_
CONTINUE, or SERVICE_CONTROL_INTERROGATE. In addition to the Se values, you can send a user-defined code in the range of 128 to 255, inclusive. Note that ControlService fails if you pass a value of SERVICE_CONTROL_
SHUTDOWN; only the system can send this code to a service's Handler. ControlService's last parameter, lpServiceStatus, must point to a SERVICE_STATUS structure. The function will initialize the members of this structure to report the service's last-reported status information. You can examine this information after ControlService returns to see how the service is doing. Here are some of the common reasons for ControlService's failure:
Polling is usually a horrible thing to do because it wastes precious CPU cycles, but you really have no choice in this case. The situation is not as bad as you think, though, because the SERVICE_STATUS structure contains the dwWaitHint member. When a service calls SetServiceStatus, this member must indicate how many milliseconds the program sending the control code should wait before polling the service's status again. In addition to this, the SCP should examine the checkpoint returned from the service to make sure it is always increasing. If a service returns the same checkpoint value or a smaller value, the SCP should assume that the service has failed. This is another area where the service architecture is awkward and could benefit from some improvement. You'll notice that the WaitForServiceToReachState function calls QueryServiceStatus: |
|
Calling this function is similar to calling ControlService passing a SERVICE_CONTROL_INTERROGATE code. However, QueryServiceStatus asks the SCM what the service's state is, while sending an interrogate code asks the service what its state is. This means that QueryServiceStatus will always report good information to you. The problem is that ControlService fails if the service you're trying to control is not running. For example, let's say that you want to stop a service and wait for it be stopped. You would call ControlService passing SERVICE_CONTROL_STOP, and then you could call ControlService passing SERVICE_CONTROL_INTERROGATE repeatedly. But if the service is stopped, its handler function won't be able to respond to the SERVICE_CONTROL_INTER-ROGATE code and ControlService will simply fail. The QueryServiceStatus function, on the Other hand, asks the SCM for the state of the service. If the service is not running, the SCM fills in the SERVICE_STATUS structure properly, setting the dwCurrentState member to SERVICE_STOPPED. You can even call QueryServiceStatus if the service is running because the SCM caches a service's status information every time the service calls SetServiceStatus. Another benefit to QueryServiceStatus is that it always returns immediately because it doesn't send an interrogate control code to the service. If a service's handler is busy when you send an interrogate code to it, the service may not be able to respond for a while, causing your SCP to be suspended. Of course, there is a downside to this as well. The SCM's cached data may not accurately reflect the state of the service. Now that you know the tradeoffs, you can query a service's state using whichever method works best in your situation.
Reconfiguring a Service
|
|
When you call this function, the hService parameter identifies the service you wish to query. The handle must be opened with SERVICE_QUERY_CONFIG access. You must also allocate a memory buffer large enough to hold a QUERY_SERVICE_CONFIG structure and all the service's string data. A QUERY_SERVICE_CONFIG structure looks like this: |
|
The cbBufSize parameter tells the function how big
your buffer is and the DWORD pointed to by the pcbBytesNeeded parameter is filled in by the function telling you how big the buffer needs to be. The buffer that you pass to QueryServiceConfig will always have to be bigger than the size of a QUERY_SERVICE_
CONFIG structure because the function copies all of the service's string data into the buffer immediately after the fixed-size data structure. The LPTSTR members will point to memory addresses inside this buffer. When I call QueryServiceConfig, I always call it twice in a row. The first call is to determine how big my buffer needs to be and the second time is to actually get the data. The code below demonstrates how to do this properly. |
|
Once you have the service's current configuration, you can change it by calling ChangeServiceConfig: |
|
As you can see, the Se parameters are practically identical to those passed to CreateService. The differences are that you cannot change the service's internal name and that the display name is the last parameter. If the service is running, the changes do not take effect until the service stopsexcept for the display name change, which takes effect immediately.
Locking the SCM Database
|
|
passing it the handle returned from a call to OpenSCManager, using the SC_MANAGER_LOCK access. This function returns a 32-bit value that identifies the lock. Hold on to this value because you'll need to pass it to UnlockServiceDatabase when you want to release the lock: |
|
Only one process at a time may own the SCM's lock, and, of course, you should own the lock for as short a time as possible. If the process that owns the lock terminates, the SCM will automatically reclaim the lock so that services may start again. Note that the lock is not released if you close the handle to the SCM. For example: |
|
There is also a function that allows you to see the status of the SCM's lock: |
|
This function returns to you whether or not the SCM is already locked. If the SCM is locked, the function also returns which user account owns the lock and how long the lock has been owned. All of this information is returned via a QUERY_SERVICE_LOCK structure: |
|
Like QueryServiceConfig, the buffer that you pass to QueryServiceLockStatus must actually be larger than the size of the structure itself. Again, this is because the structure contains a string value (lpLockOwner) that will be copied into this buffer immediately after the fixed-size structure.
Miscellaneous SCP Functions
|
|
and another function that does the reverse: |
|
The parameters to the Se functions should be self-explanatory so I won't go into the M here. See the SDK documentation for more information. Second, there is a function that asks the SCM to enumerate all of the services (and their states) contained in the database: |
|
The Services Control Panel applet calls this function to populate its list of installed services. The first parameter, hSCManager, identifies the SCM whose services you wish to enumerate. The second parameter, dwServiceType, asks to enumerate services or device drivers. For services, pass SERVICE_WIN32. The third parameter, dwServiceState, allows you to fine-tune your request. You can pass either SERVICE_ACTIVE, SERVICE_INACTIVE, or SERVICE_
STATE_ALL to enumerate running services, stopped services, or both. All of the remaining parameters have to do with the buffer that gets the returned data. When you call EnumServicesStatus, you pass it a buffer that will be filled with an array of ENUM_SERVICE_STATUS structures: |
|
Because each service has string data associated with it, the string data is copied to the end of the buffer. The fixed-size ENUM_SERVICE_STATUS structures are contiguous at the beginning of the buffer so you can easily iterate through the returned data structures. When the function returns, the DWORD pointed to by lpServicesReturned contains the number of ENUM_SERVICE_STATUS structures that fit into the buffer. The code below shows how to enumerate the set of installed services: |
|
The first time you call EnumServicesStatus, make sure that the DWORD pointed to by lpResumeHandle is initialized to zero. This lpResumeHandle is used in cases where there is more data than your buffer can hold. If the buffer is too small, EnumServicesStatus fills this DWORD with a special value that it uses the next time you call EnumServicesStatus so that it knows where to continue the enumeration. The previous code shows how to allocate a buffer that is large enough to hold all of the service data so multiple calls to EnumServicesStatus aren't necessary. The next function that I'll discuss allows you to determine which services depend on another service: |
|
Since this function is very similar to EnumServicesStatus, all of the parameters should be self-explanatory. The Services Control Panel applet calls this function if you try to stop a service that has other services dependent on it. For example, if I try to stop the Workstation service, I get a dialog box like the One shown in Figure 8. EnumDependentServices was used to fill in the list of dependent services. |
Figure 8 SCP Applet |
Finally, I come to the last two service control functions, QueryServiceObjectSecurity and SetServiceObjectSecurity: |
|
the Se two functions allow you to query and change a security descriptor associated with a service. When you call CreateService, the service is assigned a security descriptor based on the process that installs the service; SetServiceObjectSecurity can be used to change this.
The MSJ Time Service
The MSJ Time Client
|
Figure 11 | Figure 12 |
To see the client and server communication work, you must type the server name in the edit control at the top. If you are running the client and the server process on the same machine, use a period for the server (as shown in Figure 12). When you press the Request Server's Time button, the client application calls WaitNamedPipe which connects the client to the serverthis will cause the server to wake up to process the client's request. Or, if the server is not running, WaitNamedPipe will fail and the text "Service not found" will appear to the right of the date and time. Don't forget to start the MSJ Time Service using the Services Control Panel applet. If the service is up and running, WaitNamedPipe returns TRUE and the client then calls CreateFile so that it can receive data on the pipe. The client then waits for the time data to come across the pipe by placing a synchronous call to ReadFile. Once the client has the data, the pipe handle is closed, the time from the server (which came across in universal time) is converted to the client's local time, and the dialog box is updated to look like the One shown in Figure 12. I hope that the information contained here and in my October 1997 article gives you a good understanding of the Windows NT Service architecture, showing you its many faults and strengths. For those of you serious about writing services, I encourage you to investigate the Windows NT event logging facility, performance monitoring facility, the registry, and security. All of the Se issues should be firmly understood by any service developer. I hope to address some of the Se additional topics in future articles.
|
From the February 1998 issue of Microsoft Systems Journal. |