Подтвердить что ты не робот

Захват экрана на сеансе рабочего стола сервера

Я разработал графический интерфейс 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);

Любые советы приветствуются.

4b9b3361

Ответ 1

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

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

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

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

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


Приложение:

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

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

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

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

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

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

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

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

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

Селен

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

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

Ответ 4

Проблема заключается в том, что при закрытии удаленного подключения экран переходит в состояние блокировки, которое предотвращает выполнение графической работы системы, как ваш g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);

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

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

ИЗМЕНИТЬ Если вы хотите сделать это прямо в С#, попробуйте что-то вроде этого:

Process p = new Process();

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

p.StartInfo.Arguments = "0 /dest:console";
p.Start();

Ответ 5

Я нашел аналогичный вопрос Захват экрана с проблемами С# и удаленного рабочего стола. Надеюсь, это поможет вам решить проблему.

Вот код из этого ответа:

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. Взгляните на эти статьи;

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

Захват экрана из службы Windows

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

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