在服务器桌面会话上捕获屏幕

我开发了一个GUI测试框架,可以按计划对我们公司网站进行集成测试。 当某些内容失败时,它会截取桌面的屏幕截图等。 这在专用Windows Server 2008上的登录用户上无人值守。

问题是在我已断开远程桌面会话的桌面上截取屏幕截图。 我得到以下exception:

System.ComponentModel.Win32Exception (0x80004005): The handle is invalid at System.Drawing.Graphics.CopyFromScreen(Int32 sourceX, Int32 sourceY, Int32 destinationX, Int32 destinationY, Size blockRegionSize, CopyPixelOperation copyPixelOperation) at System.Drawing.Graphics.CopyFromScreen(Point upperLeftSource, Point upperLeftDestination, Size blockRegionSize) at IntegrationTester.TestCaseRunner.TakeScreenshot(String name) in C:\VS2010\IntegrationTester\IntegrationTester\Config\TestCaseRunner.cs:line 144 at IntegrationTester.TestCaseRunner.StartTest() in C:\VS2010\IntegrationTester\IntegrationTester\Config\TestCaseRunner.cs:line 96 

TakeScreenshot()方法如下所示:

 public static void TakeScreenshot(string name) { var bounds = Screen.GetBounds(Point.Empty); using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height)) { using (Graphics g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size); } bitmap.Save("someFileName", ImageFormat.Jpeg); } } 

我确保屏幕保护程序设置为“无”,没有超时。 我还实现了一段代码,它可以执行一些pinvokes来发送鼠标移动 ,希望它能生成桌面图形处理..但是没有。

 IntPtr hWnd = GetForegroundWindow(); if (hWnd != IntPtr.Zero) SendMessage(hWnd, 0x200, IntPtr.Zero, IntPtr.Zero); 

任何建议表示赞赏。

要捕获屏幕,您需要在用户的会话中运行程序。 这是因为没有用户就无法关联桌面。

要解决此问题,您可以运行桌面应用程序来获取映像,此应用程序可以在活动用户的会话中调用,这可以通过服务完成。

下面的代码允许您以在本地用户桌面上运行的方式调用桌面应用程序。

如果您需要以特定用户身份执行,请查看“ 允许服务与桌面交互”一文中的代码? 哎哟。 。 您还可以考虑使用LogonUserfunction。

