Export (0) Print
Expand All
18 out of 22 rated this helpful - Rate this topic

Implementing a Network Service on Windows CE (Windows CE 5.0)

Windows CE 5.0
 

John Spaith
Microsoft

December 2004

Applies to:
   Applies to: Microsoft® Windows® CE 4.0 and 5.0

Summary: This article focuses on writing networking services on Windows CE by using the system process services.exe. Services.exe defines an interface to make developing and managing services easier. Though this article focuses on networking scenarios, the techniques provided are valuable for non-networking services as well. If you want to write an application on Windows CE that always runs and doesn't need direct access to physical devices, services.exe is right for you. (20 printed pages)

Contents

Introduction
Services.exe Overview
System Initialization: Explaining the Registry
Application Interface into Services.exe
Implementing the Basics of a Service
Super Services
Services.exe Command-Line Interface and Service Configuration
Maintaining State
Conclusion

Introduction

When most people think of Microsoft® Windows® CE, they first thing they probably think of is a handheld device, like the Windows Mobile™ Pocket PC. What many people don't know is that Windows CE can run on more than just personal digital assistants (PDAs). It is available to original equipment manufacturers (OEMs) of embedded devices through a related product in Windows CE 5.0, which is named Platform Builder.

Windows CE is a modular operating system that allows OEMs to choose the components that best support their device. For example, an OEM adding Windows CE to a refrigerator's temperature control system may create a headless configuration with only a tiny kernel, whereas a maker of an Internet appliance may include Microsoft Internet Explorer and a Web Server.

Windows CE supports rich networking. In Windows CE, there is a TCP/IP, IrDA, and Bluetooth stack, along with Winsock, Winsock2, Wininet, and an XML parser based on MSXML3 SP1. In Windows CE 4.1, IPv6 support was added. Still, and no doubt owing to the strong association between it and PDAs, Windows CE is often seen only as a client in networking transactions. It may come as a surprise that Windows CE ships with a number of servers, including a Web server, Universal Plug and Play (UPnP), and Message Queuing (MSMQ).

Services.exe Overview

Despite all the great features available to platform builders, like Internet Explorer and Microsoft DirectX® support, Windows CE at its core is an embedded operating system. But it has certain limitations. One limitation is that only 32 processes can be running simultaneously. If every service ran in its own process, not only would they be wasting resources, but they would be moving the system closer to its 32-process maximum.

