如何通过IP远程查询虚拟机当前连接用户(基于WTSQuerySessionInformation)
远程获取虚拟机终端服务连接用户的解决方案
看起来你已经搞定了本地获取终端服务用户的逻辑,现在要把这个能力扩展到远程虚拟机,不用每台都部署程序对吧?其实WTSAPI本身就支持远程调用,核心是先建立和远程服务器的连接,再查询会话信息。
核心思路调整
你当前的代码用的是本地服务器句柄(IntPtr.Zero),要远程访问的话,需要先通过WTSOpenServer函数获取目标虚拟机的服务器句柄,然后用这个句柄去查询会话信息。另外,只查当前会话可能不够,最好遍历所有活跃会话,才能拿到所有连接用户。
修改后的完整代码示例
先补充需要的API导入,然后实现远程查询的逻辑:
using System; using System.Collections.Generic; using System.Runtime.InteropServices; public class RemoteSessionInfo { // 添加远程服务器连接相关的API [DllImport("Wtsapi32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr WTSOpenServer(string pServerName); [DllImport("Wtsapi32.dll")] private static extern void WTSCloseServer(IntPtr hServer); [DllImport("Wtsapi32.dll")] private static extern bool WTSEnumerateSessions( IntPtr hServer, int Reserved, int Version, out IntPtr ppSessionInfo, out int pCount); [DllImport("Wtsapi32.dll")] private static extern void WTSFreeMemory(IntPtr pMemory); [DllImport("Wtsapi32.dll", CharSet = CharSet.Unicode)] private static extern bool WTSQuerySessionInformation( IntPtr hServer, int sessionId, WTS_INFO_CLASS wtsInfoClass, out IntPtr ppBuffer, out uint pBytesReturned); public enum WTS_INFO_CLASS { WTSInitialProgram, WTSApplicationName, WTSWorkingDirectory, WTSOEMId, WTSSessionId, WTSUserName, WTSWinStationName, WTSDomainName, WTSConnectState, WTSClientBuildNumber, WTSClientName, WTSClientDirectory, WTSClientProductId, WTSClientHardwareId, WTSClientAddress, WTSClientDisplay, WTSClientProtocolType } [StructLayout(LayoutKind.Sequential)] private struct WTS_SESSION_INFO { public int SessionId; [MarshalAs(UnmanagedType.LPStr)] public string pWinStationName; public WTS_CONNECTSTATE_CLASS State; } public enum WTS_CONNECTSTATE_CLASS { WTSActive, WTSConnected, WTSConnectQuery, WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit } /// <summary> /// 获取远程虚拟机的所有活跃会话用户信息 /// </summary> /// <param name="remoteServerIp">目标虚拟机IP</param> /// <returns>会话ID、用户名、客户端名称的字典</returns> public static Dictionary<int, (string UserName, string ClientName)> GetRemoteSessionUsers(string remoteServerIp) { var sessionUsers = new Dictionary<int, (string, string)>(); IntPtr serverHandle = IntPtr.Zero; IntPtr sessionInfoPtr = IntPtr.Zero; try { // 打开远程服务器连接 serverHandle = WTSOpenServer(remoteServerIp); if (serverHandle == IntPtr.Zero) { throw new System.ComponentModel.Win32Exception(); } // 枚举所有会话 int sessionCount; if (!WTSEnumerateSessions(serverHandle, 0, 1, out sessionInfoPtr, out sessionCount)) { throw new System.ComponentModel.Win32Exception(); } // 遍历每个会话 var sessionInfoSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); for (int i = 0; i < sessionCount; i++) { var currentSessionPtr = new IntPtr(sessionInfoPtr.ToInt64() + (i * sessionInfoSize)); var sessionInfo = (WTS_SESSION_INFO)Marshal.PtrToStructure(currentSessionPtr, typeof(WTS_SESSION_INFO)); // 只处理活跃/已连接的会话(根据需求调整状态) if (sessionInfo.State == WTS_CONNECTSTATE_CLASS.WTSActive || sessionInfo.State == WTS_CONNECTSTATE_CLASS.WTSConnected) { // 获取用户名 string userName = GetSessionInformation(serverHandle, sessionInfo.SessionId, WTS_INFO_CLASS.WTSUserName); // 获取客户端名称 string clientName = GetSessionInformation(serverHandle, sessionInfo.SessionId, WTS_INFO_CLASS.WTSClientName); if (!string.IsNullOrEmpty(userName)) { sessionUsers.Add(sessionInfo.SessionId, (userName, clientName)); } } } } finally { // 释放资源 if (sessionInfoPtr != IntPtr.Zero) { WTSFreeMemory(sessionInfoPtr); } if (serverHandle != IntPtr.Zero) { WTSCloseServer(serverHandle); } } return sessionUsers; } /// <summary> /// 获取单个会话的指定信息 /// </summary> private static string GetSessionInformation(IntPtr serverHandle, int sessionId, WTS_INFO_CLASS infoClass) { IntPtr buffer = IntPtr.Zero; uint bytesReturned; try { if (WTSQuerySessionInformation(serverHandle, sessionId, infoClass, out buffer, out bytesReturned) && buffer != IntPtr.Zero) { // 根据API字符集选择合适的转换方式,这里用Unicode return Marshal.PtrToStringUni(buffer); } return string.Empty; } finally { if (buffer != IntPtr.Zero) { WTSFreeMemory(buffer); } } } // 示例调用 public static void Main() { try { var remoteUsers = GetRemoteSessionUsers("192.168.1.100"); foreach (var session in remoteUsers) { Console.WriteLine($"会话ID: {session.Key}, 用户: {session.Value.UserName}, 客户端名称: {session.Value.ClientName}"); } } catch (Exception ex) { Console.WriteLine($"查询失败: {ex.Message}"); } } }
关键注意事项
- 权限要求:运行这个程序的Windows账号必须拥有目标虚拟机的管理员权限,或者至少有终端服务的远程查询权限(比如加入Remote Desktop Users组,但管理员权限更稳妥)。
- 网络配置:目标虚拟机的防火墙要允许终端服务(默认3389端口)的入站连接,同时终端服务(远程桌面服务)必须启用。
- 错误处理:代码里加了Win32Exception捕获,方便排查连接失败的原因(比如权限不足、网络不通、远程服务未开启等)。
- 字符集:原来的代码用了
PtrToStringAnsi,远程调用建议用PtrToStringUni,避免中文等非ASCII字符乱码。
这样你就可以在一台机器上运行这个程序,通过IP直接查询所有远程虚拟机的连接用户,不用在每台VM上部署任何东西啦。
内容的提问来源于stack exchange,提问作者AeroBiba
相关产品推荐
相关产品推荐

