Windows XP 有一个新特性叫做“快速用户转换——Fast User
Switching”,这个特性允许多个用户同时在一台机器上登陆。当一个用户登陆后,另一个用户启动的进程仍然能够运行。这个神奇的特性所倚仗的是
WTS APIs。如果你想了解更多有关 WTS 的内容,可以参考 MSJ Oct99 的一篇文章:“
Windows NT和 Windows 2000 终端服务APIs介绍”,作者是 Frank Kim。
Windows XP为每一个登陆用户创建一个WTS会话(Session)。每个运行进程总是与这样一个Session关联。Windows
XP的任务管理器允许你列出进程清单,不论是针对所有会话的还是仅仅针对自己的会话,任务管理器对话框的进程标签中有一个"显示所有用户的进程"复选框可
以对此进行选择。(如图所示):
任务管理其的进程列表
如果你想了解某个进程隶属的 session ID,可以调用 kernel32.dll 输出的一个 API 函数
ProcessIdToSessionId。给定一个进程的ID,他返回相应的 session ID。有趣的是这个 API 函数不是由
wtsapi32.dll 输出的,而是出自于 kernel32.dll,前者是所有 Windows 终端服务 APIs
的输出动态库。实际上,即使 Windows 终端服务没有运行起来,Windows 2000 和 Windows XP 都将 session
ID 存储在 PEB 中。
注意 Windows NT 既不在 PEB 中存储 session ID,也不从
kernel32.dll 中输出 ProcessIdToSessionId 函数。当你调用 ProcessIdToSessionId,而
WTS 又没有运行,这时其返回值总是0。
除了允许你列出打开的会话之外,WTS 还有一个 API 用于枚举运行的进程,其实现方式与
PSAPI 和 TOOLHELP32 的实现方式是不同的。我写了一个类 CWTSWrapper 来打包 WTS
中与进程和会话有关的函数,以便避免与 wtsapi32.dll 进行静态链接。这个类的实现细节请参考下载的源代码,见 common 目录的
wrappers.cpp 文件。用 CWTSWrapper 很容易构造象 ProcessXP 这样的控制台应用程序。下面是ProcessXP
程序的输出,它列出了与登陆用户对应的打开的会话以及会话项下的运行进程。ProcessXP 程序的输出如下:
3 open sessions
ID State Window Station
0 (WTSActive) Console [Administrator]
1 (WTSDisconnected) [standard]
2 (WTSDisconnected) [Player]
30 running processes
0 0 ?
0 4 System \\NT AUTHORITY\SYSTEM
0 388 smss.exe \\NT AUTHORITY\SYSTEM
0 600 csrss.exe \\NT AUTHORITY\SYSTEM
0 632 winlogon.exe \\NT AUTHORITY\SYSTEM
0 676 services.exe \\NT AUTHORITY\SYSTEM
0 688 lsass.exe \\NT AUTHORITY\SYSTEM
0 856 svchost.exe \\NT AUTHORITY\SYSTEM
0 968 svchost.exe \\NT AUTHORITY\SYSTEM
0 1160 svchost.exe \\NT AUTHORITY\NETWORK SERVICE
0 1192 svchost.exe \\NT AUTHORITY\LOCAL SERVICE
0 1252 spoolsv.exe \\NT AUTHORITY\SYSTEM
0 1888 explorer.exe \\MACHINE\Administrator
0 2004 msmsgs.exe \\MACHINE\Administrator
0 104 svchost.exe \\NT AUTHORITY\SYSTEM
1 1496 csrss.exe \\NT AUTHORITY\SYSTEM
1 1172 winlogon.exe \\NT AUTHORITY\SYSTEM
1 1640 explorer.exe \\MACHINE\standard
1 1900 ctfmon.exe \\MACHINE\standard
1 352 notepad.exe \\MACHINE\standard
1 1896 freecell.exe \\MACHINE\standard
2 416 csrss.exe \\NT AUTHORITY\SYSTEM
2 268 winlogon.exe \\NT AUTHORITY\SYSTEM
2 1784 explorer.exe \\MACHINE\Player
0 1820 msiexec.exe \\NT AUTHORITY\SYSTEM
2 1544 ctfmon.exe \\MACHINE\Player
2 1632 msmsgs.exe \\MACHINE\Player
2 1268 wordpad.exe \\MACHINE\Player
0 1696 wuauclt.exe \\MACHINE\Administrator
0 1996 ProcessXP.exe \\MACHINE\Administrator
从上面的输出可以看出,名为 MACHINE 的机器上打开的会话有三各。第一个会话的 ID 为0,状态为活动(WTSActive
因为它就是运行中的 ProcessXP 所在的会话),产生这个会话的登陆用户为
Administrator。第二个会话的ID是1,处于断开状态(WTSDisconnected),产生这个会话的用户为标准用户,此用户启动了
Notepad 和 Freecell 程序,用户Player打开了会话2,并运行WordPad,但目前状态是断开的。
ProcessXP 的源代码包含在本文可下载的压缩包中。WTS
有一个与注册表类似特性,那就是允许你获取另外一台机器的信息。这就是为什么WTS枚举 APIs
函数的第一个参数都是一个服务器句柄。WTS_CURRENT_SERVER_HANDLE
用于当前的机器。第二个参数是保留参数,值应该为0。第三个参数希望的版本,其值应该是1。最后两个参数用于存放返回的信息。一个用于存放会话数或进程
数。另一个是结构数组的指针,结构可以是描述会话信息的结构,也可以是描述进程信息的结构。就看你是使用哪个枚举API,是枚举会话还是枚举进程。因为数
组的存储空间是由 WTS 分配的,你必须要记住用 WTSFreeMemory 释放这个空间。
下面是描述会话的结构:WTS_SESSION_INFO:
typedef struct _WTS_SESSION_INFO{ DWORD SessionId; LPTSTR pWinStationName; WTS_CONNECTSTATE_CLASS State;} WTS_SESSION_INFO, * PWTS_SESSION_INFO;
结构中除了会话的 SessionId,还有会话名 pWinStationName,当前会话的名字是“console”,而其它的会话是无名的。当前的会话状态为 WTSActive,其它则为 WTSDisconnected。
下面是描述进程的结构 WTS_PROCESS_ INFO:
typedef struct _WTS_PROCESS_INFO { DWORD SessionId; DWORD ProcessId; LPTSTR pProcessName; PSID pUserSid;} WTS_PROCESS_INFO, * PWTS_PROCESS_INFO;
SessionId 与 ProcessIdToSessionId 所要找的值一样,ProcessId 不用说了,是进程ID。最后一个成员
pUserSid 指向安全标示符,描述用户账号,用户正是在这个账号下运行进程。使用 LookupAccountSid,你可以获得从
pUserSid 中获得用户名。这个信息已经可以通过 CProcess 类中的 GetProcessOwner
获得,但它是通过进程记号(token),而不是通过 WTS。某些情况下,即便由 WTSEnumerateProcesses
控制对它的提供,要想获得进程记号也是不可能的,这就是在 Windows XP 环境下要用 WTS API 而不用 PSAPI 或
TOOLHELP32 的缘故
VB代码例子:
Option Explicit
Private Const WTS_CURRENT_SERVER_HANDLE = 0&
Private Type WTS_PROCESS_INFO
SessionID As Long
ProcessID As Long
pProcessName As Long
pUserSid As Long
End Type
Private Declare Function WTSEnumerateProcesses _
Lib "wtsapi32.dll" Alias "WTSEnumerateProcessesA" _
(ByVal hServer As Long, ByVal Reserved As Long, _
ByVal Version As Long, ByRef ppProcessInfo As Long, _
ByRef pCount As Long _
) As Long
Private Declare Sub WTSFreeMemory Lib "wtsapi32.dll" _
(ByVal pMemory As Long)
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, Source As Any, ByVal Length As Long)
Private Sub Command1_Click()
GetWTSProcesses
End Sub
Private Function GetStringFromLP(ByVal StrPtr As Long) As String
Dim b As Byte
Dim tempStr As String
Dim bufferStr As String
Dim Done As Boolean
Done = False
Do
' Get the byte/character that StrPtr is pointing to.
CopyMemory b, ByVal StrPtr, 1
If b = 0 Then ' If you've found a null character, then you're done.
Done = True
Else
tempStr = Chr$(b) ' Get the character for the byte's value
bufferStr = bufferStr & tempStr 'Add it to the string
StrPtr = StrPtr + 1 ' Increment the pointer to next byte/char
End If
Loop Until Done
GetStringFromLP = bufferStr
End Function
Private Sub Form_Load()
ListView1.View = lvwReport
Command1.Caption = "Refresh"
'Add the Column Headers for your ListView Control
ListView1.ColumnHeaders.Add 1, "SessionID", "Session ID"
ListView1.ColumnHeaders.Add 2, "ProcessID", "Process ID"
ListView1.ColumnHeaders.Add 3, "ProcessName", "Process Name"
ListView1.ColumnHeaders.Add 4, "UserID", "User ID"
GetWTSProcesses
End Sub
Private Sub ListView1_ColumnClick(ByVal ColumnHeader As _
MSComctlLib.ColumnHeader)
' When a ColumnHeader object is clicked, the ListView control is
' sorted by the subitems of that column.
' Set the SortKey to the Index of the ColumnHeader - 1
ListView1.SortKey = ColumnHeader.Index - 1
' Set Sorted to True to sort the list.
ListView1.Sorted = True
End Sub
Private Sub GetWTSProcesses()
Dim RetVal As Long
Dim Count As Long
Dim i As Integer
Dim lpBuffer As Long
Dim p As Long
Dim udtProcessInfo As WTS_PROCESS_INFO
Dim itmAdd As ListItem
ListView1.ListItems.Clear
RetVal = WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, _
0&, _
1, _
lpBuffer, _
Count)
If RetVal Then ' WTSEnumerateProcesses was successful
p = lpBuffer
For i = 1 To Count
' Count is the number of Structures in the buffer
' WTSEnumerateProcesses returns a pointer, so copy it to a
' WTS_PROCESS_INO UDT so you can access its members
CopyMemory udtProcessInfo, ByVal p, LenB(udtProcessInfo)
' Add items to the ListView control
Set itmAdd = ListView1.ListItems.Add(i, , _
CStr(udtProcessInfo.SessionID))
itmAdd.SubItems(1) = CStr(udtProcessInfo.ProcessID)
' Since pProcessName contains a pointer, call GetStringFromLP to get the
' variable length string it points to
itmAdd.SubItems(2) = GetStringFromLP(udtProcessInfo.pProcessName)
itmAdd.SubItems(3) = CStr(udtProcessInfo.pUserSid)
' Increment to next WTS_PROCESS_INO structure in the buffer
p = p + LenB(udtProcessInfo)
Next i
Set itmAdd = Nothing
WTSFreeMemory lpBuffer 'Free your memory buffer
Else
' Error occurred calling WTSEnumerateProcesses
' Check Err.LastDllError for error code
MsgBox "Error occurred calling WTSEnumerateProcesses. " & _
"Check the Platform SDK error codes in the MSDN Documentation" _
& " for more information.", vbCritical, "Error " & Err.LastDllError
End If
End Sub