In Windows CE 3.0 and earlier, the solution was to implement services as a device driver running in the context of device.exe. Device.exe, as the name implies, is the primary host for all device drivers on Windows CE. (Device.exe was originally named horace.exe, but for some reason most people didn't make the connection between horace and device drivers.) So in Windows CE 3.0 and earlier, the Web server appeared to be a device driver as far as the operating system was concerned. Having multiple services loaded by device.exe reduced the number of running processes, and device.exe provided a convenient means of interprocess communication since all drivers were running in the same process address space.

However, using device.exe also posed some problems. A lot of other components run in device.exe, including the network stacks and real device drivers, which could make tracking resource leaks very difficult. If device.exe is leaking memory and there are 50 other device drivers running, how does a person find the guilty party? Even worse, a poorly written DLL could potentially corrupt memory in the TCP/IP stack, PCMCIA driver or other device driver, causing inexplicable crashes.

Services.exe was created to deal with these issues. Services.exe is designed to have a programming model and application programming interface (API) set that are similar to device.exe to facilitate moving device drivers that weren't really device drivers (think about a Web server) from device.exe to services.exe. If you're not familiar with device.exe, don't worry. This article starts from the basics. If you're familiar with device drivers on Windows CE, much of this article will cover familiar ground.

Services.exe provides the following major features:

  • Services.exe automatically loads services at system boot time, consuming only one process.
  • Services.exe exports an API and has hooks to the file system CreateFile and DeviceIoControl functions, which greatly simplify interprocess communication between services and other applications on the device.
  • Services.exe can be configured to listen for requests on a specified port, and then they can forward the request to a server. Writing a service by using this model is well worth the learning curve.

What does services.exe on Windows CE have in common with Windows XP's services.exe? Both are used to host other services, including third-party services. And they have the same name. The API set and programming model used for writing services however are radically different on Windows CE and Windows XP.

System Initialization: Explaining the Registry

Services are implemented in DLLs. For example, the Web server is in httpd.dll. For an explanation about the exact functions that you have to export and when they are called, see Implementing the Basics of a Service.

Services.exe is started automatically when Windows CE boots. On startup, it enumerates through each subkey of HKEY_LOCAL_MACHINE\Services, where each subkey corresponds to a service. Services.exe uses the information in each registry key to load the service into its process space and calls the service's initialization function.

To make this more concrete, this article includes a server that implements a light-weight Finger server. This example is inspired by the UNIX Finger server, which allows users to look up the status of users on remote computers. Clients would run a command, like finger Bob@microsoft.com, which queries the host, microsoft.com, and asks for the status of the user Bob. Bob could specify simple messages, like "I'm out to lunch now," or "Go Reds!" that are returned whenever someone looks him up.

The sample in this article uses a much simpler protocol than the real Finger protocol. When a remote client opens a TCP connection to a Windows CE based-device on port 79, the Windows CE-based device returns a simple message. The message can be changed dynamically by a simple API call, also provided in the following example. Individual users cannot, however, create custom messages. The message returned is configured for the entire device.

Because this protocol is so simple, no custom Finger client is going to be included. Instead, from a computer with Windows XP, simply run telnet yourCEDevice 79 from a command prompt to establish a TCP connection to port 79, rather than to the usual telnet port 23. When the Windows CE Finger server receives this TCP connection, it sends the device message to the client. The telnet client echoes the message back exactly as it was received and then exits. Using your telnet client like this suffices for the purposes of this article. Remember, the focus of this article is about writing the server on Windows CE, not the client.

What about privacy? Think about creating services, like the Finger server, on your device. Does everyone on the Internet need to be able to get Bob's status? Network security is worth another white paper (or a book for that matter). For now, this Finger server is provided because it is easy to understand, not because it is an exemplary example of protecting privacy.

The first thing you need to do is let services.exe know how to load your DLL. All you need is one registry entry:

HKEY_LOCAL_MACHINE\Services\FINGERSERVER
  "Dll"=" fingerServer.dll"
  "Order"=dword:10
  "Prefix"="FIN"
  "Index"=dword:0
  "Context"=dword:1

For the details about these registry values, check out Services.exe Registry Settings for Windows CE 5.0 or Services.exe Registry Settings for Windows CE 4.2.

Following is a brief description of each of the preceding registry values:

  • "DLL" simply provides the name of the DLL that implements the service. The DLL should be located in the \Windows directory if the full path is not provided.
  • "Order" tells services.exe in what order to load the service, relative to other services in HKEY_LOCAL_MACHINE\Services, where a low number signifies precedence over a higher one. Services with an "Order" value set to less than 10 will load before fingerServer.dll; services with an "Order" value higher than 10 will be loaded after fingerServer.dll. The value does not have to be unique, but loading a sequence of services with the same order is not defined.
  • "Prefix" specifies the prefix of the service. All of the functions services.exe cares about in fingerServer.dll must be prefixed by FIN_. Also, applications wanting to call into the service by using CreateFile will set the file name equal to "FINX:", where X is between 0 and 9, inclusive.
  • "Index" is the index of the initial file system reference of the device (the X in "FINX:"). For most services, including the Finger server, this can be set to 0.
  • "Context" specifies the initial DWORD value to pass to FIN_Init, which is a function that fingerServer.dll exports to handle its initialization. The value 1 is a magic value services.exe uses in creating a super-service thread.

Services.exe is started rather late in the boot process. Filesys.exe and device.exe are both running by the time it is created. Because these programs are running does not, however, mean that everything on the system will be running at the time your service is initialized. The device may not have a Dynamic Host Configuration Protocol (DHCP) address yet or a removable file system may not be started. You need to be aware of these limitations when you develop your service. These limitations will not affect the simple Finger server. It can affect other, more complicated services. For example, if the Web server cannot open its log file during the first attempt, it sleeps and then tries again to handle the case where the log file has been configured to be on a file system that has not yet been initialized.

It is also possible for applications to load services programmatically, based on the registry values. To load services, use ActivateService(LPWSTR szServiceName, DWORD dwReserved), where szServiceName is the service's name in the registry. In the included sample, you would use ActivateService("FINGERSERVER",0). If the service has been unloaded, ActivateService is the only way to reload it short of rebooting the device.

Application Interface into Services.exe

Pretend you've got the Finger server written, compiled, and copied to your device. While you're at it, pretend that serivces.exe has loaded it. How can an application use this service? Application in this context means another program, which is running on the Windows CE-based device, needs to use interprocess communication to configure/control/query the state of the running service. It does not refer to the network client that wants to retrieve the device's message. That has already been taken care of with telnet yourCEDevice 79.

Services are accessed just like device drivers on Windows CE and Windows XP by using file system APIs, like CreateFile, WriteFile, ReadFile, and DeviceIoControl. CreateFile is called with the service's name and returns a handle to the service that the other APIs access. DeviceIoControl sends in custom I/O control calls that are specific to your service and the application that controls it. Services.exe also defines a number of I/O control codes that you can optionally implement, such as commands to stop or start a server. These IOCTLS live in the public header file service.h, which is located in the public\common\oak\inc directory of the Platform Builder installation for version 4.0 and 4.1 and in public\common\sdk\inc in version 4.2 and later.

In Windows XP, a device driver can be named something like "\Device\TheFingerServer" or "\Device\ThisIsTheFingerServerAndItsReallyCool." In Windows CE, you can't be so creative, which may not be a bad thing. Each service name is uniquely identified by adding a colon to the service's "Prefix" and "Index" values under HKLM\Services\{ServiceName}. In the Finger server sample, the service's name is "FIN0:". Unlike in Windows XP, there is no need for a service to explicitly register itself as callable by CreateFile by using any APIs. After a service has been loaded by services.exe, applications will be able to call it.

So what's going on behind the scenes of these APIs? When CreateFile gets a string that has three characters, a single digit, and a colon (that is, "FIN0:"), it will assume it's receiving a request to access a device driver or a service, rather than a file. Filesys.exe will first query device.exe to see whether it recognizes the device name. If it doesn't, filesys.exe calls into services.exe. Services.exe looks through its table of running services for a match. If it finds a match, services.exe creates a handle and returns it to filesys.exe, which in turn returns it to your application. When you call DeviceIoControl, ReadFile, or a similar API with this handle, the kernel calls directly into services.exe with the API's arguments.

For some servers, it makes sense to implement a stream interface, which is to say it can support ReadFile and WriteFile operations. The telnet server is the classic example. Telnet sets itself as the STDIN and STDOUT of cmd.exe, so it must deal with the scanf and printf calls that cmd.exe generates that eventually go through the filesys.exe APIs.

In most cases, however, using DeviceIoControl is the easiest (and often the only) mechanism for controlling your service. DeviceIoControl can support IOCTLs that you define and take input and output parameters at the same time. ReadFile and WriteFile cannot simultaneously take parameters. If you want to check out a stream service, look at %_WINCEROOT%\public\servers\sdk\samples\telnetd in Platform Builder. To keep Finger server from being less of a study in overkill than it already is, it is not implemented as a stream service.

In the Finger server example, the service defines and implements IOCTL codes that get and set the message sent to a Finger client. You could document what IOCTLs you use and have application programmers write to your service call CreateFile and DeviceIoControl directly, but there is a better way. You can wrap all of the details of the file system calls in a small library that the application programmer can link to. This method is precisely how MSMQ exports its interface. MQOpenQueue, for instance, is a very short function in msmqrt.lib that marshals the function parameters into a call to DeviceIoControl. This call makes application programmer's lives easier, and also makes your code easier to port to a platform that uses a different interprocess communication method than Windows CE. Just change the implementation of the stub library, and the calling application can stay the same. The Finger runtime library implementation follows.

The FingerRT.dll library (RT stands for run time) exports two functions. The first function is SetFingerMessage. This function tells the Finger server what message to display when clients connect to it. The other exported function is GetFingerMessage, which retrieves the message that Finger server is returning to network clients.

Fingerrt.dll Code: 
[fingerService.h]
#define IOCTL_FINGER_SET_MESSAGE   1
#define IOCTL_FINGER_GET_MESSAGE   2
[FingerRT.cpp]

#include <windows.h>
#include <service.h>
#include <..\inc\fingerService.h>
static HANDLE hInterface = INVALID_HANDLE_VALUE;
static int Initialize (void) {
    if (hInterface == INVALID_HANDLE_VALUE) {
        hInterface = CreateFile (L"FIN0:", 0, 0, NULL, OPEN_EXISTING, 
        FILE_ATTRIBUTE_NORMAL, NULL);
    }
    return (hInterface != INVALID_HANDLE_VALUE);
}
static void UnInitialize(void) {
    if (hInterface != INVALID_HANDLE_VALUE) {
        CloseHandle(hInterface);
        hInterface = INVALID_HANDLE_VALUE;
    }
}
BOOL SetFingerMessage (CHAR *szMessage) {
    if (!Initialize ())
        return FALSE;
    return DeviceIoControl (hInterface, IOCTL_FINGER_SET_MESSAGE, (void *) szMessage, strlen(szMessage)+1, NULL, 0, NULL, NULL);
}
BOOL GetFingerMessage (CHAR *szMessage, DWORD *pcchMessage) {
    if (!Initialize ())
        return FALSE;
    return DeviceIoControl (hInterface, IOCTL_FINGER_GET_MESSAGE,NULL,  0, (void *) szMessage, *pcchMessage, pcchMessage, NULL);
}

Implementing the Basics of a Service

Services export functions that begin with the "Prefix" value specified in the registry and are terminated with a well-defined function string. Here's the fingerServer.dll .def file:

fingerServer.def:
LIBRARY     FINGERSERVER
EXPORTS
    FIN_Init
    FIN_Deinit
    FIN_Open
    FIN_Close
    FIN_IOControl

When services.exe calls LoadLibrary on your service, the loader calls the service's DllMain function. DllMain is principally intended for creation (and destruction) of synchronization primitives, and there are a number of operations that are unsafe in DllMain in any scenario in Win32 programming, not just in services and not just in Windows CE. Do not make calls to COM or Winsock, create threads, or invoke any function that might be implemented in another DLL.

After being loaded, services.exe calls xxx_Init. (By convention, xxx represents a service's "Prefix" value, which varies from service to service; xxx=FIN in this sample.) Services.exe passes in the service's registry value of "Context" into the function. xxx_Init is the place to make calls to COM, Winsock, or all the other stuff that you can't do from DllMain.

At boot time, services.exe creates services on one thread and blocks their calls to xxx_Init. No other services can start while your service is in xxx_Init, so do not block there. Instead, if you need to perform a blocking operation (such as Winsock accept call), spin your own thread or, better yet, use services.exe super server capabilities to listen for incoming connections.

If xxx_Init returns FALSE, the service is unloaded. Assuming it succeeded, services.exe may call xxx_IOControl a number of times to pass in configuration information. The IOCTL codes that services.exe makes, including brief descriptions, can be found in the header file service.h. You don't have to handle all of the IOCTLs in service.h (or any of them for that matter). Implementing code to support service management IOCTLs (such as IOCTL_SERVICE_STOP and IOCTL_SERVICE_START) allows your service to be configured much more easily and uses very similar interfaces to existing services.

xxx_DeInit is called when the service instance is being unloaded. In cases where you can have multiple service instances simultaneously (telnet, for instance, has a "TEL0:", "TEL1:", and "TEL2:", each of which corresponds to a different telnet session and each of which has xxx_DeInit called on them when that particular instance is being closed). xxx_DeInit does not necessarily mean the DLL is about to be unloaded. Because the Finger server sample creates only "FIN0:", xxx_DeInit being called tells it that the DLL is going to be unloaded. Services.exe ignores the return value of xxx_DeInit. After this function returns, the Finger server will always be unloaded. It is essential that your service have no worker threads running when it returns from xxx_DeInit. After it returns, services.exe immediately unloads your service DLL from memory. If you have worker threads still running, they crash when they try to access the DLL's unloaded code pages. Make your xxx_DeInit block for a second, for a month, or for a year until the Red Sox win another World Series — but under no circumstances should it return until the service is ready to be unloaded.

The remaining functions that a service exports are interesting only if you're implementing a stream service, like the telnet server. (Telnet is a very interesting sample and it is well commented, so if you're serious about Windows CE services, it may be a good topic to research.) xxx_Open is called when services.exe successfully maps a call to CreateFile("PrefixX:") to the given service. This sample only has to return TRUE. Returning FALSE causes the calling application's call to CreateFile to fail. Similarly, xxx_Close is called during CloseHandle on a service's handle. Implement xxx_Read, xxx_Write, and xxx_Seek only if you're creating a stream service. These calls correspond to ReadFile, WriteFile, and SetFilePosition.

Services.exe provides minimum synchronization, so all synchronization needs to be done in your service. Although beyond the scope of this article, there are in fact scenarios where xxx_Init can be called from multiple threads at the same time. Don't take all the warnings not to block in this function to mean that you can neglect its thread safety.

The following are the non-networking related portions of our Finger server. The networking portion is discussed later.

#include <windows.h>
#include <winsock.h>
#include <service.h>
// Use strsafe.h, rather that CRT string manipulation, since these 
// functions are safer - especially when manipulating external strings
// like those provided in IOCTL calls or those read from the network
#include <strsafe.h>
#include <..\inc\fingerService.h>


// Though not required, using a DWORD and macros defined in service.h
// to keep track of state allows for easier configuration.
DWORD g_dwServiceState;
// Buffer that contains what you'll send to network client on request.
CHAR g_szMessage[MAX_PATH];
// Length of string currently held in g_szMessage
DWORD g_ccMessage;
// Before the initial call to SetFingerMessage, use this as response.
CHAR g_cszDefaultMsg[] = "Hello World! I am the sample Finger server!";
// Global critical section
CRITICAL_SECTION g_cs;

// Function that processes requests
extern "C" DWORD WINAPI FingerWorker(LPVOID lpv);
// Handle to the worker thread
HANDLE g_hWorkerThread;

// Use CE's built-in debugging framework.
#ifdef DEBUG
DBGPARAM dpCurSettings = {
    TEXT("FingerServer"), {
    TEXT("Error"),TEXT("Init"),TEXT("Net Client"),TEXT("Interface"),
    TEXT("API"),TEXT(""),TEXT(""),TEXT(""),
    TEXT(""),TEXT(""),TEXT(""),TEXT(""),
    TEXT(""),TEXT(""),TEXT(""),TEXT("") },
    0x0007  // Turn on Error, Init, and Client DEBUGZONE's by default
};

#define ZONE_ERROR  DEBUGZONE(0)
#define ZONE_INIT   DEBUGZONE(1)
#define ZONE_CLIENT DEBUGZONE(2)
#define ZONE_INTRF  DEBUGZONE(3)
#define ZONE_API    DEBUGZONE(4)
#define ZONE_NET    DEBUGZONE(5)
#endif


extern "C" BOOL WINAPI DllEntry(HANDLE hInstDll, DWORD fdwReason, 
                                LPVOID lpvReserved) {
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        g_dwServiceState = SERVICE_STATE_UNINITIALIZED;
        InitializeCriticalSection (&g_cs);
        // This DLL does not require thread attatch/deatch
        DisableThreadLibraryCalls((HMODULE)hInstDll);
        // Hook into CE system debugging
        DEBUGREGISTER((HINSTANCE)hInstDll);
        break;
        
    case DLL_PROCESS_DETACH:
        DeleteCriticalSection (&g_cs);
    break;
    }
    return TRUE;
}
extern "C" DWORD FIN_Init (DWORD dwData) {
    DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_Init(0x%08x)\r\n",dwData));

    EnterCriticalSection (&g_cs);
    if (g_dwServiceState != SERVICE_STATE_UNINITIALIZED) {
        // Someone is trying to load multiple times (for example, 
        // trying to create "FIN1:").
        // Not supported, so fail to load service.
        DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Finger service already "
                             L"initialized on FIN_Init() call\r\n"));
        LeaveCriticalSection (&g_cs);
        return 0;
    }

    StringCchCopyA(g_szMessage,sizeof(g_szMessage),g_cszDefaultMsg);
    g_ccMessage = strlen(g_cszDefaultMsg);
    g_dwServiceState = SERVICE_STATE_STARTING_UP; 
    DEBUGMSG(ZONE_INIT,(L"FINGERD: FIN_Init success - service is "
                        L" in starting up state\r\n"));
    LeaveCriticalSection (&g_cs);
    return 1;
}
extern "C" BOOL FIN_Deinit(DWORD dwData) {
    DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_DeInit(0x%08x)\r\n",dwData));

    EnterCriticalSection (&g_cs);
    g_dwServiceState = SERVICE_STATE_UNLOADING;

    // If worker threads are active, you MUST block until they
    // have shutdown.  Real services may take extra action to force
    // the threads to end (that is, closing all open sockets workers
    // are using) to speed up the shutdown process.

    // This function is also the proper place to do 
    // uninitialization of other components that are not safe
    // to be called from DllMain - that is, COM, Winsock, and so on

    if (g_hWorkerThread) {
        DEBUGMSG(ZONE_INIT,(L"FINGERD: Waiting for worker thread to "
                            L"complete before service shutdown\r\n"));
        HANDLE hWorker = g_hWorkerThread;
        LeaveCriticalSection (&g_cs);
        // Block until the worker is through running.
        WaitForSingleObject(hWorker,INFINITE);
    }
    else {
        LeaveCriticalSection (&g_cs);          
    }
    // Service is unloaded no matter what is returned.
    DEBUGMSG(ZONE_INIT,(L"FINGERD: Completed shutdown.  Returning to "
                        L"services.exe for unload\r\n"));
    return 1;
}
extern "C" DWORD FIN_Open (DWORD dwData, DWORD dwAccess, 
              DWORD dwShareMode)
{
    // Handles calls application made to CreateFile. 
    // Since no client state is maintained, no more work is required.
    DEBUGMSG(ZONE_INTRF,(L"FINGERD:FIN_Open(0x%08x,0x%08x,0x%08x)\r\n",
             dwData,dwAccess,dwShareMode));
    return 1;
}
extern "C" BOOL FIN_Close (DWORD dwData) 
{
    // Handles calls application made to CloseHandle.
    // dwData is the value returned during the call to FIN_Open
    // This is where resources allocated in the dwData handle
    // (had there been any) would be freed.
    DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_Close(0x%08x)\r\n",dwData));
    return 1;
}
extern "C" BOOL FIN_IOControl(DWORD dwData, DWORD dwCode, PBYTE pBufIn,
             DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
             PDWORD pdwActualOut)
{
    DWORD dwError = ERROR_INVALID_PARAMETER;

    DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_IOControl(0x%08x,0x%08x,"
                         L"0x%08x,0x%08x,0x%08x,0x%08x,0x%08x)\r\n",
                         dwData, dwCode, pBufIn, dwLenIn,
                         pBufOut, dwLenOut, pdwActualOut));

    EnterCriticalSection (&g_cs);
    
    switch (dwCode) {
    // Control code sent to start a service.  (services start FIN0:)
    case IOCTL_SERVICE_START:
        // In real services, you need to be very careful about 
        // state changes and timing issues, and you'll almost 
        // certainly do more work than just changing a var's value.
        // Finger server will accept connections.
        if (g_dwServiceState != SERVICE_STATE_OFF) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: IOCTL_SERVICE_START"
                                 L" fails because service is not off. "
                                 L"State=<%d>\r\n",g_dwServiceState));
            dwError = ERROR_SERVICE_ALREADY_RUNNING;
        } 
        else {
            DEBUGMSG(ZONE_INIT,(L"FINGERD: State changed to ON\r\n"));
            g_dwServiceState = SERVICE_STATE_ON;
            dwError = ERROR_SUCCESS;
        }
    break;

    // Control code sent to refresh a service. (services refresh FIN0:)
    case IOCTL_SERVICE_REFRESH:
        // In real services, you need to be very careful about 
        // state changes and timing issues, and you'll almost 
        // certainly do more work than just changing a var's value.
        
        if (g_dwServiceState != SERVICE_STATE_ON) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD:ERROR:IOCTL_SERVICE_REFRESH"
                                 L" fails because service is not on. "
                                 L"State=<%d>\r\n",g_dwServiceState));
            dwError = ERROR_SERVICE_NOT_ACTIVE;
        } 
        else {
            DEBUGMSG(ZONE_INIT,(L"FINGERD: Stop on a refresh\r\n"));
            g_dwServiceState = SERVICE_STATE_SHUTTING_DOWN;
            // Shut the service down, re-read configuration 
            // (if we have any) and then restart.
            DEBUGMSG(ZONE_INIT,(L"FINGERD: Restarting on refresh\r\n"));
            g_dwServiceState = SERVICE_STATE_ON;
            dwError = ERROR_SUCCESS;
        }
    break;

    // Control code sent to stop a service.  (services stop FIN0:)
    case IOCTL_SERVICE_STOP:
        // No longer accept new network connections.  
        // Close existing connections

        // In real services, you need to be very careful about 
        // state changes and timing issues, and you'll almost 
        // certainly do more work than just changing a var's value.
        // Finger Server will stop accepting new connections.        
        if (g_dwServiceState != SERVICE_STATE_ON) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD:ERROR:IOCTL_SERVICE_STOP "
                                 L"fails.  Service state is not on. "
                                 L"Current State=<%d>\r\n",
                                 g_dwServiceState));
            dwError = ERROR_SERVICE_NOT_ACTIVE;
        }
        else {
            // In a real service, we would most likely close sockets
            // owned by worker threads (FingerWorker() in this case).
            // Not implemented here for simplicity of the sample.
            DEBUGMSG(ZONE_INIT,(L"FINGERD: Service stopping\r\n"));
            g_dwServiceState = SERVICE_STATE_OFF;
            dwError = ERROR_SUCCESS;
        }
    break;    

    // An application (possibly services.exe itself) is 
    // querying for the service's running state.
    case IOCTL_SERVICE_STATUS:
        // No need for critical section since this is an atomic read.

        __try {
            if (pBufOut && dwLenOut == sizeof(DWORD)) {
                *(DWORD *)pBufOut = g_dwServiceState;
                if (pdwActualOut)
                    *pdwActualOut = sizeof(DWORD);
                dwError = ERROR_SUCCESS;
            }
        }
        __except (EXCEPTION_EXECUTE_HANDLER) { 
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR Invalid pointer "
                                 L"on IOCTL_SERVICE_STATUS\r\n"));
            dwError = ERROR_INVALID_PARAMETER;
        }
    break;

    // Handle API call to SetFingerMessage. 
    case IOCTL_FINGER_SET_MESSAGE:
        if (pBufIn == NULL)
            break;

        // Use a try block because you should not trust that app 
        // passed a valid buffer.
        __try {
            // Do NOT trust dwLenIn, since a malicious app may lie
            // Calculate size of buffer required based on buffer.
            size_t dwStrLen;
            if (FAILED(StringCchLengthA((CHAR*)pBufIn,sizeof(g_szMessage),
                                        &dwStrLen)))
                break;

            if (FAILED(StringCchCopyA(g_szMessage,sizeof(g_szMessage),
                                        (CHAR*)pBufIn)))
                break;
            g_ccMessage = dwStrLen;
            dwError = ERROR_SUCCESS;
            DEBUGMSG(ZONE_API,(L"FINGERD: SetFingerMessage() changed message"
                               L" to <%S>\r\n",g_szMessage));
        }
        __except (EXCEPTION_EXECUTE_HANDLER) {
            // Invalid buffer.  Reset message to system default.
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Invalid pointer "
                                 L"on SetMessage\r\n"));
            StringCchCopyA(g_szMessage,sizeof(g_szMessage),
                           g_cszDefaultMsg);
            g_ccMessage = strlen(g_cszDefaultMsg);
            dwError = ERROR_INVALID_PARAMETER;
        }
    break;

    // Handle API call to GetFingerMessage. 
    case IOCTL_FINGER_GET_MESSAGE:
        if (pBufOut==NULL)
            break;

        if (dwLenOut < (g_ccMessage+1)) {
            dwError = ERROR_MORE_DATA;
            break;
        }
        
        __try {
            if (FAILED(StringCchCopyA((CHAR*)pBufOut,dwLenOut,
                                       g_szMessage)))
                break;
            
            if (pdwActualOut)
                *pdwActualOut = g_ccMessage+1;
            dwError = ERROR_SUCCESS;
            DEBUGMSG(ZONE_API,(L"FINGERD: GetFingerMessage() got "
                               L"message <%S>\r\n",g_szMessage));
        }
        __except (EXCEPTION_EXECUTE_HANDLER) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR Invalid pointer "
                                 L"on GetMessage\r\n"));
            dwError = ERROR_INVALID_PARAMETER;
        }
    break;
    }

    if (dwError != ERROR_SUCCESS)
        SetLastError(dwError);

    LeaveCriticalSection(&g_cs);
    return (dwError==ERROR_SUCCESS);
}

