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.


October 1999

Microsoft Systems Journal Homepage

Introducing the Terminal Services APIs for Windows NT Server and Windows 2000

Frank Kim

The new Windows Terminal Server APIs were introduced in the Service Pack 4 for Windows NT, Terminal Server Edition. These APIs will be fully supported on Windows 2000. In addi­tion to updating the operating system, the new headers and libraries will be included with the service pack.

This article assumes you're familiar with Windows NT, Platform SDK

Frank Kim is a supporter for Microsoft specializing in kernel/base technologies. He enjoys spending his free time playing the stock market. He can be reached at franki@microsoft.com.

The release of Windows NT® Server, Terminal Server Edition, introduced many Win32® developers and users to the unfamiliar multiuser world. In my previous article, "Run Your Applications on a Variety of Desktop Platforms with Terminal Server" (MSJ, December 1998), I mentioned a future API set that would expose some of the new features of the operating system. Well I'm back to introduce these APIs and to provide even more information on the exciting new world of Windows NT Server, Terminal Server Edition, and Windows® 2000 Terminal Services.
      The new Windows Terminal Server APIs were introduced in the Service Pack 4 that was specifically designed for Windows NT, Terminal Server Edition. These APIs will be fully supported on Windows 2000. Most (but not all) of the new APIs begin with the letters WTS. The WTS APIs are exported from a new wtsapi32.dll included with SP4. The declarations for these new WTS APIs are located in wtsapi32.h. Other new APIs introduced with the service pack will be integrated into existing header files. In addition to updating the operating system, the new headers and libraries will be included with the service pack. Future versions of the Platform SDK will include all of the headers and libraries introduced by this service pack. Note, however, that any comments on Windows 2000 are based on a beta product. The information mentioned in this article may change for the final release.
      There's a slight change in terminology due to the transition from Windows NT to Windows 2000. In addition to the obvious name change, Terminal Server on Windows 2000 is just another component of the operating system, Terminal Services; it is not a separate operating system. This means every Windows 2000 Server has the ability to run Terminal Services just as you can run Message Queuing or Microsoft® Internet Information Services (part of Windows NT Server). These system components can be added through the Windows Component Wizard located in the Add/Remove Programs Control Panel applet (see Figure 1).
Figure 1 Adding Terminal Services
      Figure 1: Adding Terminal Services

      Before I begin, I need to discuss how an application can detect whether it is running in a Terminal Services environment. This is important since you may want the flexibility of running your application on different Windows NT platforms. As I mentioned earlier, the wtsapi32.dll will be introduced with a special SP4 release specifically for Terminal Services. On all other Windows NT platforms, if your application has implicitly linked to wstapi32.dll, the application will fail to run without this DLL.

Detecting Terminal Services
      On Windows NT 4.0, you can detect Terminal Services by examining the registry key HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Control\Product Options. If the ProductSuite registry value contains the string "Terminal Server", the system is running Terminal Services. On Windows 2000, you must use the new VerifyVersionInfo API since querying the registry will not work.

 BOOL VerifyVersionInfo(
     POSVERSIONINFOEX lpVersionInformation, 
     DWORD dwTypeMask, 
     DWORDLONG dwlConditionMask)
This is a necessary API with the proliferation of versions and varieties of Windows NT platforms. This API replaces the many API calls required to determine if the running system fulfills your application's requirements. VerifyVersionInfo wraps all this in one convenient API.
      The first parameter is a pointer to an OSVERSIONINFOEX structure initialized with the attribute you want to see on the currently running system.
 typedef struct _OSVERSIONINFOEXA {
     DWORD dwOSVersionInfoSize;
     DWORD dwMajorVersion;
     DWORD dwMinorVersion;
     DWORD dwBuildNumber;
     DWORD dwPlatformId;
     CHAR   szCSDVersion[ 128 ];   
         // Maintenance string 
         // for PSS usage
     WORD   wServicePackMajor;
     WORD   wServicePackMinor;
     WORD   wSuiteMask;
     BYTE   wProductType;
     BYTE   wReserved;
 } OSVERSIONINFOEXA, *POSVERSIONINFOEXA, 
     *LPOSVERSIONINFOEXA;
As you can see in the structure, you can base your application's requirements on various operating system attributes. In this case I'm interested in whether the system is running Terminal Services, so I've set the wSuiteMask member to VER_SUITE_TERMINAL. The next parameter indicates what member in the OSVERSIONINFOEX structure the API should examine. I've set this to VER_SUITENAME since I'm interested in the wSuiteMask member. Finally, a 64-bit comparator mask compares the specified member in OSVERSIONINFOEX with a specified comparator. The mask is created with the macro VER_SET_ CONDITION. The macro takes the interested member and the specified comparator and builds the mask. This could be, for example, VER_EQUAL if you're interested in a specific version number, or for determining a specific platform such as Terminal Services you use the VER_AND operator.
 dwlConditionMask = VER_SET_CONDITION(
     dwlConditionMask, VER_SUITENAME, VER_AND);
 ZeroMemory(&osVersionInfo, sizeof(osVersionInfo));
 osVersionInfo.dwOSVersionInfoSize = sizeof(
     osVersionInfo);
 osVersionInfo.wSuiteMask = VER_SUITE_TERMINAL;
 bResult = VerifyVersionInfoA(
     &osVersionInfo, VER_SUITENAME, dwlConditionMask);
