Захват экрана на рабочем столе сервера - PullRequest
19 голосов
/ 05 марта 2011

Я разработал тестовый фреймворк с графическим интерфейсом, который регулярно проводит тестирование интеграции сайта нашей компании. Когда что-то не получится, он сделает снимок экрана рабочего стола, между прочим. Это выполняется без присмотра вошедшего в систему пользователя на выделенном 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 ]

4 голосов
/ 12 октября 2012

Для захвата экрана вам необходимо запустить программу в сеансе пользователя. Это связано с тем, что без пользователя невозможно связать рабочий стол.

Чтобы решить эту проблему, вы можете запустить настольное приложение для получения изображения, это приложение может быть вызвано в сеансе активного пользователя, это может быть сделано из службы.

Приведенный ниже код позволяет вам вызывать настольное приложение таким образом, чтобы оно работало на рабочем столе локального пользователя.

Если вам нужно работать от имени конкретного пользователя, проверьте код в статье Разрешить сервису взаимодействовать с рабочим столом? Уч. . Вы также можете рассмотреть возможность использования функции 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;
    }
}

Этот код является модификацией кода, найденного в статье Разрешить сервису взаимодействовать с рабочим столом? Ой (ДОЛЖЕН ЧИТАТЬ)


Приложение:

Приведенный выше код позволяет запускать программу на рабочем столе пользователя, зарегистрированного локально на компьютере. Этот метод специфичен для текущего локального пользователя, но это возможно сделать для любого пользователя. Проверьте код в статье Разрешить ли сервис взаимодействовать с рабочим столом? Ой. для примера.

Ядром этого метода является функция 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), это может быть что-то еще.

3 голосов
/ 11 октября 2012

Для решения этой проблемы я вызвал 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 голосов
/ 10 октября 2012

Это не поддерживаемая функция, это правда, что она работает в XP и Windows Server 2003, однако это рассматривается как недостаток безопасности.

Чтобы предотвратить это, не используйте 'x', чтобы закрыть удаленное соединение, но вместо этого используйте% windir% \ system32 \ tscon.exe 0 / dest: console. (Это гарантирует, что экран не заблокирован). - Николас Ворон

Это правда, что если вы отключите сервер таким образом, «экран» не будет заблокирован, чтобы он оставался разблокированным, вам нужно обязательно отключить экранную заставку, так как при запуске она автоматически заблокирует вас. экран.

Существует довольно много примеров того, как люди, делающие то же самое, даже здесь, при переполнении стека. В приведенном ниже посте предлагается создать приложение Windows, работающее под реальной учетной записью пользователя, которое отправляет снимки экрана через IPC в работающую службу.

Правильный способ получить пользовательский графический интерфейс, работающий со службой, состоит в разделить их на два процесса и сделать какой-то IPC (меж процесс общения). Таким образом, сервис будет запущен, когда машина появляется и приложение GUI будет запущено в пользовательской сессии. В В этом случае графический интерфейс может создать скриншот, отправить его в службу и сервис может делать с ним все что угодно. - Снимок экрана процесса в Windows Service

Я собрал несколько стратегий, которые я нашел в Интернете, которые могут дать вам некоторые идеи.

Программное обеспечение сторонних производителей

Существует множество программ, которые делают снимки экрана веб-сайтов, таких как http://www.websitescreenshots.com/, у них есть пользовательский интерфейс и инструмент командной строки. Но если вы используете какую-то инфраструктуру тестирования, это может не сработать, так как она сделает новый запрос на выборку всех ресурсов и отрисовку страницы.

Управление веб-браузером

Я не уверен, какой браузер вы используете для тестирования веб-сайта вашей компании, однако, если вас не интересует, какой браузер. Вы можете использовать элемент управления WebBrowser и использовать DrawToBitmap метод .

Виртуализация

Я видел систему, в которой разработчики использовали виртуальные среды с браузером по своему выбору со всеми настройками, чтобы убедиться, что машина не блокируется и если она это сделает, она перезапустится.

Селен

Можно также использовать селен с селена-webdriver и безголовым рубиновым камнем, разработанным leonid-shevtsov , если ваш тест в селене, этот подход может быть лучшим. Сам Selenium поддерживает захват экрана на имеющихся у них веб-драйверах.

Конечно, все это зависит от того, что вы используете для своей среды тестирования. Если вы сможете поделиться некоторыми подробностями вашей настройки, мы сможем дать вам лучший ответ.

1 голос
/ 09 октября 2012

Кажется, проблема в том, что при закрытии удаленного соединения экран переходит в заблокированное состояние, что не позволяет системе выполнять графические операции, такие как g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);

. Для предотвращения этого не используйте'x', чтобы закрыть удаленное соединение, но вместо этого используйте %windir%\system32\tscon.exe 0 /dest:console.(Это гарантирует, что экран не заблокирован).

Прочтите этот пост для получения дополнительной информации (в VBA, но c # -различимо ;-))

РЕДАКТИРОВАТЬ Если вы хотите сделать это непосредственно в C #, попробуйте что-то вроде этого:

Process p = new Process();

p.StartInfo.FileName = "tscon";
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;

p.StartInfo.Arguments = "0 /dest:console";
p.Start();
0 голосов
/ 11 октября 2012

Я думаю, что проблема может быть в том, что вы не на той WindowStation. Посмотрите на эти статьи;

Почему экран печати в службе Windows возвращает черное изображение?

Снимок экрана из службы Windows

Возможно, ваша win-станция исчезает, когда вы отключаетесь. Запускаете ли вы приложение при входе в систему, а затем пытаетесь оставить его работающим при отключении?

Если это так, он все еще делает это, если вы соединяетесь с "mstsc /admin"? Другими словами, подключаться и работать в сеансе консоли? Если нет, то это может быть обходной путь.

0 голосов
/ 11 октября 2012

Я нашел похожий вопрос Снимок экрана с проблемами 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; 
}
...