And there it is. Oh, except the tiny detail of actually listening on the network to service requests, which is the task that we set out to accomplish in the first place.

Super Services

Back in the days of running in device.exe, each network service spun its own thread to accept incoming connections. When a connection arrived, the accept thread would simply spin off a worker thread for processing. Though better than having a process per service, this approach is not ideal. If there were multiple services listening for incoming connections, each service having a thread to listen for an incoming connection was wasteful.

Another disadvantage to the preceding approach is configuration. Suppose you wanted the telnet server to listen on port 553, or the Web server to listen on ports 442-490. You were at the mercy of the service implementer to provide the proper interface to set this up — if there was one. It also added complexity for each service to manage its own accept thread, especially when trying to stop all of the threads of a running service.

Super servers address all of these issues. Rather than have each service accept connections on its own thread, services.exe spins one thread on system startup and listens on a number of sockets, up to 64, for services that request it. When an incoming request arrives, services.exe determines the service associated with the incoming connection and notifies the service. The mapping between TCP ports and services is configured through the registry, under the base service key. In the Finger server example, the registry looks like this:

HKEY_LOCAL_MACHINE\Services\FINGERSERVER\Accept\TCP-79
"SockAddr"=hex:02,00,00,4F,00,00,00,00,00,00,00,00,00,00,00,00

