This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


February 1998

Microsoft Systems Journal Homepage

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 Developer’s 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
      An SCP is a Win32®-based application that communicates with an SCM running on either a local or a remote machine. Most people usually think of an SCP as an application that controls services by starting, stopping, pausing, or continuing the M, but an SCP can do much more than that. An SCP can manipulate an SCM's database by adding services, removing services, and enumerating the installed services. The SCP can also change a service's configuration. In this article, I'll look at the various ways that an SCP can communicate with the SCM. The SCM is also responsible for starting and stopping device drivers. Many of the functions discussed in this section apply to both services and device drivers, but I will concentrate on services and avoid discussing device drivers.
      The first step in communicating with an SCM is to call OpenSCManager:


 SC_HANDLE OpenSCManager(LPCTSTR lpMachineName, 
                         LPCTSTR lpDatabaseName, 
                         DWORD dwDesiredAccess);
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:

 BOOL CloseServiceHandle(SC_HANDLE hSCManager);

Adding a Service to the SCM Database
      By far the most common reason to manipulate the SCM database is to add a service. To add a service, you must call OpenSCManager, specifying the SC_MANAGER_ CREATE_SERVICE access, then call CreateService:


 SC_HANDLE CreateService(SC_HANDLE hSCManager, 
     LPCTSTR lpServiceName, LPCTSTR lpDisplayName, 
     DWORD dwDesiredAccess, DWORD dwServiceType, 
     DWORD dwStartType, DWORD dwErrorControl, 
     LPCTSTR lpBinaryPathName, LPCTSTR lpLoadOrderGroup,  
     LPDWORD lpdwTagId, LPCTSTR lpDependencies, 
     LPCTSTR lpServiceStartName, LPCTSTR lpPassword);
      As you can see, CreateService requires quite a few parameters—13 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:

 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Replicator
      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:

 // The buffer below ends with 2 zero characters.
 LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\0");
 CreateService(…, lpDependencies, …);
To create a service that is dependent on the Workstation and NetBios services (like the Messenger service), you would set lpDependencies like this:

 // The buffer below separates the strings with a
 // zero character and ends with 2 zero characters.
 LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\0NetBios\0");
 CreateService(…, lpDependencies, …);
      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):

 #define SC_GROUP_IDENTIFIERW    L'+'
 #define SC_GROUP_IDENTIFIERA    '+'

 #ifdef UNICODE
 #define SC_GROUP_IDENTIFIER     SC_GROUP_IDENTIFIERW
 #else
 #define SC_GROUP_IDENTIFIER     SC_GROUP_IDENTIFIERA
 #endif
      Therefore, to create a service that is dependent on the Workstation service and the TDI group, you set lpDependencies like this:

 // The buffer below

 // specifies two 
 // dependencies: the 
 // Workstation service
 // and the TDI group (a 
 // group because of the '+' // before TDI).
 LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\
        0+TDI\0");
 CreateService(…, lpDependencies, …);
      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:
  • The handle returned from OpenSCManager doesn't have SC_MANAGER_CREATE_SERVICE access.
  • The new service specifies a circular dependency.
  • A service with the same display name already exists.
  • The specified service name is invalid.
  • A parameter is invalid.
  • The specified user account does not exist.
      I always write my service executables so that they can install the Mselves. In my WinMain function, I call a function like ServiceInstall (see Figure 4) if "-install" is passed as a command-line argument. MSJTimeSrv.c, which I'll show you later, demonstrates this technique.

