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;
}
else
{
_iTSEnvironment = 1;
}
}
else
{
_iTSEnvironment = 0;
}
}
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:
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
/ vertical resolution in pixels
CString nrColorsStr; nrColorsStr.Empty();
switch( pWTSCD->ColorDepth )
{
case 1:
nrColorsStr = "16";
break;
case 2:
nrColorsStr = "256";
break;
case 4:
nrColorsStr = "65536";
break;
case 8:
nrColorsStr = "16777216";
break;
}
clientDisplay.Format( "%u x %u - %s colors",
pWTSCD->HorizontalResolution,
pWTSCD->VerticalResolution,
nrColorsStr );
WTSFreeMemory( ppBuffer );
return clientDisplay;
}
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;
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;
}
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!