The sockAddr registry value is byte for byte a SOCKADDR structure for the appropriate protocol and contains the address family and the port to listen on. In Windows CE version 4.0, the super server will listen for IPv4 TCP connections only, not UDP and not other protocols. In Windows CE version 4.1 and later, services.exe will also listen on IPv6 TCP addresses. In the previous registry example, services.exe is instructed to listen on the IPV4 port 79 and forward requests to the Finger server. The name of the registry key (TCP-79) is arbitrary. The SockAddr registry value is what services.exe reads, not the key name. This registry key could have been named Horace for all services.exe cares.

The service is notified of the incoming connection by a call to its xxx_IOControl. IOCTL_SERVICE_CONNECTION is set as the incoming code, and the pBufIn parameter is a pointer to the incoming socket. Services.exe calls Accept on the socket before calling xxx_IOControl. At this point, the service spins a worker thread to process the request. It is very important that the service do this. All calls into service's super server handling is performed on one thread. A service blocking its xxx_IOControl when it receives an IOCTL_SERVICE_CONNECTION prevents all other services from receiving incoming connections.:

To implement this in the Finger server, use the following code.

extern "C" BOOL FIN_IOControl(DWORD dwData, DWORD dwCode, PBYTE pBufIn,
             DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
             PDWORD pdwActualOut)
{
    switch (dwCode) {
    // COPY AND PASTE THESE STATEMENTS INTO THE 
    // FIN_IOControl CODE ABOVE
    // A new listen socket has been created and associated 
    // with this service.
    case IOCTL_SERVICE_REGISTER_SOCKADDR:
        if ((g_dwServiceState == SERVICE_STATE_STARTING_UP) || 
            (g_dwServiceState == SERVICE_STATE_ON)) {
            dwError = ERROR_SUCCESS;
            DEBUGMSG(ZONE_NET,(L"FINGERD: SOCKADDR registered\r\n"));
        }
        else {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD:ERROR: SOCKADDR rgstr "
                     L"failed because state != on or starting up.  "
                     L"State = <%d>\r\n",g_dwServiceState));
            dwError = ERROR_SERVICE_NOT_ACTIVE; 
        }
    break;

    // A socket associated with this service is closed.
    case IOCTL_SERVICE_DEREGISTER_SOCKADDR:
        // Since finger service doesn't maintain any state 
        // based on registered sockets, no action is required here.
        DEBUGMSG(ZONE_NET,(L"FINGERD: SOCKADDR deregistered\r\n"));
        dwError = ERROR_SUCCESS;
    break;

    // Services.exe has completed super-service init for this service.
    // Incoming super-service connections can come in at anytime now.
    case IOCTL_SERVICE_STARTED:
        // In real services, you need to be very careful about 
        // state changes and timing issues, and you'll almost 
        // certainly do more work than just changing a var's value.
        // Finger Server will accept connections.
        if (g_dwServiceState != SERVICE_STATE_STARTING_UP) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD:ERROR:IOCTL_SERVICE_STARTED"
                     L" failed because state != starting up. "
                     L"State=<%d>\r\n",g_dwServiceState));
            dwError = ERROR_SERVICE_ALREADY_RUNNING;
        }
        else {
            DEBUGMSG(ZONE_INIT,(L"FINGERD: IOCTL_SERVICE_STARTED "
                     L" changed state to SERVICE_STATE_ON\r\n"));
            g_dwServiceState = SERVICE_STATE_ON;
            dwError = ERROR_SUCCESS;
        }
    break;

    // A new incoming request on a super-service port has arrived.
    case IOCTL_SERVICE_CONNECTION:
    {
        if (dwLenIn != sizeof(SOCKET)) {
            dwError = ERROR_INVALID_PARAMETER;
            break;
        }
        SOCKET sock;

        __try {
            sock = * ((SOCKET*)pBufIn);
        }
        __except (EXCEPTION_EXECUTE_HANDLER) {
            dwError = ERROR_INVALID_PARAMETER;
            break;
        }

        if (g_dwServiceState != SERVICE_STATE_ON) {
            // Do not accept connections unless the service is on

            // ALWAYS close the socket, even if you return 
            // FALSE. Services.exe leaves it to the 
            // service to close the socket in all cases.
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Service Conn fails."
                     L"Service State != ON.  State = <%d>\r\n",
                     g_dwServiceState));
            closesocket(sock);
            dwError = ERROR_SERVICE_NOT_ACTIVE;
            break;                
        }
    
        if (g_hWorkerThread != NULL) {
            // For simplicity, only allow one connection at a time.  
            // Most real servers cannot be this simple.
            DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Finger service is"
                     L" servicing another connection\r\n"));
            closesocket(sock);
            dwError = ERROR_BUSY;
            break;                
        }
        g_hWorkerThread = CreateThread(NULL, 0, FingerWorker, 
                          (LPVOID*) sock, 0, NULL);
        if (g_hWorkerThread == NULL) {
            DEBUGMSG(ZONE_ERROR,(L"FINGERD:ERROR:CreateThread fails."
                     L" GetLastError() returns <0x%08x>\r\n",
                     GetLastError()));
            closesocket(sock);
            dwError = ERROR_INTERNAL_ERROR;
            break;                
        }
        // Worker thread will close the socket.
        DEBUGMSG(ZONE_CLIENT | ZONE_NET,(L"FINGERD: Creates worker "
                 L"thread handle=<0x%08x> to service request\r\n",
                 g_hWorkerThread));
        dwError = ERROR_SUCCESS;
    }
    break;
    }
}