Deleting a Service from the SCM Database
      After adding a service to the SCM, removing a service is the next most popular reason for an application to talk to the SCM. To remove a service, you must first open it:


 SC_HANDLE OpenService(SC_HANDLE hSCManager,
                       LPCTSTR pszInternalName, 
                       DWORD dwDesiredAccess);
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

 BOOL DeleteService(SC_HANDLE hService);
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
      As I mentioned earlier, many services ship with a client-side application that allows administrators to start, stop, pause, continue, and otherwise control a service. Writing this SCP is very easy. The program will first open the SCM on the desired machine by calling OpenSCManager using SC_MANAGER_CONNECT access. Then you call OpenService to open the service you wish to control using the desired combination of SERVICE_START, SERVICE_ STOP, SERVICE_PAUSE_CONTINUE, SERVICE_USER_ DEFINED_CONTROL, and SERVICE_INTERROGATE access.
      Once opened, StartService starts the service:


 BOOL StartService(SC_HANDLE hService, 
                   DWORD dwNumServiceArgs,
                   LPCTSTR *lpServiceArgVectors);
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:
  • The handle returned from OpenService doesn't have the proper access.
  • The service's executable file is not in the location specified in the directory.
  • The service is already running, disabled, or marked for deletion.
  • The SCM's database is locked. (I'll talk more about this later.)
  • The service depends on another service that doesn't exist or failed to start.
  • The user account for the service could not be validated.
  • The service didn't respond to the start request in a timely fashion.
      Once the service is running, ControlService can be used for further control:

 BOOL ControlService(SC_HANDLE hService,
                     DWORD dwControl,
                     LPSERVICE_STATUS lpServiceStatus);
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:
  • The handle returned from OpenService doesn't have the proper access.
  • The service can't be stopped because other services depend on it. In this case, you may want to stop the dependent services first.
  • The control code is not valid or is not acceptable to the service. A service sets the SERVICE_STATUS structure's dwAcceptedControls member when it calls SetServiceStatus.
  • The control code can't be sent to the service because the service is reporting SERVICE_STOPPED, SERVICE_START_PENDING, or SERVICE_STOP_PENDING.
  • The service is not running.
  • The service has not responded to the control request in a timely fashion.
       Figure 6 shows how to stop a service. You'll notice that StopService calls the WaitForServiceToReachState function. This function (see Figure 7) is not a Win32 function but one that I have written. It is usually desirable for an SCP to keep close tabs on the service. For example, the user clicks on the Stop button in the Services Control Panel applet. The applet tells the SCM that the selected service should stop. The SCM notifies the service, and the service should respond by setting the dwCurrentState member to SERVICE_STOP_PENDING. However, the service has not stopped yet, so the Control Panel applet shouldn't update its UI to reflect that the service has stopped. The system doesn't provide a way for an application to be notified of service state changes, so an SCP must periodically poll the service to determine when its state has changed. The WaitForServiceToReachState function demonstrates how to handle the polling of a service properly.
      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:

 BOOL QueryServiceStatus(SC_HANDLE hService,
     LPSERVICE_STATUS lpServiceStatus);
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
      The CreateService function adds the new service's entry to the SCM database. It's unusual to do so, but occasionally you may want to change this information. For instance, the user account associated with the entry may need to change its password or may want to change the service from manual start to automatic start. Well, Win32 offers you two functions that help to reconfigure a service. The first function, QueryServiceConfig, allows you to retrieve the service's entry from the SCM's database:


 BOOL QueryServiceConfig(SC_HANDLE hService,
    LPQUERY_SERVICE_CONFIG lpServiceConfig,
    DWORD cbBufSize, LPDWORD pcbBytesNeeded); 
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:

 typedef struct _QUERY_SERVICE_CONFIG {
    DWORD   dwServiceType;
    DWORD   dwStartType;
    DWORD   dwErrorControl;
    LPTSTR  lpBinaryPathName;
    LPTSTR  lpLoadOrderGroup;
    DWORD   dwTagId;
    LPTSTR  lpDependencies;
    LPTSTR  lpServiceStartName;
    LPTSTR  lpDisplayName;
 } QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG; 
      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.

 DWORD cbBytesNeeded;
 LPQUERY_SERVICE_CONFIG pqsc;
 QueryServiceConfig(hService, NULL, 0, &cbBytesNeeded);
 pqsc = malloc(cbBytesNeeded);
 QueryServiceConfig(hService, pqsc, cbBytesNeeded, 
                    &cbBytesNeeded);
 
 // Refer to the members inside pqsc
 •
 •
 •
 free(pqsc);
      Once you have the service's current configuration, you can change it by calling ChangeServiceConfig:

 BOOL ChangeServiceConfig(SC_HANDLE hService, 
     DWORD dwServiceType, DWORD dwStartType, 
     DWORD dwErrorControl, LPCTSTR lpBinaryPathName,
     LPCTSTR lpLoadOrderGroup, LPDWORD lpdwTagId, 
     LPCTSTR lpDependencies, LPCTSTR lpServiceStartName, 
     LPCTSTR lpPassword, LPCTSTR lpDisplayName);
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 stops—except for the display name change, which takes effect immediately.

