I’m luckily have opportunity to works with Windows Terminal Services (WTS). The application that my team developed is running under Citrix machine, and there is a issue related with session information of each client ~ brief introduction.
Lets look more details about Windows Terminal Services (WTS).
Windows Terminal Services is one of Remote Access Technologies that allow IT administrators to perform administrative tasks; and IT users run programs on a remote machine as if they were working locally. This component provides the ability to host multiple, simultaneous client sessions on Windows Server machine. Through terminal emulation, its allows the same set of applications to run on diverse types of desktop hardware.
For organizations that looks flexibility to deploy applications and control desktop management costs, this architecture offers an important enhancement to the traditional two or three-tier client-server architecture based on servers and full-scale personal machine.
Picture: Windows Terminal Services Architecture
Windows Terminal Services used the Remote Desktop Protocol (RDP) to interact with user. This protocol have following features and capabilities:
- Encryption – RDP uses RSA security’s RC4 cipher ~ a stream cipher designed to efficiently encrypt small amounts of data. RC4 is designed for secure communications over networks.
- Bandwidth Reduction – RDP supports various mechanisms to reduce the amount of data transmitted over a network connection. Mechanisms include data compression, persistent caching of bitmaps, and caching of glyphs and fragments in RAM
- Roaming Disconnect – A user can manually disconnect from a remote desktop session without logging off. The user is automatically reconnected to their disconnected session when he or she logs back onto the system, either from the same device or a different device. When a user's session is unexpectedly terminated by a network or client failure, the user is disconnected but not logged off.
- Clipboard Mapping – Users can delete, copy, and paste text and graphics between applications running on the local computer and those running in a remote desktop session, and between sessions.
- Print Redirection – Applications running within a remote desktop session can print to a printer attached to the client device.
- Virtual Channels – By using RDP virtual channel architecture, existing applications can be augmented and new applications can be developed to add features that require communications between the client device and an application running in a remote desktop session.
- Remote Control – Computer support staff can view and control a remote desktop session. Sharing input and display graphics between two remote desktop sessions gives a support person the ability to diagnose and resolve problems remotely.
- Network Load Balancing.
- Smart Card Authentication through Remote Desktop Services.
Picture: Terminal Server Interactions with RDP
The user/ client initiates a connection to the Terminal Server through TCP port 3389. The Terminal Server RDP listener thread detects the session request and creates a new RDP stack instance to handle the new session request. The listener thread hands over the incoming session to the new RDP stack instance and continues listening on the TCP port for further connection attempts. Each RDP stack is created as the client sessions are connected to handle negotiation of session configuration details.
So, how I used it to help me to solve my team issue. There is one of Windows API called Remote Desktop Services API to implement the additional functionality, and dynamically link to the WtsApi32 library. There are several functions that I used to help me solve my team issue.
- WTSEnumerateSessions – Retrieves a list of sessions on a specified Remote Desktop (RD) Session Host server.
- WTSFreeMemory – Frees memory allocated by a Remote Desktop Services function.
- WTSQuerySessionInformation – Retrieves session information for the specified session on the specified Remote Desktop (RD) Session Host server. It can be used to query session information on local and remote RD Session Host servers.
- WTSOpenServer – Opens a handle to the specified Remote Desktop Session Host server.
- WTSCloseServer – Closes an open handle to a Remote Desktop Session Host (RD Session Host) server.
And there are another functions from “Kernel32” library, that I used. There are:
- GetCurrentProcessId – Retrieve the current of process Id.
- WTSGetActiveConsoleSessionId – Retrieves the Remote Desktop Services session that is currently attached to the physical console. The physical console is the monitor, keyboard, and mouse. Note that it is not necessary that Remote Desktop Services be running for this function to succeed.
- ProcessIdToSessionId – Retrieves the Remote Desktop Services session associated with a specified process.
1: Private Declare Function ProcessIdToSessionId Lib "Kernel32.dll" Alias "ProcessIdToSessionId" (ByVal processId As Int32, ByRef sessionId As Int32) As Boolean
2:
3: Private Declare Function WTSQuerySessionInformation Lib "WtsApi32.dll" Alias "WTSQuerySessionInformationW" (ByVal hServer As IntPtr, ByVal SessionId As Int32, ByVal WTSInfoClass As Int32, <MarshalAs(UnmanagedType.LPWStr)> ByRef ppBuffer As String, ByRef pCount As Int32) As Boolean
4:
5: Private Declare Function WTSQuerySessionInformation2 Lib "WtsApi32.dll" Alias "WTSQuerySessionInformationW" (ByVal hServer As IntPtr, ByVal SessionId As Int32, ByVal WTSInfoClass As Int32, ByRef ppBuffer As IntPtr, ByRef pCount As Int32) As Boolean
6:
7: Private Declare Function WTSEnumerateSessions Lib "WtsApi32.dll" Alias "WTSEnumerateSessions" (ByVal hServer As IntPtr, <MarshalAs(UnmanagedType.U4)> ByVal Reserved As Int32, <MarshalAs(UnmanagedType.U4)> ByVal Version As Int32, ByRef pSessionInfo As IntPtr, <MarshalAs(UnmanagedType.U4)> ByVal pCount As Int32) As Int32
8:
9: Private Declare Function WTSOpenServer Lib "WtsApi32.dll" Alias "WTSOpenServer" (ByVal pServerName As String) As IntPtr
10:
11: Private Declare Function WTSGetActiveConsoleSessionId Lib "Kernel32.dll" Alias "WTSGetActiveConsoleSessionId" () As Int32
12:
13: Private Declare Sub WTSCloseServer Lib "WtsApi32.dll" Alias "WTSCloseServer" (ByVal hServer As IntPtr)
14:
15: Private Declare Sub WTSFreeMemory Lib "WtsApi32.dll" Alias "WTSFreeMemory" (ByVal pMemory As IntPtr)
16:
17: Private Declare Function GetCurrentProcessId Lib "Kernel32.dll" Alias "GetCurrentProcessId" () As Int32
And I have enumeration of WTS information, called WTSInfoClass.
1: Private Enum WTSInfoClass As Integer
2: WTSInitialProgram
3: WTSApplicationName
4: WTSWorkingDirectory
5: WTSOEMId
6: WTSSessionId
7: WTSUserName
8: WTSWinStationName
9: WTSDomainName
10: WTSConnectState
11: WTSClientBuildNumber
12: WTSClientName
13: WTSClientDirectory
14: WTSClientProductId
15: WTSClientHardwareId
16: WTSClientAddress
17: WTSClientDisplay
18: WTSClientProtocolTyep
19: WTSIdleTime
20: WTSLogonTime
21: WTSIncomingBytes
22: WTSOutgoingBytes
23: WTSIncomingFrames
24: WTSOutgoingFrames
25: End Enum
And below is the main method to load session information of specified server name.
1: Public Function LoadSessionInfo(ByVal serverName As String) As SessionInfo
2: Dim ptrOpenedServer As IntPtr
3: Dim dictSessionIds As New Dictionary(Of String, Integer)
4:
5: Try
6: ptrOpenedServer = WTSOpenServer(serverName)
7: If ptrOpenedServer = vbNull Then
8: Throw New Exception("Terminal Services not running on: " & serverName)
9: End If
10:
11: Dim retVal, count As Int32
12: Dim ptrSessionInfo As IntPtr = IntPtr.Zero
13: count = 0
14: Try
15: retVal = WTSEnumerateSessions(ptrOpenedServer, 0, 1, ptrSessionInfo, count)
16: If retVal <> 0 Then
17: Dim sessionInfos() As WTS_SESSION_INFO = New WTS_SESSION_INFO(count) {}
18: Dim ptrSession As IntPtr
19: For i As Integer = 0 To count - 1
20: ptrSession = ptrSessionInfo.ToInt32() + (i * Marshal.SizeOf(sessionInfos(i)))
21: sessionInfos(i) = CType(Marshal.PtrToStructure(ptrSession, GetType(WTS_SESSION_INFO)), WTS_SESSION_INFO)
22: Next
23:
24: WTSFreeMemory(ptrSessionInfo)
25:
26: Dim winStationName As String
27: Dim tmpArr(sessionInfos.GetUpperBound(0)) As STR_SESSION_INFO
28: For i = 0 To tmpArr.GetUpperBound(0)
29: tmpArr(i).SessionID = sessionInfos(i).SessionId
30:
31: winStationName = sessionInfos(i).pWinStationName
32: tmpArr(i).StationName = winStationName
33:
34: tmpArr(i).ConnectionState = sessionInfos(i).State.ToString()
35:
36: If Not String.IsNullOrEmpty(winStationName) Then
37: If Not dictSessionIds.ContainsKey(winStationName) Then
38: dictSessionIds.Add(winStationName, sessionInfos(i).SessionId)
39: End If
40: End If
41: Next
42:
43: ReDim sessionInfos(-1)
44: Else
45: Throw New Exception("No data returned")
46: End If
47: Catch exInner As Exception
48: Throw New Exception(exInner.Message & Environment.NewLine & Marshal.GetLastWin32Error)
49: End Try
50: Catch ex As Exception
51: Try
52: WTSCloseServer(ptrOpenedServer)
53: WTSFreeMemory(ptrOpenedServer)
54: Catch exWtsCloseServer As Exception
55: End Try
56:
57: Throw ex
58: End Try
59:
60: Dim result As New SessionInfo
61:
62: 'Get ProcessId of TS Session that executed this TS Session
63: Dim activeProcess As Int32 = GetCurrentProcessId()
64: Dim activeSession As Int32 = 0
65: If Not ProcessIdToSessionId(activeProcess, activeSession) Then
66: 'Nothing
67: End If
68:
69: Dim strBuffer As String
70: Dim ptrBuffer As IntPtr
71: Dim returnedLength As Int32
72:
73: 'Member in this list will be ignore during generic query session iteration.
74: Dim ignoreQuerySession As New List(Of WTSInfoClass)
75:
76: 'Member in this list will use function WTSQuerySessionInformation2
77: ' to get their information, instead WTSQuerySessionInformation.
78: ' By default, use WTSQuerySessionInformation is not contains in list.
79: Dim querySession2 As New List(Of WTSInfoClass)
80: querySession2.Add(WTSInfoClass.WTSClientAddress)
81:
82: 'Get Windows Station Name, as a pre-requirement for some info,
83: ' likes: SessionId, ClientName, ClientAddress.
84: If WTSQuerySessionInformation(ptrOpenedServer, activeSession, WTSInfoClass.WTSWinStationName, strBuffer, returnedLength) Then
85: result.WinStationName = strBuffer
86: ignoreQuerySession.Add(WTSInfoClass.WTSWinStationName)
87:
88: 'Skip ClientAddress and ClientName if this is a console session
89: If String.Equals(result.WinStationName, "Console") Then
90: ignoreQuerySession.Add(WTSInfoClass.WTSClientAddress)
91: ignoreQuerySession.Add(WTSInfoClass.WTSClientName)
92: End If
93: End If
94:
95: 'Use stationName to get the correct sessionId from dictSessionId.
96: If dictSessionIds.ContainsKey(result.WinStationName) Then
97: Try
98: result.SessionId = Integer.Parse(dictSessionIds(result.WinStationName))
99: ignoreQuerySession.Add(WTSInfoClass.WTSSessionId)
100: Catch ex As Exception
101: End Try
102: End If
103:
104: Dim wtsInfoClassValues As Array = [Enum].GetValues(GetType(WTSInfoClass))
105: Dim wtsInfoClassItem As WTSInfoClass
106: Dim index As Integer = -1
107: Dim querySessionResult As Boolean
108: While index < wtsInfoClassValues.Length - 1
109: index += 1
110: wtsInfoClassItem = wtsInfoClassValues.GetValue(index)
111:
112: If ignoreQuerySession.Contains(wtsInfoClassItem) Then Continue While
113:
114: If querySession2.Contains(wtsInfoClassItem) Then
115: querySessionResult = WTSQuerySessionInformation2(ptrOpenedServer, activeSession, wtsInfoClassItem, ptrBuffer, returnedLength)
116: Else
117: querySessionResult = WTSQuerySessionInformation(ptrOpenedServer, activeSession, wtsInfoClassItem, strBuffer, returnedLength)
118: End If
119:
120: If querySessionResult Then
121: Select Case wtsInfoClassItem
122:
123: 'TODO: Assign to proper place
124: Case WTSInfoClass.WTSClientAddress
125: Dim obj As New WTS_CLIENT_ADDRESS()
126: obj = CType(Marshal.PtrToStructure(ptrBuffer, obj.GetType()), WTS_CLIENT_ADDRESS)
127: result.Address(2) = obj.Address(2)
128: result.Address(3) = obj.Address(3)
129: result.Address(4) = obj.Address(4)
130: result.Address(5) = obj.Address(5)
131:
132: End Select
133: End If
134:
135: If ptrBuffer <> IntPtr.Zero Then
136: WTSFreeMemory(ptrBuffer)
137: ptrBuffer = IntPtr.Zero
138: End If
139: strBuffer = ""
140: End While
141:
142: WTSCloseServer(ptrOpenedServer)
143:
144: Return result
145: End Function
To supported above codes, I used 3 (three) structures: WTS_SESSION_INFO, STR_SESSION_INFO, and WTS_CLIENT_ADDRESS.
1: <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
2: Private Structure WTS_SESSION_INFO
3: Dim SessionId As Int32 'DWORD integer
4: Dim pWinStationName As String ' Integer LPTSTR - Pointer to a null-terminated string containing the name of the WinStation of this session
5: Dim State As WTSConnectStateClass
6: End Structure
7:
8: Private Structure STR_SESSION_INFO
9: Dim SessionId As Integer
10: Dim StationName As String
11: Dim ConnectionState As String
12: End Structure
13:
14: <StructLayout(LayoutKind.Sequential)> _
15: Private Structure WTS_CLIENT_ADDRESS
16: Public AddressFamily As Integer
17: <MarshalAs(UnmanagedType.ByValArray, SizeConst:=20)> _
18: Public Address As Byte()
19: End Structure
20:
21: Public Enum WTSConnectStateClass As Integer
22: WTSActive
23: WTSConnected
24: WTSConnectQuery
25: WTSShadow
26: WTSDisconnected
27: WTSIdle
28: WTSListen
29: WTSReset
30: WTSDown
31: WTSInit
32: End Enum
The return value of function LoadSessionInfo is SessionInfo.
SessionInfo is the plain/ model class that hold the WTS Information (I considered enumeration WTSInfoClass for those properties).
On line-123 “TODO: Assign to proper place”, this is the place to assign WTS information to our model class ~ SessionInfo. I put sample codes that assign the information of client address (IP).
I also have logic to check either application is running on remote terminal or local terminal ~ called IsRunningLocally.
1: Public Function IsRunningLocally() As Boolean
2: Dim ptrBuffer As IntPtr = IntPtr.Zero
3: Dim sessionId, currentSessionId, returnedLength As Integer
4:
5: Try
6: WTSQuerySessionInformation2(IntPtr.Zero, -1, WTSInfoClass.WTSSessionId, ptrBuffer, returnedLength)
7: sessionId = Marshal.ReadInt32(ptrBuffer, returnedLength)
8: Catch ex As Exception
9: sessionId = -1
10: Finally
11: WTSFreeMemory(ptrBuffer)
12: ptrBuffer = IntPtr.Zero
13: End Try
14:
15: Try
16: currentSessionId = WTSGetActiveConsoleSessionId()
17: Catch ex As Exception
18: currentSessionId = -1
19: End Try
20:
21: If currentSessionId <> sessionId Then Return False
22:
23: Return True
24: End Function
References
- Remote Desktop Protocol - http://msdn.microsoft.com/en-us/library/aa383015(v=vs.85).aspx
- Remote Desktop Services - http://en.wikipedia.org/wiki/Remote_Desktop_Services
- Remote Desktop Services API Functions - http://msdn.microsoft.com/en-us/library/aa383464(v=VS.85).aspx