extern "C" DWORD WINAPI FingerWorker(LPVOID lpv)  {
    // In real services, you'd call select() and recv() and process 
    // the request based on client input.  For instance, 
    // the real Finger protocol has a mechanism of specifying that 
    // the client wants information about the user "Bob".
    // To keep things simple, always send the same message.
    SOCKET sock = (SOCKET)lpv;
    // In real services, the worker will release the global critical
    // section quickly (if it needs it at all)
    EnterCriticalSection (&g_cs);

    DEBUGMSG(ZONE_CLIENT,(L"FINGERD: Sending message <%S> to "
                          L"client\r\n",g_szMessage));

    send(sock, g_szMessage, g_ccMessage,0);
    closesocket(sock);
    CloseHandle(g_hWorkerThread);
    g_hWorkerThread = NULL;  
    LeaveCriticalSection (&g_cs);
    return 1;
}

Services.exe Command-Line Interface and Service Configuration

Hooray! The Finger server is now running! But suppose you want to stop and then start it? Or suppose during debugging, you want to unload it, recompile, and then reload the modified DLL? Look at FIN_IOControl earlier in this article, and notice that the Finger server has implemented IOCTLs named SERVICE_IOCTL_STOP and SERVICE_IOCTL_START.

By convention, when a service receives a SERVICE_IOCTL_STOP it should stop whatever it is doing. If there are open network connections, they should be closed and new connections should not be accepted in the case of a network service. SERVICE_IOCTL_START causes a stopped service to be restarted. These IOCTLs do not cause a service to be loaded or unloaded. You can think of the IOCTLs as being a request to pause and resume a service. A service may ignore these IOCTLs entirely if it desires.