Locking the SCM Database
      When reconfiguring a service, you may want to prohibit the SCM from starting any services temporarily. This will allow you to query a service's entry and change it knowing that the SCM will not be able to start the service between the Se two operations. This may also be useful if a service is dependent on other services. To prevent the SCM from starting any more services, call


 SC_LOCK LockServiceDatabase(SC_HANDLE hSCManager);
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:

 BOOL UnlockServiceDatabase(SC_LOCK ScLock);
      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:

 SC_HANDLE hSCM = OpenSCManager(NULL, NULL, 
                                SC_MANAGER_LOCK);
 // Lock the SCM's database
 SC_LOCK scLock = LockServiceDatabase(hSCM);
 CloseServiceHandle(hSCM);
 // NOTE: The database is still locked
 •
 •
 •
 UnlockServiceDatabase(scLock);
 // The database is now unlocked
      There is also a function that allows you to see the status of the SCM's lock:

 BOOL QueryServiceLockStatus(SC_HANDLE hSCManager,
                             LPQUERY_SERVICE_LOCK_STATUS 
                             lpLockStatus,
                             DWORD cbBufSize, 
                             LPDWORD pcbBytesNeeded);
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:

 typedef struct _QUERY_SERVICE_LOCK_STATUS {
     DWORD   fIsLocked;
     LPTSTR  lpLockOwner;
     DWORD   dwLockDuration;
 } QUERY_SERVICE_LOCK_STATUS, *LPQUERY_SERVICE_LOCK_STATUS;
      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
      There are just a few more service control functions that Win32 offers. I'd like to mention the M briefly to complete my discussion.
      First, there is a function that looks up a service's display name from its internal name


 BOOL GetServiceDisplayName(SC_HANDLE hSCManager,
     LPCTSTR lpServiceName, LPTSTR lpDisplayName,
     LPDWORD lpcchBuffer);
