Click here to Skip to main content
Click here to Skip to main content

Windows-based application over Terminal Services using WtsAPI32

, 6 Oct 2003
Rate this:
Please Sign up or sign in to vote.
Application management into Windows Terminal Services environment.

Introduction

I want to propose an application that runs in a Terminal Services environment. It’s a small Windows Terminal Services monitor aware if some session tries to run another instance of it.

Background

Terminal Services is a multi-session environment that provides remote computers, access to Windows based programs running on the server. Because of this, that application should know if from the console or from some other session someone runs an instance of it and act in consequence. WTSMonitor - with the help of a mutex - is developed in this way. Also I want to demonstrate how to use the WTS API to display information about the TS activity ("Session Name", "Session ID", "State", "User", "Station", "Domain", "Type", "Client Product ID", "Client Display Info", "Client Address Info", "Client Folder") or to do some actions ("Send Message", "Terminate Process").

Using the code

From a Terminal Service client session or from the console, running the first instance of WTSMonitor (that opens in the tray!) we will see a dialog CWTSMonitorDlg with the list of information related with TS activity. Also a text message that indicates what type of TS session the application execution request is being initiated from: [Console] if the app runs on the server console session, [Remote Session] if the app runs on a remote session or [Non Terminal Services] if the TS is not installed.

So first of all we have to decide if the application runs on a workstation or on a server. To do it, the easiest way is to use:

  • OSVERSIONINFOEX structure that contains operating system version information,
  • VER_SET_CONDITION macro with VER_PRODUCT_TYPE attribute type and
  • VerifyVersionInfo( . ) function to compare.

Here we have to use the Platform SDK because the wProductType member is defined only in OSVERSIONINFOEX structure from WinNT.h PSDK and is not in OSVERSIONINFOEX structure from WINBASE.h VC98. wProductType indicates if the system is running Windows NT 4.0 Workstation, Windows 2000 Professional, Windows XP Home Edition, or Windows XP Professional (VER_NT_WORKSTATION) or the system is a server (VER_NT_SERVER). VER_SET_CONDITION(.) is declared only in Winnt.h PSDK and VerifyVersionInfo(.) is declared only in Winbase.h PSDK.

////////////
BOOL CWTSMonitorApp::IsOSVersionTypeNT_WORKSTATION()
{
    DWORDLONG       dwlConditionMask = 0;
    OSVERSIONINFOEX osVersionInfo;

    ZeroMemory( &osVersionInfo, sizeof(OSVERSIONINFOEX) );

    osVersionInfo.dwOSVersionInfoSize = sizeof( OSVERSIONINFOEX );
    osVersionInfo.wProductType        = VER_NT_WORKSTATION;

    VER_SET_CONDITION( dwlConditionMask, VER_PRODUCT_TYPE, VER_EQUAL );

    return VerifyVersionInfo( &osVersionInfo, 
            VER_PRODUCT_TYPE, dwlConditionMask );
}
//////

Second we have to see if the Terminal Services is enabled. To do it I use the same VerifyVersionInfo(.) with OSVERSIONINFOEX structure and the VER_SET_CONDITION(.) macro:

///////
BOOL CWTSMonitorApp::IsTerminalServicesEnabled()
{
    DWORDLONG       dwlConditionMask = 0;
    OSVERSIONINFOEX osVersionInfo;

    ZeroMemory(&osVersionInfo, sizeof(OSVERSIONINFOEX));

    osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
    osVersionInfo.wSuiteMask  = VER_SUITE_TERMINAL;

    VER_SET_CONDITION( dwlConditionMask, VER_SUITENAME, VER_AND );

    return VerifyVersionInfo( &osVersionInfo, 
             VER_SUITENAME, dwlConditionMask );
}
////////////

We can decide now. I created a "complicated" function GetWTSEnvironment() to demonstrate:

///////
void CWTSMonitorApp::GetWTSEnvironment()
{
    if(_bIsTSEnabled )
    {
        if( GetSystemMetrics( SM_REMOTESESSION ) )
        {
            _iTSEnvironment = 2; // remote
        }
        else
        {
            _iTSEnvironment = 1; // console
        }
    }
    else
    {
        _iTSEnvironment     = 0; // non WTS
    }
}
/////////////

Based on these information we decide how to create the mutex. If our application runs on a server with TS enabled, the mutex name has to contain "Session" word like this:

/////
g_hAppRunningMutex = ::CreateMutex( NULL, FALSE, "Session\\WTSMonitor" );
/////

otherwise the mutex name has to be simple like:

//////
g_hAppRunningMutex = ::CreateMutex( NULL, FALSE, "WTSMonitor" );
//////

Now if from another session we execute the same application, the already created "Session" mutex is found. The decision is based on ERROR_ALREADY_EXISTS error that appears when the app tries to create the mutex. In this case we decide (_bReadOnly = TRUE;) to show another dialog (CWTSMonitorReadOnlyDlg) with "read-only" capabilities.

This "read-only" dialog (and also the "main" dialog - CWTSMonitorDlg) has a minimize button which minimizes the app on the screen (or into the system tray for the "main" dialog). If we have the app minimized and we try to run again the app in the same session, the only thing that we have to do is to "restore" the app on the screen and nothing else. So find the window and restore it:

//////
if( ::GetLastError() == ERROR_ALREADY_EXISTS )
{
    CWnd *pWndNAG = CWnd::FindWindow( "#32770", 
                     "Terminal Services Monitor" );

    if( pWndNAG == NULL )
    {
        _bReadOnly = TRUE;
    }
    else
    {
        pWndNAG->ShowWindow( SW_RESTORE );
        pWndNAG->SetForegroundWindow();

        return FALSE;
    }
}
////

When we create the "read-only" dialog we send a notification (SendNotification(.)) to the other sessions where the same app is running, saying that "A WTS Monitor Read-Only has been instantiated".

When we close the "main" dialog we send a notification saying: "The WTS Monitor has been terminated" and in addition to this we "terminate" the application (FinalTerminateProcess(.)) in all sessions.

SendNotification( BOOL termination ) is going through all active sessions and wherever finds an active WTSMonitor process having termination FALSE just sends a message or having termination TRUE sends a message and ends the process. To send messages over TS we use WTSSendMessage(.) and to end the process over TS we use WTSTerminateProcess(.)

Therefore we did a Terminal Services aware application which concludes the first part of the paper.

Next I want to discuss a bit about what we see in the main dialog list.

The list displays some information regarding the TS sessions. There can be many more but I just want to show how we can use some of the WTS API functions, because in MSDN there is no sample code anywhere.