Unloading the service is a little trickier. First you'll need the handle that services.exe created when it initially loaded the service. To get this handle, call GetServiceHandle("FIN0:") and pass the return value to DeregisterService. This is not the same handle you'd get when calling CreateFile("FIN0:"). In fact, it's not even a true kernel handle, so don't bother calling CloseHandle. The code is simpler than the description:

    HANDLE h = GetServiceHandle("FIN0:");
    if (h != INVALID_HANDLE_VALUE)
        DeregisterService(h);

You could write a small application that made the appropriate calls to DeviceIoControl or DeregisterService to configure your service. The good news is that you don't have to. In a command prompt on the Windows CE device, you can run services.exe with command line parameters to configure any running service, provided the service implemented the appropriate IOCTL in service.h. Descriptions of all options that can be found by running services help.

Following are some of the options:

services list

FIN0: 0x00040d60 FingerServer.dll Running

Now stop the service. When a service is stopped, services.exe stops listening on any super-service sockets bound to the service. Similarly, when your service is restarted, services.exe will listen to the services again.

services stop FIN0:

services start FIN0:

To unload:

services unload FIN0:

Loading the service after it's been unloaded is slightly different than other management options. Because services.exe doesn't have "FIN0:" in its running table anymore, we'll have to refer to the service by its entry in HKLM\Services\{ServiceName} registry field.