Session Overview
      Every interactive user is associated with a session object. Like processes, sessions are identified by both a numerical ID and a name. Sessions can either be local or remote. The local session is associated with the interactive user physically logged on to the machine. Service processes are also assigned to the local session. This session is always assigned the ID of 0, and is also referred to as the console. Like many other Windows NT objects, the session object has security associated with it. Unfortunately, there are no APIs to directly manipulate it. Since this functionality is not available, the security access rights are not defined anywhere. This isn't new; the Service Controller does not have any security manipulation APIs, although there is security associated with it as seen through OpenSCManager. (See Knowledge Base article Q179249 for more information.)
      The reason I said you can't manipulate it directly is because if you carefully examine the various applications included with the operating system, the Terminal Server Connection Configuration is able to modify user access to the session object. Two UI transport protocols are available: RDP and ICA (Independent Computing Architecture). I will be focusing on the RDP transport as shown in Figure 2.
Figure 2 Terminal Server RDP Configuration
      Figure 2: Terminal Server RDP Configuration

      Before I continue, I'll warn you that the following information may change in the future, so use it at your own discretion.
      Well, after poking around the system, I discovered the security descriptor for the session object was stored in the registry. This is the same technique used for storing DCOM object security descriptors. The system may store two security descriptors, a default and a modified security descriptor. The default security descriptor is located under HKEY_ LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ Terminal Server\WinStations. The security descriptor is stored as a binary value, DefaultSecurity. The modified security descriptor is stored under HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Control\TerminalServer\ WinStations\RDP-tcp. The binary value is named Security.
      If the security has not been changed, the system uses the default security descriptor. If it has the modified binary value, it is created and stored under the specific transport. This means if it has not been changed and you are changing the security descriptor, you will have to create the Security value. By the way, only Administrators and the LocalSystem account have write access to this registry key. Changes to the security descriptor are not dynamic. Any clients connected must reconnect in order for the security changes to take effect.
      The session object default security descriptor is set up as shown in Figure 3.
      I was able to obtain security access rights for the session object by creating ACEs with just the individual access right and then dumping the DACL to obtain the values.
 QUERY_INFORMATION 0x00000001
 SET_INFORMATION   0x00000002
 RESET             0x00000004
 VIRTUAL           0x00000008
 SHADOW            0x00000010
 LOGON             0x00000020
 LOGOFF            0x00000040
 MESSAGE           0x00000080
 CONNECT           0x00000100             
 DISCONNECT        0x00000200
For those who are not familiar with Windows NT security, the security descriptor can be in one of two formats, self-relative and absolute. In the self-relative format, the security descriptor contains offsets to its various parts stored in one contiguous buffer, while the absolute format contains pointers to the various parts stored in individual buffers (see Figure 4).
Figure 4 Descriptor Formats
      Figure 4: Descriptor Formats

      The security descriptor can only be stored in the registry in the self-relative format. On the other hand, the Win32 security APIs work with security descriptors in the absolute format. When extracting a security descriptor stored in the registry, it needs to be converted with the MakeAbsoluteSD API. Once you are done with the security descriptor and it is ready to be stored in the registry, it can be converted back with MakeSelfRelativeSD.
      One reason why the security access rights may not be defined is the fact that none of the WTS APIs that return a handle allow you to specify the security access requested for the returned handle. The absence of access rights gives a false sense of security. One thing I can guarantee is that if you are developing on Windows NT, security is always an issue. My job will be to mention the security implications for the various new APIs as I come across them.
      On Windows 2000, a new group security identifier (SID) has been created, the Terminal Server User, S-1-5-13. This is similar to other well-known group SIDs such as INTERACTIVE and SERVICE. A specialized group SID allows the system to mark a specified logon based on a type. You can consider it an attribute. Why would the system have these groups? Maybe an application requires that only the interactive user access a file and it doesn't want any users on the network to access it. You cannot easily do this with a local or global group. In the case of a Terminal Services user, any interactive user connected to Terminal Services will have this group SID. If you are interested in obtaining this SID, it can be extracted from GetTokenInformation with the TokenGroups class.
      The system assigns session IDs to all running processes. The system uses this ID to determine user ownership of a process. This ID is stored in the process environment block (PEB). The API to obtain the ID is ProcessIdToSessionId. The caller needs PROCESS_QUERY_INFORMATION access to the targeted process. One thing to note is that this API is exported in kernel32.dll and defined in winbase.h. This means if you are writing an application that will run on different versions of Windows NT, you will have to dynamically load this function for your application to run.
      On Windows 2000, the session ID is also stored in the process token. You can obtain the session ID with GetTokenInformation specifying the TokenSessionId class. The session ID will not change if the token is duplicated or impersonated. Any child processes launched by the process will continue to have the same ID. A process's session ID can be changed through the SetTokenInformation API.
      If you are not interested in the specific session ID, but only whether your application is running in a remote session, GetSystemMetrics has been modified to handle the new flag SM_REMOTESESSION. The API will return TRUE if the application is running in a remote session. This is a handy API for an application that displays a large high-color bitmap during startup. The application could disable this feature when running in a remote session to reduce the amount of data transmitted over the network.

Terminal Services Basics
      Before I jump into all the new and cool WTS APIs, I need to review some items that you should consider when running in the Terminal Services environment. First, remember that instead of just one interactive user, you may have many of them. This has several important implications. Sharing files in a system directory such as %systemroot% or any of its subdirectories is a bad idea. For those who have not seen %blah%, this refers to a defined environment variable. In this example, the environment variable %blah% could be C:\blah.
      Since many applications already share files in %systemroot%, Terminal Services gets around this problem by virtualizing the windows directory. The system creates a new windows directory in the user's %USERPROFILE% directory that is defined as %homedrive%\%homepath%. The %homepath% is defined differently on Windows NT 4.0 and Windows 2000. On Windows NT 4.0 it is defined as %systemroot%\profiles\%username%, while on Windows 2000 it is defined as "document and settings\%username%". Definitely stay away from hardcoding these paths and stick with the Windows directory returned by GetWindowsDirectory. On Windows 2000, you should use SHGetFolderPath with CSIDL_WINDOWS to obtain the Windows Directory. For the developers who insist on getting the real Windows directory, a new API called GetSystemWindowsDirectory has been created. This will return you the real Windows directory.
      In addition, the actual Windows directory is available by switching the system from execute mode to install mode by using the change.exe utility included with the system. The command to change the system to install mode is "change user /install". These modes were introduced to allow legacy applications to properly install in a Terminal Services environment. Not only does it preserve registry settings, but it also forces GetWindowsDirectory to return the true Windows directory since you may have files that are shared by all users, such as DLLs.
      Windows 2000 will provide another power feature for developers, the new linker flag TSAWARE. Supposedly, once the linker supports this flag, an application linked with it will be marked as Terminal Services-aware. This means the application will get the same information as if it was running on a non-Terminal Services platform. Essentially, the application is telling the system that there is no need to pamper and protect it; it knows what it is doing.
      Another important issue is named objects. On Windows NT, there is only one namespace. All processes running as their respective user share the namespace. If a process creates a named object, any process running on the system can view the named object. This also implies that once the name is used, other processes cannot use the same name. On Terminal Services this is not the case. Every session is given a unique namespace. This allows the same process launched by two different interactive users to create the same named object in their unique namespaces. If a process chooses to share the named object with other processes running in other sessions, the special keyword Global\ can be prepended to the named object. This notifies the system to create this object in the global namespace. The one exception is the console session. By default, any named objects created in the console session are created in the global namespace.
      At this point I want to talk about the confusing differences and changes in the namespace moving toward Windows 2000. What I've found is that a special namespace exists for processes running in the LocalSystem security context. I've previously stated that all processes launched from the console session (including services) share the global namespace. This is not entirely true. Services running in the LocalSystem account default to a global namespace. Non-LocalSystem account services and processes originating from the console session use another global namespace. The confusing part is that this namespace can sometimes act like a per-session namespace.
      Here is an example. If a process running from a remote session specifies the Global keyword, which global namespace does the system use? Well, it turns out the system first checks the namespace for the LocalSystem account. If the object does not exist in that namespace, it checks the console session's global namespace. If the object does not exist in either, the system creates the global object in the local system namespace. Confusing, eh? The simple and easy rule is to always use the Global keyword if you want to share your named object with the entire system.
      Adding to the confusion is another new keyword, Local. This tells the system to create the named object in the local namespace. This keyword should be used specifically for a LocalSystem process. This allows a process in the console session to create a named object with any keywords, and a LocalSystem process can access this named object with the Local\ keyword prepended to the name. (If you don't prepend Local\ to the name, it will be created in the LocalSystem global namespace.)
      The good news is that Windows 2000 has only one global namespace associated with the console session. So with Windows 2000 you can disregard all the confusion with the LocalSystem account global namespace.

Terminal Services Administration
      The first question that comes up whenever a new operating system is released is how can my application get the same information as the bundled applications. The Terminal Server Administration application (see Figure 5) provides important system information for both local and remote servers. This information is related specifically to sessions and processes.Figure 1).

Figure 5 Terminal Server Administration
      Figure 5: Terminal Server Administration

      One great feature available with the WTS APIs is the ability to obtain information on remote servers. But how can you find out which computers on the network are using Terminal Services? This information can be obtained through NetServerEnum by specifying SV_TYPE_TERMINALSERVER. If you haven't noticed, this is a Lan Manager API. Note that the API is UNICODE-only and the API allocates the buffer for you. This means when you're finished, make sure to call NetApiBufferFree.
      Since I'm on the subject of freeing memory, many WTS APIs allocate a buffer for you. Unlike many Win32 APIs, it is not necessary to guess the buffer size or call the API twice (the first time to determine the correct buffer size and the second time to obtain the information). Once you've finished with the returned buffer, call WTSFreeMemory to deallocate it.
      The next set of APIs requires a server handle. The API to obtain a handle is WTSOpenServer.
 HANDLE WTSOpenServer(LPTSTR pServerName)
The API requires only one parameter, the server name. The documentation specifies a NETBIOS name, which essentially means the computer name.
      As I mentioned before, the API is missing a parameter specifying security access for the returned handle. This does not mean you have free reign on the machine. By default, the Administrator group and LocalSystem account can call all the APIs and receive all available information. The LocalSystem account, of course, can only access the local machine. Remember, it does not have any network access to Windows NT LanMan secured resources due to NULL credentials. The user group on the targeted machine will be allowed to call any APIs just like the Administrator group or LocalSystem account. (I will mention any exceptions.) Of course, you can always change the security to allow a specific user to do anything you want.
      If you are only interested in the local machine, there is no need to call WTSOpenServer. You can just use the constant WTS_CURRENT_SERVER_HANDLE. Make sure to call WTSCloseServer if a handle was obtained successfully with WTSOpenServer.
      Once you have a handle, what kind of session information can you acquire? WTSEnumerateSessions returns a list of sessions. Depending on the caller's security context, the API behaves differently. Unlike Win32 APIs that just return "Access Denied," the API will return different results depending on the caller. First, the caller must have at least QUERY_INFORMATION access. Now, if it is either the LocalSystem or Administrator group, it will return all sessions on the system. If not, it returns only your session. Without the access, the caller will succeed in calling the API, but it will return no sessions. The information is returned as an array of WTS_SESSION_INFO structures.
 typedef  struct _WTS_SESSION_INFO{
     DWORD SessionId;
     LPTSTR pWinStationName;
     WTS_CONNECTSTATE_CLASS State;
 } WTS_SESSION_INFO, * PWTS_SESSION_INFO;