Basically by using WTSEnumerateSessions(.) we find all the WTS_SESSION_INFO structures, each one containing information about a session, and going through each session by using WTSQuerySessionInformation(.) we find all the information that we want:

  1. WTSQuerySessionInformation(.) with WTSClientDirectory parameter gives a pointer to a null-terminated string indicating the directory in which the client is installed. If the function is called from the TS console, ppBuffer returns a NULL pointer.
    ////////
    CString CWTSMonitorDlg::GetTSClientDir( DWORD sessionID )
    {
        LPTSTR  ppBuffer        = NULL;
        DWORD   pBytesReturned  = 0;
        CString clientDir; clientDir.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                          sessionID,
                                          WTSClientDirectory,
                                          &ppBuffer,
                                          &pBytesReturned) )
        {
            clientDir = CString( ppBuffer );
        }
    
        WTSFreeMemory( ppBuffer );
    
        return clientDir;
    }
    /////////
  2. WTSQuerySessionInformation(.) with WTSUserName parameter gives a pointer to a null-terminated string containing the name of the user associated with the session.
    //////////
    CString CWTSMonitorDlg::GetTSUserName( DWORD sessionID )
    {
        LPTSTR  ppBuffer        = NULL;
        DWORD   pBytesReturned  = 0;
        CString currentUserName; currentUserName.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                sessionID,
                                                WTSUserName,
                                                &ppBuffer,
                                                &pBytesReturned) )
        {
            currentUserName = CString( ppBuffer );
        }
    
        WTSFreeMemory( ppBuffer );
    
        return currentUserName;
    }
    ////////
  3. WTSQuerySessionInformation(.) with WTSDomainName parameter gives a pointer to a null-terminated string that names the domain of the logged-on user.
    /////////////////
    CString CWTSMonitorDlg::GetTSDomainName(DWORD sessionID)
    {
        LPTSTR  ppBuffer  = NULL;
        DWORD   pBytesReturned = 0;
        CString currentDomainName; currentDomainName.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                sessionID,
                                                WTSDomainName,
                                                &ppBuffer,
                                                &pBytesReturned) )
        {
            currentDomainName = CString( ppBuffer );
        }
    
        WTSFreeMemory( ppBuffer );
    
        return currentDomainName;
    }
    /////////
  4. WTSQuerySessionInformation(.) with WTSClientProtocolType parameter gives a pointer to a USHORT variable containing the protocol type.
    //////////
    CString CWTSMonitorDlg::GetTSClientProtocolType(DWORD sessionID)
    {
        LPTSTR  ppBuffer           = NULL;
        DWORD   pBytesReturned     = 0;
        CString clientProtocolTypeStr; clientProtocolTypeStr.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                sessionID,
                                                WTSClientProtocolType,
                                                &ppBuffer,
                                                &pBytesReturned) )
        {
            switch( *ppBuffer )
            {
                case WTS_PROTOCOL_TYPE_CONSOLE:
                    clientProtocolTypeStr = "Console";
                    break;
                case WTS_PROTOCOL_TYPE_ICA:
                    clientProtocolTypeStr = "ICA";
                    break;
                case WTS_PROTOCOL_TYPE_RDP:
                    clientProtocolTypeStr = "RDP";
                    break;
                default:
                    break;
            }
        }
    
        WTSFreeMemory( ppBuffer );
    
        return clientProtocolTypeStr;
    }
    //////
  5. WTSQuerySessionInformation(.) with WTSClientProductId parameter gives a pointer to a USHORT variable containing a client-specific product identifier. If the function is called from the Terminal Services console, ppBuffer returns a NULL pointer.
    /////////
    CString CWTSMonitorDlg::GetTSClientProductId(DWORD sessionID)
    {
        LPTSTR  ppBuffer  = NULL;
        DWORD   pBytesReturned = 0;
        CString clientProductIdStr; clientProductIdStr.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                sessionID,
                                                WTSClientProductId,
                                                &ppBuffer,
                                                &pBytesReturned) )
        {
            clientProductIdStr.Format( "%u", *ppBuffer );
        }
    
        WTSFreeMemory( ppBuffer );
    
        return clientProductIdStr;
    }
    /////////
  6. WTSQuerySessionInformation(.) with WTSClientName parameter gives a pointer to a null-terminated string containing the name of the client. If the function is called from the Terminal Services console, ppBuffer returns a NULL pointer.
    /////////////
    CString CWTSMonitorDlg::GetTSClientName(DWORD sessionID)
    {
        LPTSTR  ppBuffer  = NULL;
        DWORD   pBytesReturned = 0;
        CString currentWinStationName; currentWinStationName.Empty();
    
        if( WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                sessionID,
                                                WTSClientName,
                                                &ppBuffer,
                                                &pBytesReturned) )
        {
            currentWinStationName = CString( ppBuffer );
        }
    
        WTSFreeMemory( ppBuffer );
    
        return currentWinStationName;
    }
    /////////
  7. WTSQuerySessionInformation(.) with WTSClientDisplay parameter gives a pointer to a WTS_CLIENT_DISPLAY structure containing information about the client's display. If the function is called from the Terminal Services console, ppBuffer returns a NULL pointer.
    ////////
    CString CWTSMonitorDlg::GetTSClientDisplay( DWORD sessionID )
    {
        LPTSTR ppBuffer            = NULL;
        DWORD  pBytesReturned      = 0;
        PWTS_CLIENT_DISPLAY pWTSCD = NULL;
    
        CString clientDisplay; clientDisplay.Empty();
    
        BOOL b = WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                     sessionID,
                                                     WTSClientDisplay,
                                                     &ppBuffer,
                                                     &pBytesReturned);
    
        pWTSCD = (PWTS_CLIENT_DISPLAY)ppBuffer;
    
        // we can read also direct from the buffer:
        // ppBuffer[5] and ppBuffer[4] gives the maximum of colors
        // ppBuffer[3] and ppBuffer[2] gives
        /       vertical resolution in pixels
        // ppBuffer[1] and ppBuffer[0] 
        //      gives horizontal resolution in pixels
    
        CString nrColorsStr; nrColorsStr.Empty();
    
        switch( pWTSCD->ColorDepth )
        {
            case 1: // The display uses 4 bits per pixel
                    // for a maximum of 16 colors.
                    nrColorsStr = "16";
                    break;
            case 2: // The display uses 8 bits per pixel
                    // for a maximum of 256 colors.
                    nrColorsStr = "256";
                    break;
            case 4: // The display uses 16 bits per pixel
                    // for a maximum of 2^16 colors.
                    nrColorsStr = "65536";
                    break;
            case 8: // The display uses 3-byte RGB
                    // values for a maximum of 2^24 colors.
                    nrColorsStr = "16777216";
                    break;
        }
    
        clientDisplay.Format( "%u x %u - %s colors",
                                      pWTSCD->HorizontalResolution,
                                      pWTSCD->VerticalResolution,
                                      nrColorsStr );
    
        WTSFreeMemory( ppBuffer );
    
        return clientDisplay;
    }
    ///////
  8. WTSQuerySessionInformation(.) with WTSClientAddress parameter gives a pointer to a WTS_CLIENT_ADDRESS structure containing the network type and network address of the client. If the function is called from the Terminal Services console, ppBuffer returns a NULL pointer. Note that the first byte of the IP address returned in the ppBuffer parameter will be located at an offset of two bytes from the first location of the buffer.
    ///////
    CString CWTSMonitorDlg::GetTSClientAddress( DWORD sessionID )
    {
        LPTSTR ppBuffer            = NULL;
        DWORD  pBytesReturned      = 0;
        PWTS_CLIENT_ADDRESS pWTSCA = NULL;
    
        CString clientFamilyAndAddress; clientFamilyAndAddress.Empty();
    
        BOOL b = WTSQuerySessionInformation( WTS_CURRENT_SERVER_HANDLE,
                                                     sessionID,
                                                     WTSClientAddress,
                                                     &ppBuffer,
                                                     &pBytesReturned);
    
        pWTSCA = (PWTS_CLIENT_ADDRESS)ppBuffer;
    
        // Address family can be only:
        // AF_UNSPEC  = 0 (unspecified)
        // AF_INET    = 2 (internetwork: UDP, TCP, etc.)
        // AF_IPX     = AF_NS = 6 (IPX protocols: IPX, SPX, etc.)
        // AF_NETBIOS = 17 (NetBios-style addresses)
    
        CString familyStr; familyStr.Empty();
    
        switch( pWTSCA->AddressFamily )
        {
            case 0:
                   familyStr = "AF_UNSPEC";
                   break;
            case 2:
                   familyStr = "AF_INET";
                   break;
            case 6:
                   familyStr = "AF_IPX";
                   break;
            case 17:
                   familyStr = "AF_NETBIOS";
                   break;
        }
    
        // 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.
    
        CString addStr; addStr.Empty();
        addStr.Format( "%u.%u.%u.%u",
                               pWTSCA->Address[2],
                               pWTSCA->Address[3],
                               pWTSCA->Address[4],
                               pWTSCA->Address[5] );
    
        clientFamilyAndAddress.Format( "%s - %s",
                                                familyStr,
                                                addStr );
    
        WTSFreeMemory( ppBuffer );
    
        return clientFamilyAndAddress;
    }
    /////////