services load FINGERSERVER

Because the telnet server has been hyped up so much in this article, it is fitting that it can be used now. The example of running the services.exe command line were copied and pasted from a telnet session running on a Windows CE device.

Welcome to the Windows CE Telnet Service on jspaithcepc3

Pocket CMD v 4.2

\> services list

FIN0: 0x00040d60 fingerServer.dll Running

HTP0: 0x0004cd00 HTTPD.DLL Running

TEL0: 0x00052da0 TELNETD.Dll Running

TEL1: 0x0005cf90 telnetd.dll Running

FTP0: 0x0004ebd0 FTPD.Dll Off

MMQ1: 0x00057830 MSMQD.Dll Off

NTP0: 0x0005b1c0 timesvc.dll Running

\>

Next, the Finger server is stopped and immediately restarted. Services.exe simply sends an IOCTL_SERVICE_STOP or IOCTL_SERVICE_START to the specified service in this case. Services.exe only prints out text if there was an error. The second attempt to stop FIN0: failed because it was already stopped.

\> services stop FIN0:

\> services stop FIN0:

Operation failed. Error code 0x00000426

\> services start FIN0:

\>

Now unload and then reload the Finger server. Remember that no text is displayed on success.

\> services unload FIN0:

\> services unload FIN0:

FIN0: is not a valid service