代码:

 public void Execute() { IntPtr sessionTokenHandle = IntPtr.Zero; try { sessionTokenHandle = SessionFinder.GetLocalInteractiveSession(); if (sessionTokenHandle != IntPtr.Zero) { ProcessLauncher.StartProcessAsUser("Executable Path", "Command Line", "Working Directory", sessionTokenHandle); } } catch { //What are we gonna do? } finally { if (sessionTokenHandle != IntPtr.Zero) { NativeMethods.CloseHandle(sessionTokenHandle); } } } internal static class SessionFinder { private const int INT_ConsoleSession = -1; internal static IntPtr GetLocalInteractiveSession() { IntPtr tokenHandle = IntPtr.Zero; int sessionID = NativeMethods.WTSGetActiveConsoleSessionId(); if (sessionID != INT_ConsoleSession) { if (!NativeMethods.WTSQueryUserToken(sessionID, out tokenHandle)) { throw new System.ComponentModel.Win32Exception(); } } return tokenHandle; } } 

 internal static class ProcessLauncher { internal static void StartProcessAsUser(string executablePath, string commandline, string workingDirectory, IntPtr sessionTokenHandle) { var processInformation = new NativeMethods.PROCESS_INFORMATION(); try { var startupInformation = new NativeMethods.STARTUPINFO(); startupInformation.length = Marshal.SizeOf(startupInformation); startupInformation.desktop = string.Empty; bool result = NativeMethods.CreateProcessAsUser ( sessionTokenHandle, executablePath, commandline, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, workingDirectory, ref startupInformation, ref processInformation ); if (!result) { int error = Marshal.GetLastWin32Error(); string message = string.Format("CreateProcessAsUser Error: {0}", error); throw new ApplicationException(message); } } finally { if (processInformation.processHandle != IntPtr.Zero) { NativeMethods.CloseHandle(processInformation.processHandle); } if (processInformation.threadHandle != IntPtr.Zero) { NativeMethods.CloseHandle(processInformation.threadHandle); } if (sessionTokenHandle != IntPtr.Zero) { NativeMethods.CloseHandle(sessionTokenHandle); } } } } internal static class NativeMethods { [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] internal static extern bool CloseHandle(IntPtr handle); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern bool CreateProcessAsUser(IntPtr tokenHandle, string applicationName, string commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandle, int creationFlags, IntPtr envrionment, string currentDirectory, ref STARTUPINFO startupInfo, ref PROCESS_INFORMATION processInformation); [DllImport("Kernel32.dll", EntryPoint = "WTSGetActiveConsoleSessionId")] internal static extern int WTSGetActiveConsoleSessionId(); [DllImport("WtsApi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool WTSQueryUserToken(int SessionId, out IntPtr phToken); [StructLayout(LayoutKind.Sequential)] internal struct PROCESS_INFORMATION { public IntPtr processHandle; public IntPtr threadHandle; public int processID; public int threadID; } [StructLayout(LayoutKind.Sequential)] internal struct STARTUPINFO { public int length; public string reserved; public string desktop; public string title; public int x; public int y; public int width; public int height; public int consoleColumns; public int consoleRows; public int consoleFillAttribute; public int flags; public short showWindow; public short reserverd2; public IntPtr reserved3; public IntPtr stdInputHandle; public IntPtr stdOutputHandle; public IntPtr stdErrorHandle; } } 

此代码是对“ 允许服务与桌面交互”一文中的修改。 哎哟。 (必读)


附录:

上面的代码允许在本地记录在机器上的用户的桌面上执行程序。 此方法特定于当前本地用户,但可以为任何用户执行此操作。 检查文章允许服务与桌面交互的代码? 哎哟。 举个例子。

此方法的核心是CreateProcessAsUser函数,您可以在MSDN上找到更多相关信息。

"Executable Path"替换为要运行的可执行文件的路径。 将"Command Line"替换为作为执行参数传递的字符串,并将"Working Directory"替换为所需的工作目录。 例如,您可以提取可执行路径的文件夹:

  internal static string GetFolder(string path) { var folder = System.IO.Directory.GetParent(path).FullName; if (!folder.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) { folder += System.IO.Path.DirectorySeparatorChar; } return folder; } 

如果您有服务,则可以在服务中使用此代码来调用桌面应用程序。 该桌面应用程序也可以是服务可执行文件…为此,您可以使用Assembly.GetExecutingAssembly().Location作为可执行文件路径。 然后,您可以使用System.Environment.UserInteractive来检测可执行文件是否未作为服务运行,并作为执行参数传递有关所需任务的信息。 在这个捕获屏幕的答案的上下文中(例如使用CopyFromScreen ),它可能是其他东西。

我解决这个问题的方法是调用tscon.exe并告诉它在截屏之前将会话重定向回控制台。 它是这样的(注意,这个确切的代码是未经测试的):

 public static void TakeScreenshot(string path) { try { InternalTakeScreenshot(path); } catch(Win32Exception) { var winDir = System.Environment.GetEnvironmentVariable("WINDIR"); Process.Start( Path.Combine(winDir, "system32", "tscon.exe"), String.Format("{0} /dest:console", GetTerminalServicesSessionId())) .WaitForExit(); InternalTakeScreenshot(path); } } static void InternalTakeScreenshot(string path) { var point = new System.Drawing.Point(0,0); var bounds = System.Windows.Forms.Screen.GetBounds(point); var size = new System.Drawing.Size(bounds.Width, bounds.Height); var screenshot = new System.Drawing.Bitmap(bounds.Width, bounds.Height); var g = System.Drawing.Graphics.FromImage(screenshot) g.CopyFromScreen(0,0,0,0,size); screenshot.Save(path, System.Drawing.Imaging.ImageFormat.Jpeg); } [DllImport("kernel32.dll")] static extern bool ProcessIdToSessionId(uint dwProcessId, out uint pSessionId); static uint GetTerminalServicesSessionId() { var proc = Process.GetCurrentProcess(); var pid = proc.Id; var sessionId = 0U; if(ProcessIdToSessionId((uint)pid, out sessionId)) return sessionId; return 1U; // fallback, the console session is session 1 } 

这不是一个支持的function,它确实适用于XP和Windows Server 2003,但这被视为安全漏洞。

要防止这种情况,请不要使用“x”关闭远程连接,而是使用%windir%\ system32 \ tscon.exe 0 / dest:console。 (这将确保屏幕未锁定)。 – 尼古拉斯沃隆

确实,如果您以这种方式断开与服务器的连接,“屏幕”将无法锁定以确保其保持解锁状态,您需要确保关闭屏幕保护程序,因为一旦启动它将自动锁定您的屏幕。

有很多例子只是人们做同样的事情,即使在这里堆栈溢出,下面的post建议你创建一个在真实的用户帐户下运行的Windows应用程序,通过IPC向正在运行的服务发送屏幕截图。

获得与服务一起使用的自定义GUI的正确方法是将它们分成两个进程并进行某种IPC(进程间通信)。 因此,当机器启动并且将在用户会话中启动GUI应用程序时,服务将启动。 在这种情况下,GUI可以创建屏幕截图,将其发送到服务,服务可以使用它,无论您喜欢什么。 – Windows服务下的进程截图

我已经整理了一些我在网上找到的可能会给你一些想法的策略。

第三方软件

有很多程序可以制作网站的屏幕截图,例如http://www.websitescreenshots.com/,它们有一个用户界面和命令行工具。 但是如果你使用的是一些测试框架,那么这可能不会起作用,因为它会提出新的请求以获取所有资产并绘制页面。

WebBrowser控件

我不确定您使用什么浏览器来测试您的公司网站,但如果您不关心哪个浏览器,您可以使用WebBrowser控件并使用DrawToBitmap方法 。

虚拟化

我见过一个系统,开发人员使用他们选择的浏览器使用虚拟环境,并完成所有设置以确保机器没有锁定,如果确实如此,它将重新启动。

selenium

如果您的测试是在selenium中,这种方法可能是最好的,也可以使用selenium与selenium – 网络驱动器和无头的leonid-shevtsov开发的rubygem。 Selenium本身支持他们可用的网络驱动程序上的屏幕截图。

当然,所有这些都取决于您在测试框架中使用的内容,如果您可以分享有关设置的一些详细信息,我们将能够为您提供更好的答案。

问题似乎是当你关闭远程连接时,屏幕进入锁定状态,这阻止系统执行图形操作,如g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);

要防止这种情况,请不要使用“x”关闭远程连接,而是使用%windir%\system32\tscon.exe 0 /dest:console 。 (这将确保屏幕未锁定)。

阅读这篇文章了解更多信息(在VBA中,但是c#-understandable ;-))

编辑如果你想直接在c#中做,请尝试这样的事情:

 Process p = new Process(); p.StartInfo.FileName = "tscon"; p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; p.StartInfo.Arguments = "0 /dest:console"; p.Start(); 

我发现了类似的问题用C#和远程桌面问题进行屏幕截图 。 希望它可以帮助您解决问题。

以下是该答案的代码:

 public Image CaptureWindow(IntPtr handle) { // get te hDC of the target window IntPtr hdcSrc = User32.GetWindowDC(handle); // get the size User32.RECT windowRect = new User32.RECT(); User32.GetWindowRect(handle, ref windowRect); int width = windowRect.right - windowRect.left; int height = windowRect.bottom - windowRect.top; // create a device context we can copy to IntPtr hdcDest = GDI32.CreateCompatibleDC(hdcSrc); // create a bitmap we can copy it to, // using GetDeviceCaps to get the width/height IntPtr hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height); // select the bitmap object IntPtr hOld = GDI32.SelectObject(hdcDest, hBitmap); // bitblt over GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, GDI32.SRCCOPY); // restore selection GDI32.SelectObject(hdcDest, hOld); // clean up GDI32.DeleteDC(hdcDest); User32.ReleaseDC(handle, hdcSrc); // get a .NET image object for it Image img = Image.FromHbitmap(hBitmap); // free up the Bitmap object GDI32.DeleteObject(hBitmap); return img; } 

我认为问题可能是你在错误的WindowStation上。 看看这些文章;

为什么Windows服务中的打印屏幕返回黑色图像?

从Windows服务截屏

当你断开连接时,你的win-station可能会消失。 您是否在登录时运行应用程序,然后在断开连接时尝试使其保持运行状态?

如果是这样,如果您使用“ mstsc /admin ”连接,它仍然可以吗? 换句话说,连接到控制台会话并在其上运行? 如果没有,这可能是一种解决方法。