Test-IsolatedProcess.ps1 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #Requires -PSEdition Desktop
  2. param(
  3. [Parameter(Mandatory = $true)][string]$Executable,
  4. [string]$ArgumentLine = '',
  5. [Parameter(Mandatory = $true)][string]$OutputDirectory,
  6. [int]$TimeoutSeconds = 30,
  7. [switch]$ExpectMainWindow
  8. )
  9. $ErrorActionPreference = 'Stop'
  10. $Executable = [IO.Path]::GetFullPath($Executable)
  11. $OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
  12. New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
  13. # Run our own test process on an inactive desktop. FormMain moves itself to the
  14. # foreground during startup, so merely setting WindowStyle Hidden is insufficient.
  15. if (-not ('RedIsolatedTest' -as [type])) {
  16. Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @'
  17. using System;
  18. using System.Collections.Generic;
  19. using System.ComponentModel;
  20. using System.Diagnostics;
  21. using System.Drawing;
  22. using System.IO;
  23. using System.Runtime.InteropServices;
  24. using System.Text;
  25. using System.Threading;
  26. public static class RedIsolatedTest {
  27. [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  28. private struct StartupInfo {
  29. public int cb; public string reserved; public string desktop; public string title;
  30. public int x, y, width, height, charsX, charsY, fill, flags;
  31. public short show, reservedSize;
  32. public IntPtr reservedPointer, input, output, error;
  33. }
  34. [StructLayout(LayoutKind.Sequential)]
  35. private struct ProcessInfo { public IntPtr process, thread; public uint id, threadId; }
  36. [StructLayout(LayoutKind.Sequential)]
  37. private struct Rect { public int left, top, right, bottom; }
  38. private delegate bool EnumWindow(IntPtr window, IntPtr parameter);
  39. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  40. private static extern IntPtr CreateDesktop(string name, IntPtr device, IntPtr mode, int flags, uint access, IntPtr security);
  41. [DllImport("user32.dll")] private static extern bool CloseDesktop(IntPtr desktop);
  42. [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  43. private static extern bool CreateProcess(string file, StringBuilder command, IntPtr processSecurity, IntPtr threadSecurity,
  44. bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out ProcessInfo process);
  45. [DllImport("kernel32.dll")] private static extern uint WaitForSingleObject(IntPtr handle, uint timeout);
  46. [DllImport("kernel32.dll")] private static extern bool GetExitCodeProcess(IntPtr process, out uint code);
  47. [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
  48. [DllImport("user32.dll")] private static extern bool EnumDesktopWindows(IntPtr desktop, EnumWindow callback, IntPtr parameter);
  49. [DllImport("user32.dll")] private static extern bool EnumChildWindows(IntPtr parent, EnumWindow callback, IntPtr parameter);
  50. [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr window, out uint process);
  51. [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowText(IntPtr window, StringBuilder text, int max);
  52. [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr window, StringBuilder text, int max);
  53. [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr window, out Rect rectangle);
  54. [DllImport("user32.dll")] private static extern bool PrintWindow(IntPtr window, IntPtr context, uint flags);
  55. public static int Run(string executable, string arguments, string output, int seconds, bool expectWindow) {
  56. string name = "RedTest-" + Guid.NewGuid().ToString("N");
  57. IntPtr desktop = CreateDesktop(name, IntPtr.Zero, IntPtr.Zero, 0, 0x01FF, IntPtr.Zero);
  58. if (desktop == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
  59. ProcessInfo process = new ProcessInfo();
  60. try {
  61. var startup = new StartupInfo { cb = Marshal.SizeOf(typeof(StartupInfo)), desktop = name };
  62. var command = new StringBuilder("\"" + executable + "\" " + arguments);
  63. if (!CreateProcess(executable, command, IntPtr.Zero, IntPtr.Zero, false, 0x08000000,
  64. IntPtr.Zero, Path.GetDirectoryName(executable), ref startup, out process))
  65. throw new Win32Exception(Marshal.GetLastWin32Error());
  66. bool exited = WaitForSingleObject(process.process, (uint)(seconds * 1000)) == 0;
  67. uint code;
  68. if (exited) {
  69. GetExitCodeProcess(process.process, out code);
  70. File.WriteAllText(Path.Combine(output, "isolated-process.txt"), "Process exited: " + code);
  71. if (expectWindow) throw new Exception("Launcher exited before main-window inspection: " + code);
  72. return (int)code;
  73. }
  74. var windows = new List<string>();
  75. IntPtr main = IntPtr.Zero;
  76. bool errorDialog = false;
  77. EnumDesktopWindows(desktop, delegate(IntPtr window, IntPtr parameter) {
  78. uint owner;
  79. GetWindowThreadProcessId(window, out owner);
  80. if (owner != process.id) return true;
  81. var title = new StringBuilder(512);
  82. var kind = new StringBuilder(256);
  83. GetWindowText(window, title, title.Capacity);
  84. GetClassName(window, kind, kind.Capacity);
  85. windows.Add(kind + " | " + title);
  86. if (title.ToString() == "PCL Communist" && kind.ToString().StartsWith("HwndWrapper")) main = window;
  87. if (kind.ToString() == "#32770") {
  88. errorDialog = true;
  89. EnumChildWindows(window, delegate(IntPtr child, IntPtr unused) {
  90. var content = new StringBuilder(8192);
  91. GetWindowText(child, content, content.Capacity);
  92. if (content.Length > 0) windows.Add(" " + content);
  93. return true;
  94. }, IntPtr.Zero);
  95. }
  96. return true;
  97. }, IntPtr.Zero);
  98. File.WriteAllLines(Path.Combine(output, "isolated-process.txt"), windows);
  99. if (!expectWindow) throw new TimeoutException("Test process exceeded " + seconds + " seconds.");
  100. if (main == IntPtr.Zero || errorDialog) throw new Exception("Launcher main window missing or native error dialog detected. See isolated-process.txt.");
  101. Rect bounds;
  102. GetWindowRect(main, out bounds);
  103. using (var bitmap = new Bitmap(bounds.right - bounds.left, bounds.bottom - bounds.top))
  104. using (var graphics = Graphics.FromImage(bitmap)) {
  105. IntPtr dc = graphics.GetHdc();
  106. bool captured;
  107. try { captured = PrintWindow(main, dc, 2); }
  108. finally { graphics.ReleaseHdc(dc); }
  109. if (captured) bitmap.Save(Path.Combine(output, "standalone-startup.png"), System.Drawing.Imaging.ImageFormat.Png);
  110. }
  111. File.AppendAllText(Path.Combine(output, "isolated-process.txt"), Environment.NewLine + "PASS: standalone launcher main window is alive; no native initialization error dialog.");
  112. return 0;
  113. } finally {
  114. if (process.process != IntPtr.Zero) {
  115. if (WaitForSingleObject(process.process, 0) != 0) {
  116. try { Process.GetProcessById((int)process.id).Kill(); } catch (ArgumentException) { }
  117. WaitForSingleObject(process.process, 5000);
  118. }
  119. CloseHandle(process.thread);
  120. CloseHandle(process.process);
  121. }
  122. CloseDesktop(desktop);
  123. }
  124. }
  125. }
  126. '@
  127. }
  128. $testExitCode = [RedIsolatedTest]::Run($Executable, $ArgumentLine, $OutputDirectory, $TimeoutSeconds, $ExpectMainWindow.IsPresent)
  129. if ($testExitCode -ne 0) { throw "Isolated test failed with exit code $testExitCode. See $OutputDirectory." }
  130. Write-Output "PASS: isolated process check. Results: $OutputDirectory"