\> services load FINGERSERVER

Maintaining State

The Finger server sample has very little state to keep track of — only a status DWORD representing its running state and a buffer containing the data to be sent to clients that query it. Not all services are so simple. Take for example the telnet server. For each telnet session, a copy of cmd.exe is created to process the commands that the user inputs. Inside services.exe, the telnet server maintains for each session its connection socket, a history of recently typed in commands, a handle to the process of cmd.exe that is associated with the session, and other state variables. Typically, a service allocates some resources in its xxx_Open implementation. It will free these resources in its xxx_Close implementation. Because these resources map directly to kernel handles, if a process shuts down without explicitly calling CloseHandle on these handles, the service will still be called with xxx_Close by services.exe. A service should be prepared to have its calling process closed at any stage.

Another, trickier scenario occurs when a process is dying but has a thread blocked while calling into a service. Suppose, for instance, that cmd.exe is blocked while calling ReadFile on a handle returned by the telnet server. The kernel does not kill cmd.exe until worker threads that are blocked in calls into services.exe return. To alert the telnet server that it needs to unblock callers associated with the dying process, the kernel sends the telnet server the IOCTL_PSL_NOTIFY IOCTL. See MSDN documentation for IOCTL_PSL_NOTIFY for the hairy details of all this.

If your service never has code that can be blocked for extended periods of time calling into it (as is the case in our sample), you do not need to worry about this IOCTL.

Conclusion

This article should have conveyed two things. First of all, you should start thinking about Windows CE as a platform for network services. A rich infrastructure, including a Web server, XML, SOAP, and MSMQ are provided out of the box. Second, you shouldn't think of writing a low-level service without writing it for the services.exe framework. Services.exe simplifies the management and development process for writing services, making life easier for both the service implementer and any programmer making use of extensions it provides.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.