如何在Windows服务中切换用户桌面以录制RDP会话?
解决Windows服务Session 0隔离下的RDP桌面录制问题
问题背景
你基于Windows服务的OnSessionChange事件实现RDP登录监控,但受Session 0隔离限制,服务无法直接访问用户的交互式会话桌面,导致录制功能崩溃。核心原因是Windows服务默认运行在Session 0,而用户RDP登录属于独立的交互式会话(通常是Session 1及以上),两者的桌面环境完全隔离。
核心解决方案
要实现用户桌面录制,必须将录制进程切换到目标用户的交互式会话中运行,而不是在服务的Session 0中执行录制逻辑。具体通过以下Windows API实现:
WTSQueryUserToken:获取目标会话的用户令牌CreateEnvironmentBlock:创建用户会话的环境变量块CreateProcessAsUser:使用用户令牌在目标会话中启动录制进程
具体实现步骤
1. 添加Windows API声明
在你的服务项目中添加以下API声明(需引用System.Runtime.InteropServices):
using System.Runtime.InteropServices; public static class Win32Api { [DllImport("wtsapi32.dll", SetLastError = true)] public static extern bool WTSQueryUserToken(int sessionId, out IntPtr tokenHandle); [DllImport("userenv.dll", SetLastError = true)] public static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit); [DllImport("userenv.dll", SetLastError = true)] public static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } public const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; public const uint CREATE_NO_WINDOW = 0x08000000; } [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject);
2. 修改服务的OnSessionChange逻辑
在用户登录/连接/解锁时,启动独立的录制程序到目标会话;在用户注销/断开/锁定时停止录制:
// 先给RdpSession结构体添加进程ID字段 struct RdpSession { // ... 原有字段 public int sessionRecorderProcessId; // ... 原有方法 } protected override void OnSessionChange(SessionChangeDescription sessionChangeDescription) { try { var userInfo = TermServicesManager.GetSessionInfo(Dns.GetHostEntry("").HostName, sessionChangeDescription.SessionId); IPAddress ipAddress = new IPAddress(userInfo.ClientAddress.Address.Skip(2).Take(4).ToArray()); if (!rdpSessionList.ContainsKey(sessionChangeDescription.SessionId)) { onRdpSession.sessionId = sessionChangeDescription.SessionId; onRdpSession.ipAddress = ipAddress.ToString(); onRdpSession.userName = userInfo.UserName; onRdpSession.sessionDateTime = DateTime.Now.ToString("yyyy'-'MM'-'dd'T'HH'-'mm'-'ss"); onRdpSession.recordFileName = onRdpSession.generateRecordFileName(); rdpSessionList.Add(sessionChangeDescription.SessionId, onRdpSession); } else { onRdpSession = rdpSessionList[sessionChangeDescription.SessionId]; } WriteToFile("SessionChange event"); switch (sessionChangeDescription.Reason) { case SessionChangeReason.SessionLogon: case SessionChangeReason.RemoteConnect: case SessionChangeReason.SessionUnlock: WriteToFile($"SessionChange{DateTime.Now.ToLongTimeString()}, SessionUnlock|RemoteConnect|SessionLogon [{sessionChangeDescription.SessionId}], User: {userInfo.UserName}, Connect state: {userInfo.ConnectState}, Client address: {ipAddress}, WinStationName: {userInfo.WinStationName}"); // 启动录制进程到目标会话 StartRecorderInUserSession(sessionChangeDescription.SessionId, onRdpSession.recordFileName); break; case SessionChangeReason.SessionLock: case SessionChangeReason.SessionLogoff: case SessionChangeReason.RemoteDisconnect: WriteToFile($"SessionChange: {onRdpSession}, {DateTime.Now.ToLongTimeString()} RemoteDisconnect|SessionLogoff|SessionLock [{sessionChangeDescription.SessionId}], User: {userInfo.UserName}, Connect state: {userInfo.ConnectState}, Client address: {ipAddress}, WinStationName: {userInfo.WinStationName}"); // 停止对应会话的录制进程 StopRecorderForSession(sessionChangeDescription.SessionId); break; default: break; } } catch (Exception ex) { WriteToFile($"SessionChange exception: {ex.Message} || {sessionChangeDescription.SessionId} || {onRdpSession.sessionId}"); } } private void StartRecorderInUserSession(int sessionId, string recordFilePath) { IntPtr userToken = IntPtr.Zero; IntPtr environmentBlock = IntPtr.Zero; Win32Api.PROCESS_INFORMATION processInfo = new Win32Api.PROCESS_INFORMATION(); Win32Api.STARTUPINFO startupInfo = new Win32Api.STARTUPINFO(); startupInfo.cb = Marshal.SizeOf(startupInfo); // 指定用户会话的桌面格式 startupInfo.lpDesktop = $"WinSta0\\Default"; try { // 获取用户会话令牌 if (!Win32Api.WTSQueryUserToken(sessionId, out userToken)) { WriteToFile($"WTSQueryUserToken failed, error: {Marshal.GetLastWin32Error()}"); return; } // 创建环境变量块 if (!Win32Api.CreateEnvironmentBlock(out environmentBlock, userToken, false)) { WriteToFile($"CreateEnvironmentBlock failed, error: {Marshal.GetLastWin32Error()}"); return; } // 录制程序路径,替换为你实际的程序路径 string recorderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DesktopRecorder.exe"); // 传递录制文件路径和会话ID作为参数 string commandLine = $"\"{recorderPath}\" \"{recordFilePath}\" {sessionId}"; // 在用户会话中启动录制进程 if (!Win32Api.CreateProcessAsUser( userToken, null, commandLine, IntPtr.Zero, IntPtr.Zero, false, Win32Api.CREATE_UNICODE_ENVIRONMENT | Win32Api.CREATE_NO_WINDOW, environmentBlock, null, ref startupInfo, out processInfo)) { WriteToFile($"CreateProcessAsUser failed, error: {Marshal.GetLastWin32Error()}"); return; } // 保存进程ID用于后续停止 onRdpSession.sessionRecorderProcessId = processInfo.dwProcessId; rdpSessionList[sessionId] = onRdpSession; WriteToFile($"Recorder started for session {sessionId}, process ID: {processInfo.dwProcessId}"); } finally { // 释放资源 if (userToken != IntPtr.Zero) CloseHandle(userToken); if (environmentBlock != IntPtr.Zero) Win32Api.DestroyEnvironmentBlock(environmentBlock); if (processInfo.hProcess != IntPtr.Zero) CloseHandle(processInfo.hProcess); if (processInfo.hThread != IntPtr.Zero) CloseHandle(processInfo.hThread); } } private void StopRecorderForSession(int sessionId) { if (rdpSessionList.TryGetValue(sessionId, out var session)) { try { Process recorderProcess = Process.GetProcessById(session.sessionRecorderProcessId); recorderProcess.Kill(); recorderProcess.WaitForExit(); WriteToFile($"Recorder stopped for session {sessionId}"); } catch (Exception ex) { WriteToFile($"Failed to stop recorder: {ex.Message}"); } } }
3. 独立录制程序实现
创建一个单独的控制台应用程序(比如DesktopRecorder.exe),负责实际的桌面录制逻辑(示例使用SharpAvi库):
using SharpAvi; using SharpAvi.Codecs; using SharpAvi.Output; using System.Drawing; using System.Drawing.Imaging; using System.Threading; namespace DesktopRecorder { class Program { static void Main(string[] args) { if (args.Length < 2) { return; } string outputPath = args[0]; int sessionId = int.Parse(args[1]); var screenBounds = Screen.PrimaryScreen.Bounds; int width = screenBounds.Width; int height = screenBounds.Height; using (var writer = new AviWriter(outputPath) { FramesPerSecond = 15, EmitIndex1 = true }) { var videoStream = writer.AddVideoStream(width, height, CodecIds.MotionJpeg); videoStream.Quality = 70; // 持续录制直到进程被终止 while (!Console.KeyAvailable) { using (var bitmap = new Bitmap(width, height)) { using (var g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size); } var bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); videoStream.WriteFrame(true, bitmapData.Scan0, bitmapData.Stride * height); bitmap.UnlockBits(bitmapData); } Thread.Sleep(1000 / (int)writer.FramesPerSecond); } } } } }
注意事项
- 服务权限:服务必须以
LocalSystem账户运行,并且需要分配替换进程级令牌和作为批处理作业登录的权限(可通过本地安全策略配置)。 - 依赖管理:确保录制程序和服务在同一目录,或指定完整路径,且包含所有依赖库(比如SharpAvi的DLL文件)。
- 资源清理:用户注销/断开时必须停止录制进程,避免资源泄漏。
- 错误处理:所有API调用需检查返回值,处理Win32错误码,防止服务崩溃。
内容的提问来源于stack exchange,提问作者H Aßdöµ
相关产品推荐
相关产品推荐

