【问题标题】:Capture screen on server desktop session在服务器桌面会话上捕获屏幕
【发布时间】:2011-07-09 04:35:47
【问题描述】:

我开发了一个 GUI 测试框架,可以按计划对我们公司的网站进行集成测试。当出现故障时,它会截取桌面截图等。这将在专用 Windows Server 2008 上的登录用户上无人看管地运行。

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

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);

感谢任何建议。

【问题讨论】:

  • SendMessage 到底是什么?我们是否要查找消息代码0x200?难道你不能为我们省去麻烦并放入适当的 WM_**。无论如何,您不能发送按键,您需要发布输入。在 C# 中,您应该使用 SendKeys()。不过,这可能不是你真正的问题!
  • 这是应用程序还是服务?
  • 如果你想伪造输入,那么你应该使用SendInput() 而不是'SendMessage()'。我不确定这会解决你的问题,但SendMessage(WM_MOUSE***, ...) 是完全错误的。
  • 目前是一个控制台应用程序。如果需要,它会保持原样。
  • 远程桌面和屏幕绘制存在已知(设计)问题,并非特定于 .NET/C#。您可以尝试此处描述的一些技巧:bharath-marrivada.blogspot.fr/2011/11/…

标签: c# .net windows-server-2008 pinvoke system.drawing


【解决方案1】:

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

为了解决这个问题,你可以运行一个桌面应用程序来获取图像,这个应用程序可以在活动用户的会话中调用,这可以通过服务来完成。

下面的代码允许您调用桌面应用程序,使其在本地用户的桌面上运行。

如果您需要以特定用户身份执行,请查看文章Allow service to interact with desktop? Ouch. 中的代码。也可以考虑使用LogonUser函数。

代码:

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;
    }
}

此代码是对文章 Allow service to interact with desktop? Ouch.(必读)中的代码的修改


附录:

上面的代码允许在机器上本地登录的用户的桌面上执行程序。此方法特定于当前本地用户,但可以为任何用户执行此操作。查看文章Allow service to interact with desktop? Ouch.中的代码以获取示例。

这个方法的核心是函数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),它可能是别的东西。

【讨论】:

    【解决方案2】:

    我解决这个问题的方法是调用 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
    }
    

    【讨论】:

      【解决方案3】:

      这不是一个受支持的功能,它确实可以在 XP 和 windows server 2003 中运行,但这被视为安全漏洞。

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

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

      即使在堆栈溢出时,也有很多人在做同样的事情。下面的帖子建议您创建一个在实际用户帐户下运行的 Windows 应用程序,该应用程序通过 IPC 将屏幕截图发送到正在运行的服务。

      获得与服务一起使用的自定义 GUI 的正确方法是 将它们分成两个进程并执行某种 IPC(inter 进程通信)。所以服务会在机器启动时 出现并在用户会话中启动一个 GUI 应用程序。在 在这种情况下,GUI 可以创建屏幕截图,将其发送到服务并 服务可以随心所欲,随心所欲。 - Screenshot of process under Windows Service

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

      第三方软件

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

      WebBrowser 控件

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

      虚拟化

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

      如果您的测试是在 selenium 中进行的,那么也可以将 selenium 与 selenium-webdriver 和由 leonid-shevtsov 开发的无头红宝石 gem 一起使用,这种方法可能是最好的。 Selenium 本身支持在可用的网络驱动程序上进行屏幕捕获。

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

      【讨论】:

        【解决方案4】:

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

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

        阅读this post 了解更多信息(在 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();
        

        【讨论】:

        • 问题是,随机开发人员正在远程桌面进入随机机器(这是一个测试自动化农场),我无法让他们可靠地做到这一点......
        【解决方案5】:

        我发现了一个类似的问题Screen capture with C# and Remote Desktop problems。希望它可以帮助您解决问题。

        这是该答案的代码:

        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; 
        }
        

        【讨论】:

          【解决方案6】:

          我认为问题可能在于您使用了错误的 WindowStation。看看这些文章;

          Why does print screen in a Windows Service return a black image?

          Screen capture from windows service

          当您断开连接时,您的胜利站可能会消失。您是否在登录时运行应用程序,然后在断开连接时尝试让它运行?

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-06-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多