On the end...

Please remember, if you want to test this application you have to use a server Windows operating system with Terminal Services installed. Also you have to have Platform SDK installed in order to be able to compile the source. If you are testing from a workstation, you still can run, but you cannot see any info regarding TS.

Good luck!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Liviu Birjega
Software Developer (Senior) IBI Group
Canada Canada
No Biography provided

Comments and Discussions

 
QuestionHow to logoff the specified user PinmemberDeeJayX27-Jun-12 14:30 
Questionlogoff Pinmemberfikusys30-Dec-11 1:01 
QuestionWTSSendMessage PinmemberMember 404464129-Jul-09 7:49 
Questionabout MAC PinmemberMember 376180117-Oct-08 14:51 
Generaldetermining unique instances Pinmembergabegabe13-Dec-07 0:29 
GeneralGreat job!! Pinmemberpointer12-Dec-07 14:43 
GeneralSetting a WTSClientName at Login PinmemberGary Parkinson12-Jul-07 1:17 
Generalwtsapi32.dll Pinmemberdi3220-Feb-07 23:54 
AnswerRe: wtsapi32.dll PinmemberDeeJayX27-Jun-12 14:38 
QuestionTerminal Services Application Control PinmemberGPeele31-Oct-06 10:11 
QuestionReporting current username and session ID Pinmembersupport@gchc.on.ca22-Aug-06 10:56 
QuestionHow to Identify WTS Client PinmemberChandruIT21-Jun-06 21:29 
GeneralWTSWaitSystemEvent - Terminal Services Pinmemberjlegault30-Apr-06 7:24 
GeneralLink between the WTS session and the host PinmemberAlexEvans11-Apr-06 21:57 
GeneralSend a WAV file over TS PinmemberY G15-Nov-05 12:27 
QuestionHow to create Terminal Services session on server? Pinmemberjacobkurien18-Aug-05 14:00 
GeneralGetting info of an App on Server PinsussAnonymous21-Mar-05 19:34 
GeneralRe: Getting info of an App on Server PinmemberPablo Aliskevicius23-Apr-06 4:39 
GeneralClient username PinmemberRob Cooper21-Jan-05 0:44 
Generalusing Terminal Services API with ActiveDirectory PinmemberNirmalya Sirkar3-Nov-04 3:50 
QuestionHow to get Notification from Clients PinsussSunki25-Aug-04 20:04 
AnswerRe: How to get Notification from Clients PinmemberLiviu Birjega26-Aug-04 5:21 
GeneralExcellent work Pinmemberarchatech22-Aug-04 21:14 
GeneralTerminal Services Programming Pinmember4apai5-Aug-04 6:15 
Generalc# version of the c++ code PinmemberCoolCoyotes27-Jul-04 8:26 
GeneralRe: c# version of the c++ code Pinmemberfhunth13-Aug-04 6:23 
GeneralRe: c# version of the c++ code Pinmemberrasatavohary24-Jun-05 1:45 
Generalconfiguring terminal services PinsussdekelA9-Jun-04 22:51 
Generalconfiguring terminal services PinsussAnonymous9-Jun-04 22:48 
GeneralThe Microsoft Docs Are Lacking PinmemberBlake Miller15-Apr-04 10:04 
GeneralRe: Console Application PinmemberBlake Miller15-Apr-04 10:03 
GeneralAbout RDP PinsussChinalygyang200216-Nov-03 21:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141220.1 | Last Updated 7 Oct 2003
Article Copyright 2003 by Liviu Birjega
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid