WTS APIs(Windows终端服务API)获取进程信息
 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