and another function that does the reverse:

 BOOL GetServiceKeyName(
     SC_HANDLE hSCManager,
     LPCTSTR lpDisplayName,
     LPTSTR lpServiceName,
     LPDWORD lpcchBuffer);
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:

 BOOL EnumServicesStatus(SC_HANDLE hSCManager, 
     DWORD dwServiceType, DWORD dwServiceState,     
     LPENUM_SERVICE_STATUS lpServices,
     DWORD cbBufSize, LPDWORD pcbBytesNeeded, 
     LPDWORD lpServicesReturned, 
     LPDWORD lpResumeHandle);
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:

 typedef struct _ENUM_SERVICE_STATUS {
    LPTSTR lpServiceName;
    LPTSTR lpDisplayName;
    SERVICE_STATUS ServiceStatus; 
 } ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS;
      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:

 DWORD cbBytesNeeded, dwServicesReturned, 
     dwResumeHandle = 0;
 LPQUERY_SERVICE_CONFIG pqsc;
 EnumServicesStatus(hSCManager, SERVICE_WIN32, 
                    SERVICE_STATE_ALL, NULL, 0, 
                    &cbBytesNeeded, &dwServicesReturned,
                    &dwResumeHandle);
 pess = malloc(cbBytesNeeded);
 EnumServicesStatus(hSCManager, SERVICE_WIN32, 
                    SERVICE_STATE_ALL, pess, 
                    cbBytesNeeded, &cbBytesNeeded, 
                    &dwServicesReturned,  
                    &dwResumeHandle);
 for (DWORD dw = 0; dw < dwServicesReturned; dw++) {
    // Refer to the members inside pess, for example
    printf("%s\n", pess[dw].lpDisplayName);
 }
 free(pess);
      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:

 BOOL EnumDependentServices(SC_HANDLE hService, 
                      DWORD dwServiceState, 
                      LPENUM_SERVICE_STATUS lpServices,
                      DWORD cbBufSize, 
                      LPDWORD pcbBytesNeeded, 
                      LPDWORD lpServicesReturned);
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
Figure 8 SCP Applet

      Finally, I come to the last two service control functions, QueryServiceObjectSecurity and SetServiceObjectSecurity:

 BOOL QueryServiceObjectSecurity(SC_HANDLE hService,
     SECURITY_INFORMATION dwSecurityInformation,  
     PSECURITY_DESCRIPTOR lpSecurityDescriptor,
     DWORD cbBufSize, LPDWORD pcbBytesNeeded); 
 BOOL SetServiceObjectSecurity(SC_HANDLE hService,
     SECURITY_INFORMATION dwSecurityInformation,
     PSECURITY_DESCRIPTOR lpSecurityDescriptor); 
      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 MSJTimeSrv service (shown in Figure 9) includes all the components necessary to build a Windows NT service. This very simple service returns the server machine's date and time when a client connects to the server. The service assumes that you are somewhat familiar with named pipes and I/O completion ports. If you need more information about the Se things, see the Platform SDK documentation.
      If you examine the WinMain function, you'll see that this service has the ability to install and remove itself from the SCM 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.
      The most important thing to notice in WinMain is that an array of SERVICE_ TABLE_ENTRY structures is initialized with two members: one for the service and the Other with NULL entries to identify the last service. The address of this structure is then passed to StartServiceCtrlDispatcher, which creates a thread for the service. This new thread will begin its execution starting with the TimeServiceMain function. Note that StartServiceCtrlDispatcher will not return to the WinMain function until the TimeServiceMain function returns 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 will sit in a loop waiting for requests to enter the completion port. Two types of requests are possible: a client connection and a time request. However, the service thread may also wake up if it needs to process a service control notification, such as pause, continue, or stop.
      Once the completion port is created, RegisterServiceCtrlHandler is called so that the SCM is told the address of this service's handler function, TimeServiceHandler. The TimeServiceHandler function receives any control notifications for this service. Remember, the thread that originally called StartServiceCtrlDispatcher is the same thread that executes the code in the TimeServiceHandler function.
      You'll notice that my TimeServiceHandler function contains just one line of code. It simply transfers the control code to the service's thread by calling PostQueuedCompletionStatus (with a completion key of CK_SERVICECONTROL) and then returns. The service code 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 dwCompKey variable to see what action the service needs to respond to next. Since this variable is initialized to CK_SERVICECONTROL, the service's first order of action 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 with 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 filling out a SERVICE_STATUS structure and passing the address to SetServiceStatus.
      Now, the service calls GetQueuedCompletionStatus, which causes its thread to sleep until an event appears in the completion port. If a service control code appears (because TimeServiceHandler called PostQueuedCompletionStatus), the service thread wakes, reports to the SCM that the Operation is pending (if appropriate), processes the control code (as appropriate), and then reports in again to the SCM that the Operation is complete.
      If the service thread wakes because GetQueuedCompletionStatus returns a completion key of CK_PIPE, then a client has connected to the pipe. At this point, the service gets the system time and calls WriteFile to send the time off to the client. Then, the service disconnects the client and issues another asynchronous call to ConnectNamedPipe so that another client may connect.
      When the service thread wakes with a SERVICE_CONTROL_STOP or SERVICE_ CONTROL_SHUTDOWN code, it closes the pipe and the service thread terminates. This causes the completion port to be closed and the TimeServiceMain function returns, killing the services thread. At this point, the StartServiceCtrlDispatcher returns back to the WinMain function which also returns, killing the process.

The MSJ Time Client
      To test the service, you must also run a client application, MSJTimeClient.c (as shown in Figure 10). When you run this client application, the dialog box shown in Figure 11 appears.

Figure 11 Figure 12
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 server—this 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.