The pWinStationName is not a window station name as in desktops and window stations. This is the session name. The documentation refers to it as a WinStation. Most of the possible states in the WTS_CONNECTSTATE_CLASS are self-explanatory. The only state I will mention is WTSShadow. This is a feature that existed in Windows NT 4.0, but has finally been implemented for Windows 2000. Shadowing allows a user to actually connect to another user's session and watch it. In addition to viewing the session, you can actually control their session and the user can observe your actions.
      If you want more detailed information for a specific session, you can call WTSQuerySessionInformation. The API will return specific information for a targeted session ID based on the WTS_INFO_CLASS value—again, as long as you have QUERY_INFORMATION access. It will return error code 5 or "Access Denied" if you do not.
 WTS_INFO_CLASS {
     WTSInitialProgram,
     WTSApplicationName,
     WTSWorkingDirectory,
     WTSOEMId,
     WTSSessionId,
     WTSUserName,
     WTSWinStationName,
     WTSDomainName,
     WTSConnectState,
     WTSClientBuildNumber,
     WTSClientName,
     WTSClientDirectory,
     WTSClientProductId,
     WTSClientHardwareId,
     WTSClientAddress,
     WTSClientDisplay,
 } WTS_INFO_CLASS;
      If you are only interested in your session, the WTS_CURRENT_SESSION constant can be substituted for the actual session ID. The constant is usable with any WTS API that requires a session ID. You'll find all the enumerator values are self-explanatory. The only interesting enumerator is the WTSClientAddress. Specifying this enumerator returns the following structure.
 typedef struct _WTS_CLIENT_ADDRESS {
     DWORD AddressFamily;
     BYTE Address[20];
 } WTS_CLIENT_ADDRESS,  * PWTS_CLIENT_ADDRESS;
      To determine the client's IP address, the Terminal Services client must be connected to the server via TCP/IP. If the AddressFamily returns AF_INET, this statement is true. The IP address is located in bytes 2, 3, 4, and 5. The other bytes are not used. If AddressFamily returns AF_UNSPEC, the first byte in Address is initialized to zero.
      As I mentioned earlier, services are associated with the console session. I stated in my previous article that it was impossible for a service to display a message box in a remote session. Even the MB_SERVICE_NOTIFICATION flag in conjunction with MessageBox could not accomplish this task. This has changed in Windows 2000. MB_SERVICE_ NOTIFICATION will display the message box in the session associated with the caller. This means that if the caller is impersonating a user running in a remote session, the message box will appear in that user's session. The other method available now—and introduced through a new API—is WTSSendMessage. The API will display a message box for a targeted session. The caller must have MESSAGE access. If not, the API returns error code 87 or "invalid parameter" instead of "Access Denied." One major difference from MessageBox is the option to display the message for a set timeout period. After it has expired, the message box disappears. It is not necessary to rely on the user to close the message box or spawn an extraneous thread to get rid of it.
      Finally, you can disconnect or log off a session. Remember, in a disconnected session, the system maintains the user's session so that if they reconnect at a later time the user will be in the same state. All running applications and their desktop positions will be preserved. The two APIs that accomplish these tasks are WTSDisconnectSession and WTSLogoffSession. The disconnect requires DISCONNECT access, but the logoff requires DISCONNECT, RESET, and LOGOFF access. This makes sense since a logoff includes both a reset and a disconnect. Without proper access, the APIs fail with "Access Denied." Both APIs allow you to synchronously wait for them or return immediately. If you want to verify that the API succeeded, you can query the session state with WTSQuerySessionInformation. The API will fail for the logoff since it doesn't exist anymore. Both APIs only allow the Administrator group or the LocalSystem to disconnect or log off an interactive system user.
      There are two WTS APIs specific to processes, WTSEnumerateProcesses and WTSTerminateProcess. If you are looking to enumerate processes or terminate a specific process, you'll find these APIs very helpful. In fact, these APIs have some advantages over the standard Win32 APIs for accomplishing the same tasks. For enumerating processes, you'll find WTSEnumerateProcesses easier to use than the PSAPI APIs or the performance data registry. Like the performance data registry, enumerating processes on a remote server is possible. The other big bonus is the information returned from the API.
 typedef struct _WTS_PROCESS_INFO {
     DWORD SessionId;
     DWORD ProcessId;
     LPTSTR pProcessName;
     PSID pUsersSid;
 } WTS_PROCESS_INFO, * PWTS_PROCESS_INFO;
      The pUsersSid saves you the trouble of calling several APIs if you are interested in a process's security context. It is not necessary to obtain a process token and then call GetTokenInformation. In addition, since the WTS APIs are implemented through a corresponding server service, the API will be executed in the LocalSystem security context. This allows you to obtain the security context of all running processes, which is important since you need PROCESS_ QUERY_INFORMATION access to the process handle and TOKEN_QUERY access to the process token. The LocalSystem account by default always has this access for both objects in every process. Essentially only the LocalSystem account can easily obtain this information. In fact, even the Administrator group does not have this access. Other users can get all the SIDs, but this API saves you a lot of work.
      The power of WTSTerminateProcess is the ability to terminate processes remotely. Currently, TerminateProcess can't do this. To remotely terminate a process you need the cooperation of a remote process on the targeted server to execute the task. If you are running in a Terminal Services environment, you can take advantage of this feature. One interesting feature is that the API does not allow you to terminate a process running in the LocalSystem security context. It will return error code 5 or "Access Denied." The API assumes LocalSystem represents a system process and that terminating a system process would be bad. The caller terminating the process requires PROCESS_ QUERY_INFORMATION and PROCESS_TERMINATE access. The API will attempt to take advantage of the debug privilege to guarantee that a process handle is obtained if the user has been granted the privilege.
      The release of Windows NT Server 4.0, Terminal Server Edition, added various new user account attributes specific to Terminal Services as shown in Figure 6. Prior to SP4, the only way a developer could query or set these attributes was through the tsprof.exe utility. The two APIs to set and query these attributes are WTSQueryUserConfig and WTSSetUserConfig. The APIs set and query attributes for a user on a targeted server, but they do not require a server handle like the other WTS APIs. If you are working with a domain user, you'll have to get the actual server name for the domain. This information can be obtained with NetGetDCName.Figure 1).
Figure 6 User Account Attributes for Terminal Services
      Figure 6: User Account Attributes for Terminal Services

      The two APIs query and set a specific attribute based on the WTS_CONFIG_CLASS enumerated type (see Figure 7). I will not go into any depth on the various enumerated types. Ordinary users can only set configuration information on their account.

Terminal Services Odds and Ends
      The final two APIs do not fit into any of the previous topics so I've left them for last. WTSWaitSystemEvent is a handy API.

 BOOL WTSWaitSystemEvent(HANDLE hServer,
                         DWORD EventMask, 
                         DWORD *pEventFlags);   
