Pantalla de captura en la sesión de escritorio del servidor

Desarrollé un marco de prueba GUI que realiza pruebas de integración del sitio web de la compañía de manera progtwigda. Cuando algo falla, tomará una captura de pantalla del escritorio, entre otras cosas. Esto se ejecuta sin supervisión en un usuario conectado en un servidor de Windows dedicado 2008.

El problema es tomar una captura de pantalla en un escritorio del que he desconectado mi sesión de escritorio remoto. Obtengo la siguiente excepción:

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 

El método TakeScreenshot () se ve así:

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

Me he asegurado de que el protector de pantalla esté configurado en “Ninguno” sin tiempo de espera. También he implementado una pieza de código que hace un par de pinvokes para enviar un movimiento de mouse , esperando que genere un control gráfico de escritorio … pero no.

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

Cualquier consejo es apreciado.

Para capturar la pantalla, necesita ejecutar un progtwig en la sesión de un usuario. Eso es porque sin el usuario no hay forma de tener un escritorio asociado.

Para resolver esto, puede ejecutar una aplicación de escritorio para tomar la imagen, esta aplicación se puede invocar en la sesión del usuario activo, esto se puede hacer desde un servicio.

El siguiente código le permite invocar una aplicación de escritorio de tal manera que se ejecute en el escritorio del usuario local.

Si necesita ejecutar como un usuario en particular, verifique el código en el artículo ¿ Permitir que el servicio interactúe con el escritorio? Ay. . También puede considerar usar la función LogonUser .

El código:

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

Este código es una modificación del que se encuentra en el artículo ¿ Permitir que el servicio interactúe con el escritorio? Ay. (DEBE LEER)


Apéndice:

El código anterior permite ejecutar un progtwig en el escritorio del usuario registrado localmente en la máquina. Este método es específico para el usuario local actual, pero es posible hacerlo para cualquier usuario. Compruebe el código en el artículo ¿ Permitir que el servicio interactúe con el escritorio? Ay. para un ejemplo.

El núcleo de este método es la función CreateProcessAsUser , puede encontrar más información en MSDN .

Reemplace "Executable Path" con la ruta del ejecutable para ejecutar. Reemplace "Command Line" con la cadena pasada como argumentos de ejecución, y reemplace "Working Directory" con el directorio de trabajo que desee. Por ejemplo, puede extraer la carpeta de la ruta ejecutable:

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

Si tiene un servicio, puede usar este código en el servicio para invocar una aplicación de escritorio. Esa aplicación de escritorio también puede ser el ejecutable del servicio … para eso puede usar Assembly.GetExecutingAssembly().Location como la ruta ejecutable. Luego puede usar System.Environment.UserInteractive para detectar si el ejecutable no se está ejecutando como un servicio y pasar como argumentos de ejecución información sobre la tarea que se debe realizar. En el contexto de esta respuesta que es capturar la pantalla (por ejemplo, con CopyFromScreen ), podría ser otra cosa.

Lo que hice para resolver esto es llamar a tscon.exe y decirle que redirija la sesión a la consola justo antes de que se tome la captura de pantalla . Funciona así (tenga en cuenta que este código exacto no se ha probado):

 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 } 

Esta no es una característica compatible, es cierto que funciona en XP y Windows Server 2003, sin embargo, esto se ve como un defecto de seguridad.

Para evitar esto, no use la ‘x’ para cerrar la conexión remota, pero use% windir% \ system32 \ tscon.exe 0 / dest: console en su lugar. (Eso asegurará que la pantalla no esté bloqueada). – Nicolas Voron

Es cierto que si se desconecta del servidor de esta manera, la “pantalla” no se bloqueará para garantizar que permanezca desbloqueada, debe asegurarse de apagar el protector de pantalla, ya que tan pronto como se inicie, bloqueará automáticamente la pantalla.

Hay bastantes ejemplos solo de personas que hacen lo mismo, incluso aquí en el desbordamiento de stack. La publicación siguiente sugiere que cree una aplicación de Windows que se ejecute bajo una cuenta de usuario real que envíe capturas de pantalla a través de IPC al servicio en ejecución.

La forma correcta de obtener una GUI personalizada que funcione con un servicio es separarlos en dos procesos y realizar algún tipo de IPC (comunicación entre procesos). Por lo tanto, el servicio se iniciará cuando aparezca la máquina y se iniciará una aplicación GUI en la sesión del usuario. En ese caso, la GUI puede crear una captura de pantalla, enviarla al servicio y el servicio puede hacerlo, lo que quiera. – Captura de pantalla del proceso bajo el Servicio de Windows

He cotejado algunas estrategias que he encontrado en línea que pueden darte algunas ideas.

Software de terceros

Hay muchos progtwigs que hacen capturas de pantalla de sitios web como http://www.websitescreenshots.com/ tienen una interfaz de usuario y una herramienta de línea de comandos. Pero si está utilizando algún marco de prueba, es posible que esto no funcione, ya que hará una nueva solicitud para recuperar todos los activos y dibujar la página.

Control WebBrowser

No estoy seguro de qué navegador está utilizando para probar el sitio web de su compañía, sin embargo, si no está preocupado acerca de qué navegador es, podría usar un control WebBrowser y usar el método DrawToBitmap .

Virtualización

He visto un sistema donde los desarrolladores usaban entornos virtuales con el navegador de su elección con todas las configuraciones hechas para asegurarse de que la máquina no se bloqueara y, si lo hiciera, se reiniciaría.

Selenio

También es posible usar selenium con el destornillador de selenium y una gem de Ruby sin cabeza desarrollada por leonid-shevtsov, si su prueba está en selenium, este enfoque podría ser el mejor. El propio selenium admite la captura de pantalla en los controladores de red que tienen disponibles.

Por supuesto, todo esto depende de lo que esté utilizando para su marco de prueba, si puede compartir algunos detalles sobre su configuración, podremos darle una mejor respuesta.

El problema parece ser que cuando cierra la conexión remota, la pantalla entra en un estado bloqueado que impide que el sistema realice operaciones gráficas como su g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);

Para evitar esto, no use la ‘x’ para cerrar la conexión remota, pero use %windir%\system32\tscon.exe 0 /dest:console lugar. (Eso asegurará que la pantalla no esté bloqueada).

Lea esta publicación para obtener más información (en VBA, pero c # – entendible ;-))

EDITAR Si desea hacerlo directamente en c #, intente algo como esto:

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

Encontré una pregunta similar Captura de pantalla con problemas de C # y Escritorio remoto . Espero que te ayude a resolver el problema.

Aquí está el código de esa respuesta:

 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); // restre 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; } 

Creo que el problema puede ser que estás en el WindowStation equivocado. Echa un vistazo a estos artículos;

¿Por qué la pantalla de impresión en un servicio de Windows devuelve una imagen negra?

Captura de pantalla del servicio de Windows

Podría ser que tu Win-Station desaparezca cuando te desconectas. ¿Está ejecutando la aplicación cuando inicia sesión y luego intenta dejarla en funcionamiento cuando se desconecta?

Si es así, ¿todavía lo hace si se conecta con ” mstsc /admin “? En otras palabras, ¿conectando y ejecutándose en la sesión de la consola? Si no, esto podría ser una solución.