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

时间:2011-03-04 23:17:21

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

我开发了一个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);
            }
        }

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

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

感谢任何建议。

6 个答案:

答案 0 :(得分:4)

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

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

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

如果您需要以特定用户身份执行,请查看文章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),它可能是其他内容。

答案 1 :(得分:3)

我要解决的问题是调用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
}

答案 2 :(得分:2)

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

  

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

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

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

  

获取与服务一起使用的自定义GUI的正确方法是   将它们分成两个过程并进行某种形式的IPC(inter   过程沟通)。因此服务将在机器启动时启动   启动并在用户会话中启动GUI应用程序。在   在这种情况下,GUI可以创建屏幕截图,将其发送到服务和   无论你喜欢什么,服务都可以做到。 - Screenshot of process under Windows Service

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

第三方软件

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

WebBrowser控件

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

虚拟化

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

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

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

答案 3 :(得分:1)

问题似乎是当你关闭远程连接时,屏幕进入锁定状态,阻止系统执行图形操作,如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();

答案 4 :(得分:0)

我发现了一个类似的问题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; 
}

答案 5 :(得分:0)

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

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

Screen capture from windows service

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

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