It allows an application to know when various events occur in Terminal Services. For example, maybe your application monitors all the interactive users on the system and you want to know when user X logs on. Using this API, you can be notified when a user logs on or off the system. When this occurs, you can enumerate all running processes and examine the user's SID in each of them to determine if user X is now on the system. This is a useful API since there isn't a really easy method to determine when a user interactively logs on to the system.
      The API works differently from other Win32 APIs that wait for various events to occur such as RegNotifyChangeKeyValue, FindFirstPrinterChangeNotification, ReadDirectoriesChangesW, or FindFirstChangeNotification. All these APIs provide the flexibility of waiting asynchronously through either a waitable handle, an event object, or overlapped I/O. Unfortunately, WTSWaitSystemEvent does not. Like all event notifying APIs, the EventMask parameter is a filter to indicate which events you're interested in. Here's a list of these events:
 WTS_EVENT_CREATE
 WTS_EVENT_DELETE
 WTS_EVENT_LOGON
 WTS_EVENT_LOGOFF
 WTS_EVENT_CONNECT
 WTS_EVENT_DISCONNECT
 WTS_EVENT_RENAME
 WTS_EVENT_STATECHANGE
 WTS_EVENT_LICENSE
 WTS_EVENT_ALL
      The API takes an additional DWORD parameter that is initialized by the API when an event occurs. The API call can be canceled by making another call to WTSWaitSystemEvent with the EventMask set to WTS_EVENT_ FLUSH. This API call will return an EventFlags initialized to WTS_EVENT_NONE. The blocked API call will return with an error code of 995, or ERROR_OPERATION_ ABORTED. The cancel will only work on all APIs that have used the same handle as the flush. This includes using WTS_CURRENT_SERVER_HANDLE and obtaining a handle to the local machine with WTSOpenServer.
      The other miscellaneous API is WTSShutdownSystem. You may be wondering why another shutdown API has been added to an already crowded field that includes InitiateSystemShutdown and ExitWindowsEx. One reason is that WTSShutdownSystem allows a user to shut down or reboot the machine. This means the user must have the SE_SHUTDOWN_NAME privilege and it must be enabled just like the other shutdown APIs. WTSShutdownSystem adds several new interesting features. The WTS_ WSD_LOGOFF flag allows the console user to log off all remote session users and not allow them to reconnect until the machine is rebooted. This flag can only be called from a process running in the console session. The API will succeed from a remote session, but nothing will happen.
      If you're wondering why a flag like WTS_WSD_LOGOFF is needed, imagine installing some new special software that requires a reboot. Well, it would be annoying if you kicked the users off, but they logged back on while you're in the process of installing the software. The system administrator could prevent users from logging on again through their user settings. This would have to be done for each user. Then, when the software was installed, the settings would have to be changed again to allow users access. This would be a pain, and most system administrators would love for the system to take care of this for them.
      If you're wondering which shutdown API to call, I've summarized their special features in Figure 8.

Virtual Channels
      Virtual channels are a new feature available only for Windows 2000. In my previous article, I mentioned the ability of RDP to support multiple channels of data. Well, Windows 2000 Terminal Services will allow you to take advantage of them. This is a new feature of RDP 5.0 and it is not available in the RDP 4.0 used in Windows NT Server 4.0, Terminal Server Edition.
      Virtual channels will allow a user-mode application or kernel-mode device driver to communicate with a corresponding application running on the client. This will allow an application in a Terminal Services session to take advantage of the local resources available on the client. A great example of this is the ability to cut and paste data from an application running in Terminal Services to an application running on the client machine. The system takes advantage of virtual channels to pass clipboard data between the two applications. The following discussion will only focus on user-mode applications running in remote sessions and not kernel-mode device drivers.
      A virtual channel application consists of two components: a client-side extension DLL loaded by the Terminal Services client during initialization and a server-side application running in the Terminal Services session (see Figure 9). The client extension DLL must be registered on the client system. This information is stored in the registry key HKEY_CURRENT_USER\Software\Microsoft\Terminal Server Client\Default\AddIns\<name>. The subkey <name> can be defined as anything. Under this key you'll find a value called Name of type REG_SZ. The Name value contains the client extension DLL's path and name. By placing the information under the Default subkey, this will load the client extension DLL for all sessions running on the client. The client extension DLL can be targeted for a specific connection.

Figure 9  Using Virtual Channels
      Figure 9: Using Virtual Channels

      The Client Connection Manager allows you to create a customized connection. The registry location is the same except the subkey is created under the connection name instead of Default. The same registry value should be created. The client extension DLL is only loaded when the Terminal Services client starts; client extension DLLs cannot be loaded dynamically. The Terminal Services client must be restarted to take advantage of a new client extension DLL. You can write a client extension DLL for 16-bit windows, but that's all I'll say about it.
      The client extension DLL is required to export one function, VirtualChannelEntry. This function has one parameter, a pointer to a CHANNEL_ENTRY_POINTS structure.
 typedef struct tagCHANNEL_ENTRY_POINTS
 {
     DWORD cbSize;
     DWORD protocolVersion;
     PVIRTUALCHANNELINIT  pVirtualChannelInit;
     PVIRTUALCHANNELOPEN  pVirtualChannelOpen;
     PVIRTUALCHANNELCLOSE pVirtualChannelClose;
     PVIRTUALCHANNELWRITE pVirtualChannelWrite;
 } CHANNEL_ENTRY_POINTS, FAR * PCHANNEL_ENTRY_POINTS;
