Test-IsolatedProcess.ps1 8.1 KB

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