This function is called when the DLL is loaded by the Terminal Services client. The structure is initialized by the system. In addition to providing the structure size and the protocol version, it provides four pointer functions that the client DLL will utilize. The protocol version will currently return VIRTUAL_CHANNEL_VERSION_WIN2000. By providing the function pointers within the DLL, it will continue to work with future Terminal Services client versions. To eliminate any confusion, pVirtualChannelInit will be known as just VirtualChannelInit. This applies for the other three functions as well. One thing your client DLL must do is copy the CHANNEL_ENTRY_POINTS structure into a private buffer since you cannot rely on the system keeping this structure around.
      The first item the client must address is initializing the virtual channel by calling VirtualChannelInit. This must be done before the client connects to the server. This requires the function to be called from the VirtualChannelEntry function.
 UINT VirtualChannelInit (
     LPVOID *ppInitHandle,
     PCHANNEL_DEF pChannel,
     INT channelCount,
     ULONG versionRequested,
     PCHANNEL_INIT_EVENT_FN pChannelInitEventProc
 );
If it is not called from VirtualChannelEntry, the function will return CHANNEL_RC_NOT_IN_VIRTUALCHANNELENTRY. If VirtualChannelInit succeeds, the return value will be CHANNEL_RC_OK.
      The function sets up several important items. First, it returns a handle that will be used later to open any virtual channels defined by this function. Next, you define one or more channels, up to a maximum of CHANNEL_MAX_ COUNT. This is defined as 30. In fact, the system has 32, but two are taken up by the system. Two additional channels are used by Microsoft for the clipboard and printer redirection. This really leaves 28 available channels. The channel is defined through a CHANNEL_DEF structure.
 typedef struct tagCHANNEL_DEF {
     char   name[CHANNEL_NAME_LEN + 1];
     ULONG  options;
 } CHANNEL_DEF, * PCHANNEL_DEF, * * PPCHANNEL_DEF;
      The channel name can have a maximum length of CHANNEL_NAME_LEN or seven characters. The name is defined as an ANSI string. The other member options define the properties for the channel. This includes the data priority, the encryption level, and the compression level.
      Another property is CHANNEL_OPTION_SHOW_PROTOCOL. When this property is specified, data sent from the client to the server will be preceded by a CHANNEL_PDU_ HEADER. This structure will contain both the length of the data block and information about it. I will discuss the importance of specifying this later.
      Finally, you specify an application-defined VirtualChannelInitEvent callback function that the Terminal Services client calls to notify your DLL of a variety of different virtual channel events. This callback function will be the heart and soul of the client extension DLL. The callback function VirtualChannelInitEvent returns a notification event, the initialization handle (the same handle returned from VirtualChannelInit), and any data associated with the specific notification event.
      When the Terminal Services client has completed initialization, the DLL will be notified with a CHANNEL_ EVENT_INITIALIZED event. At this point, the client extension DLL does not obtain a channel handle until the Terminal Services client connects to a server. When this occurs, the client extension DLL could receive one of two events, CHANNEL_EVENT_V1_CONNECTED or CHANNEL_EVENT_CONNECTED. A V1 connected event indicates that the server is an older version that does not support virtual channels. Once CHANNEL_EVENT_CONNECTED is received, you can proceed to obtain a handle to a virtual channel. pData will point to the server name, which is an ANSI string.
      Before I discuss sending and receiving data through a virtual channel, I will mention the two other notification events. Once the Terminal Services client disconnects from the server, a CHANNEL_EVENT_DISCONNECTED will be sent. When the Terminal Services client ends, a CHANNEL_EVENT_TERMINATED is sent.
      I have now explained the different notification events that can be sent by a Terminal Services client. As I mentioned earlier, establishing the channels should be done after the CHANNEL_ EVENT_CONNECTED notification. A channel is opened though the VirtualChannelOpen function. You must provide the channel name and the init handle returned from either the VirtualChannelInit call or the pInitHandle parameter if you are doing this from the VirtualChannelInitEvent callback function. The VirtualChannelOpen function requires another application-defined callback function, VirtualChannelOpenEvent, which will be called by the Terminal Services client when receiving data and when writing data to the channel has completed. If the function succeeds, it will return a handle that can be called by either VirtualChannelWrite or VirtualChannelClose when it's finished with the channel.
      If you closely examined the functions returned in the CHANNEL_ENTRY_POINTS structure, you might have noticed that there wasn't a VirtualChannelRead function. Well, the VirtualChannelOpenEvent callback function serves two purposes: to receive data from the server application, and to notify the client extension DLL when data written by the client has been completely written to the virtual channel.
      Data is sent to the server via VirtualChannelWrite. The call is asynchronous. When the data is completely written to the channel, your VirtualChannelOpenEvent callback function receives a CHANNEL_EVENT_WRITE_COMPLETE. In addition to specifying the data to be written in pData, the pUserData parameter is used to determine which writes have completed in the VirtualChannelOpenEvent callback function. This will allow you to do multiple writes and easily determine which writes have completed. One thing to watch out for is that the buffer for pData must not be freed or reused until your callback function is notified. This means you should not use the stack unless the function making the write is going to block until the callback function is notified.
      A CHANNEL_EVENT_WRITE_CANCELLED will be received by your client extension DLL when the client session is disconnected and there is a write pending. This will give you an opportunity to free the pData buffer associated with the write.
      When the client extension DLL receives data, the VirtualChannelOpenEvent callback will return with CHANNEL_ EVENT_DATA_RECEIVED. No more than CHANNEL_ CHUNK_LENGTH (currently defined as 1600 bytes) will be received. The totalLength parameter will contain the actual number of bytes written by the server, while dataLength will be the size of the chunk. The dataFlags parameter indicates what kind of chunk has been sent. This could be the first chunk (CHANNEL_FLAG_FIRST), the middle chunk (CHANNEL_FLAG_MIDDLE), or the last chunk (CHANNEL_ FLAG_LAST). If the parameter returns CHANNEL_ FLAG_ONLY, the chunk contains all the data written by the server. Instead of keeping track of the total number of bytes read in comparison to the total bytes sent, you could just look for the first chunk, read all the middle chunks, and then end the read when you receive the last chunk.
      Once you are done with the channel, make sure to close (using VirtualChannelClose) the init handle returned from VirtualChannelInit.

The Virtual Channel Server Application
      Taking advantage of virtual channels means using two components: the client extension DLL and a server application. Like the client extension DLL, the server application needs to be registered with Terminal Services. The registry location is HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Terminal Server\AddIns \<name>, where <name> can be anything. This new subkey requires two values. The Name value is defined as a REG_SZ type, and the Type value is a REG_DWORD. The Type value is initialized to 3. This indicates a user-mode application to the system.
      The server application is allowed to create two events that will notify it when the session has been disconnected and reconnected. When a user decides to disconnect instead of logging off, all processes in the session continue to run as if the user was still connected. The two events will allow your server application to respond appropriately. The event names are global and are based on a combination of the session ID and the server registration name that is defined in the Name value.

 Global\<Name>-<session id>-Reconnect
 Global\<Name>-<session id>-Disconnect
The session ID can be obtained from a variety of APIs including ProcessIdToSessionId, WTSQuerySessionInformation, or WTSEnumerateProcesses. Remember that the named events are case-sensitive, so make sure to pay attention to name set in the Name value.
      To communicate with the client extension DLL, the server application needs to open the virtual channel. This is done through WTSVirtualChannelOpen. The API requires a server handle, the session ID, and the virtual channel name for connecting to the correct client extension DLL. This means the server application can connect to a client extension DLL running in a different session as long as the other remote session is connected to the same server. As with any Win32 object handles, once you are done, the handle should be closed with WTSVirtualChannelClose.
      Once you have a handle to the virtual channel, the server application can read and write to the channel with WTSVirtualChannelRead and WTSVirtualChannelWrite. WTSVirtualChannelRead is different from other Win32 I/O APIs that you maybe familiar with, including ReadFile and ReadConsole. WTSVirtualChannelRead includes a TimeOut parameter. If there is no data to read in the virtual channel, the API will block. With the TimeOut parameter, an application can specify how long it is willing to wait for data before returning.
      Earlier I mentioned how the client extension DLL can include the CHANNEL_OPTION_SHOW_PROTOCOL. Let me explain the importance of this option. Since data from a single write in a virtual channel cannot be greater than CHANNEL_CHUNK_LENGTH, the system will break up the data. WTSVirtualChannelRead has only one parameter indicating the number of bytes read. Well, if the system breaks up the write, this leads to a couple of questions. How many times does the server app read the channel to correct the broken write from the client extension DLL? By specifying CHANNEL_OPTION_SHOW_PROTOCOL, the system appends a CHANNEL_PDU_HEADER structure to the data read in the API. The structure will tell you how many total bytes were written and provides information about the data. This is the same information as on the client extension DLL side. It tells you whether the chunk is a first, middle, last, or that it was entirely sent.
      One thing to watch out for is that the bytes read by the API include the CHANNEL_PDU_HEADER structure. This is an additional 8 bytes. You will have to subtract this from the actual bytes read in the virtual channel to determine how many bytes are left from the client extension DLL.
      The only item to mention in regard to WTSVirtualChannelWrite is that it is a synchronous call.
      There are some other miscellaneous virtual channel APIs I should mention. WTSVirtualChannelPurgeOutput and WTSVirtualChannelPurgeInput allow you to flush any queued data. Output represents data sent from the server application to the client extension DLL, and input is data sent from the client extension DLL to the server app.

Conclusion
      The new APIs introduced with Terminal Services, Service Pack 4, provide some exciting opportunities for exploiting specific Terminal Services features. As usual, forward progress means changes. You should now understand some of the differences between Terminal Services in Windows NT 4.0 and Windows 2000, as well as a preview to Virtual Channels. It is always good to have an understanding of both the APIs and the underlying operating system.


For related information see: Terminal Services at: http://msdn.microsoft.com/library/psdk/termserv/termserv_652r.htm.

  Also check http://msdn.microsoft.com for daily updates on developer programs, resources and events.


From the October 1999 issue of Microsoft Systems Journal.