aboutsummaryrefslogtreecommitdiff
path: root/src/internal
diff options
context:
space:
mode:
Diffstat (limited to 'src/internal')
-rw-r--r--src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs288
-rw-r--r--src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs4
-rw-r--r--src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs2
3 files changed, 291 insertions, 3 deletions
diff --git a/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs b/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs
index eb07aa13..4a932645 100644
--- a/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs
+++ b/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs
@@ -2,11 +2,16 @@
2 2
3namespace WixBuildTools.TestSupport 3namespace WixBuildTools.TestSupport
4{ 4{
5 using System;
5 using System.Collections.Concurrent; 6 using System.Collections.Concurrent;
6 using System.Collections.Generic; 7 using System.Collections.Generic;
8 using System.ComponentModel;
7 using System.Diagnostics; 9 using System.Diagnostics;
8 using System.IO; 10 using System.IO;
11 using System.Runtime.InteropServices;
9 using System.Text; 12 using System.Text;
13 using System.Threading.Tasks;
14 using Microsoft.Win32.SafeHandles;
10 15
11 public abstract class ExternalExecutable 16 public abstract class ExternalExecutable
12 { 17 {
@@ -19,6 +24,125 @@ namespace WixBuildTools.TestSupport
19 24
20 protected ExternalExecutableResult Run(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null) 25 protected ExternalExecutableResult Run(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null)
21 { 26 {
27 // https://github.com/dotnet/runtime/issues/58492
28 // Process.Start doesn't currently support starting a process with a long path,
29 // but the way to support long paths doesn't support searching for the executable if it was a relative path.
30 // Avoid the managed way of doing this even if the target isn't a long path to help verify that the native way works.
31 if (!Path.IsPathRooted(this.exePath))
32 {
33 return this.RunManaged(args, mergeErrorIntoOutput, workingDirectory);
34 }
35
36 // https://web.archive.org/web/20150331190801/https://support.microsoft.com/en-us/kb/190351
37 var commandLine = $"\"{this.exePath}\" {args}";
38 var currentDirectory = workingDirectory ?? Path.GetDirectoryName(this.exePath);
39 if (String.IsNullOrEmpty(currentDirectory))
40 {
41 currentDirectory = null;
42 }
43 var processInfo = new PROCESS_INFORMATION();
44 var startInfo = new STARTUPINFOW
45 {
46 cb = Marshal.SizeOf(typeof(STARTUPINFOW)),
47 dwFlags = StartupInfoFlags.STARTF_FORCEOFFFEEDBACK | StartupInfoFlags.STARTF_USESTDHANDLES,
48 hStdInput = GetStdHandle(StdHandleType.STD_INPUT_HANDLE),
49 };
50 SafeFileHandle hStdOutputParent = null;
51 SafeFileHandle hStdErrorParent = null;
52
53 try
54 {
55 CreatePipeForProcess(out hStdOutputParent, out startInfo.hStdOutput);
56
57 if (!mergeErrorIntoOutput)
58 {
59 CreatePipeForProcess(out hStdErrorParent, out startInfo.hStdError);
60 }
61 else
62 {
63 if (!DuplicateHandle(GetCurrentProcess(), startInfo.hStdOutput, GetCurrentProcess(), out startInfo.hStdError, 0, true, DuplicateHandleOptions.DUPLICATE_SAME_ACCESS))
64 {
65 throw new Win32Exception();
66 }
67 }
68
69 if (!CreateProcessW(this.exePath, commandLine, IntPtr.Zero, IntPtr.Zero, true, CreateProcessFlags.CREATE_NO_WINDOW, IntPtr.Zero,
70 currentDirectory, ref startInfo, ref processInfo))
71 {
72 throw new Win32Exception();
73 }
74
75 startInfo.Dispose();
76
77 return GetResultFromNative(mergeErrorIntoOutput, hStdOutputParent, hStdErrorParent, processInfo.hProcess, this.exePath, args);
78 }
79 finally
80 {
81 hStdErrorParent?.Dispose();
82 hStdOutputParent?.Dispose();
83
84 startInfo.Dispose();
85 processInfo.Dispose();
86 }
87 }
88
89 private static ExternalExecutableResult GetResultFromNative(bool mergeErrorIntoOutput, SafeFileHandle hStdOutputParent, SafeFileHandle hStdErrorParent, IntPtr hProcess, string fileName, string args)
90 {
91 using (var outputStream = new StreamReader(new FileStream(hStdOutputParent, FileAccess.Read)))
92 using (var errorStream = mergeErrorIntoOutput ? null : new StreamReader(new FileStream(hStdErrorParent, FileAccess.Read)))
93 {
94 var outputTask = Task.Run(() => ReadProcessStreamLines(outputStream));
95 var errorTask = Task.Run(() => ReadProcessStreamLines(errorStream));
96
97 while (!outputTask.Wait(100) || !errorTask.Wait(100)) { Task.Yield(); }
98 var standardOutput = outputTask.Result;
99 var standardError = errorTask.Result;
100
101 if (WaitForSingleObject(hProcess, -1) != 0)
102 {
103 throw new Win32Exception();
104 }
105
106 if (!GetExitCodeProcess(hProcess, out var exitCode))
107 {
108 throw new Win32Exception();
109 }
110
111 return new ExternalExecutableResult
112 {
113 ExitCode = exitCode,
114 StandardError = standardError,
115 StandardOutput = standardOutput,
116 FileName = fileName,
117 Arguments = args,
118 };
119 }
120 }
121
122 private static string[] ReadProcessStreamLines(StreamReader streamReader)
123 {
124 if (streamReader == null)
125 {
126 return null;
127 }
128
129 var lines = new List<string>();
130 while (true)
131 {
132 var line = streamReader.ReadLine();
133 if (line == null)
134 {
135 break;
136 }
137
138 lines.Add(line);
139 }
140
141 return lines.ToArray();
142 }
143
144 protected ExternalExecutableResult RunManaged(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null)
145 {
22 var startInfo = new ProcessStartInfo(this.exePath, args) 146 var startInfo = new ProcessStartInfo(this.exePath, args)
23 { 147 {
24 CreateNoWindow = true, 148 CreateNoWindow = true,
@@ -48,7 +172,8 @@ namespace WixBuildTools.TestSupport
48 ExitCode = process.ExitCode, 172 ExitCode = process.ExitCode,
49 StandardError = mergeErrorIntoOutput ? null : standardError.ToArray(), 173 StandardError = mergeErrorIntoOutput ? null : standardError.ToArray(),
50 StandardOutput = standardOutput.ToArray(), 174 StandardOutput = standardOutput.ToArray(),
51 StartInfo = startInfo, 175 FileName = this.exePath,
176 Arguments = args,
52 }; 177 };
53 } 178 }
54 } 179 }
@@ -84,5 +209,166 @@ namespace WixBuildTools.TestSupport
84 209
85 return sb.ToString(); 210 return sb.ToString();
86 } 211 }
212
213 private static void CreatePipeForProcess(out SafeFileHandle hReadPipe, out IntPtr hWritePipe)
214 {
215 var securityAttributes = new SECURITY_ATTRIBUTES
216 {
217 nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES)),
218 bInheritHandle = true,
219 };
220
221 if (!CreatePipe(out var hReadTemp, out hWritePipe, ref securityAttributes, 0))
222 {
223 throw new Win32Exception();
224 }
225
226 // Only the handle passed to the process should be inheritable, so have to duplicate the other handle to get an uninheritable one.
227 if (!DuplicateHandle(GetCurrentProcess(), hReadTemp, GetCurrentProcess(), out var hReadPipePtr, 0, false, DuplicateHandleOptions.DUPLICATE_CLOSE_SOURCE | DuplicateHandleOptions.DUPLICATE_SAME_ACCESS))
228 {
229 throw new Win32Exception();
230 }
231
232 hReadPipe = new SafeFileHandle(hReadPipePtr, true);
233 }
234
235 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
236 private extern static IntPtr GetStdHandle(StdHandleType nStdHandle);
237
238 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
239 [return: MarshalAs(UnmanagedType.Bool)]
240 private extern static bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);
241
242 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
243 [return: MarshalAs(UnmanagedType.Bool)]
244 private extern static bool CreateProcessW(
245 string lpApplicationName,
246 string lpCommandLine,
247 IntPtr lpProcessAttributes,
248 IntPtr lpThreadAttributes,
249 [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles,
250 CreateProcessFlags dwCreationFlags,
251 IntPtr lpEnvironment,
252 string lpCurrentDirectory,
253 ref STARTUPINFOW lpStartupInfo,
254 ref PROCESS_INFORMATION lpProcessInformation);
255
256 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
257 private extern static IntPtr GetCurrentProcess();
258
259 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
260 [return: MarshalAs(UnmanagedType.Bool)]
261 private extern static bool GetExitCodeProcess(IntPtr hHandle, out int lpExitCode);
262
263 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
264 private extern static int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
265
266 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
267 [return: MarshalAs(UnmanagedType.Bool)]
268 private extern static bool CloseHandle(IntPtr hObject);
269
270 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
271 [return: MarshalAs(UnmanagedType.Bool)]
272 private extern static bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, DuplicateHandleOptions dwOptions);
273
274 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
275 private struct SECURITY_ATTRIBUTES
276 {
277 public int nLength;
278 public IntPtr lpSecurityDescriptor;
279 [MarshalAs(UnmanagedType.Bool)]
280 public bool bInheritHandle;
281 }
282
283 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
284 private struct STARTUPINFOW
285 {
286 public int cb;
287 public string lpReserved;
288 public string lpDesktop;
289 public string lpTitle;
290 public int dwX;
291 public int dwY;
292 public int dwXSize;
293 public int dwYSize;
294 public int dwXCountChars;
295 public int dwYCountChars;
296 public int dwFillAttribute;
297 public StartupInfoFlags dwFlags;
298 public short wShowWindow;
299 public short cbReserved2;
300 public IntPtr lpReserved2;
301 public IntPtr hStdInput;
302 public IntPtr hStdOutput;
303 public IntPtr hStdError;
304
305 public void Dispose()
306 {
307 // This makes assumptions based on how it's used above.
308 if (this.hStdError != IntPtr.Zero)
309 {
310 CloseHandle(this.hStdError);
311 this.hStdError = IntPtr.Zero;
312 }
313
314 if (this.hStdOutput != IntPtr.Zero)
315 {
316 CloseHandle(this.hStdOutput);
317 this.hStdOutput = IntPtr.Zero;
318 }
319 }
320 }
321
322 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
323 private struct PROCESS_INFORMATION
324 {
325 public IntPtr hProcess;
326 public IntPtr hThread;
327 public int dwProcessId;
328 public int dwThreadId;
329
330 public void Dispose()
331 {
332 if (this.hProcess != IntPtr.Zero)
333 {
334 CloseHandle(this.hProcess);
335 this.hProcess = IntPtr.Zero;
336 }
337
338 if (this.hThread != IntPtr.Zero)
339 {
340 CloseHandle(this.hThread);
341 this.hThread = IntPtr.Zero;
342 }
343 }
344 }
345
346 private enum StdHandleType
347 {
348 STD_INPUT_HANDLE = -10,
349 STD_OUTPUT_HANDLE = -11,
350 STD_ERROR_HANDLE = -12,
351 }
352
353 [Flags]
354 private enum CreateProcessFlags
355 {
356 None = 0x0,
357 CREATE_NO_WINDOW = 0x08000000,
358 }
359
360 [Flags]
361 private enum StartupInfoFlags
362 {
363 None = 0x0,
364 STARTF_FORCEOFFFEEDBACK = 0x80,
365 STARTF_USESTDHANDLES = 0x100,
366 }
367
368 private enum DuplicateHandleOptions
369 {
370 DUPLICATE_CLOSE_SOURCE = 1,
371 DUPLICATE_SAME_ACCESS = 2,
372 }
87 } 373 }
88} 374}
diff --git a/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs b/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs
index 19b5183b..950ee4bd 100644
--- a/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs
+++ b/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs
@@ -12,6 +12,8 @@ namespace WixBuildTools.TestSupport
12 12
13 public string[] StandardOutput { get; set; } 13 public string[] StandardOutput { get; set; }
14 14
15 public ProcessStartInfo StartInfo { get; set; } 15 public string FileName { get; set; }
16
17 public string Arguments { get; set; }
16 } 18 }
17} 19}
diff --git a/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs b/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs
index 8d670bf0..15b7631d 100644
--- a/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs
+++ b/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs
@@ -33,7 +33,7 @@ namespace WixBuildTools.TestSupport
33 RobocopyFolder(sourceDirectoryPath, this.BaseFolder); 33 RobocopyFolder(sourceDirectoryPath, this.BaseFolder);
34 } 34 }
35 35
36 private static ExternalExecutableResult RobocopyFolder(string sourceFolderPath, string destinationFolderPath) 36 public static ExternalExecutableResult RobocopyFolder(string sourceFolderPath, string destinationFolderPath)
37 { 37 {
38 var args = $"\"{sourceFolderPath}\" \"{destinationFolderPath}\" /E /R:1 /W:1"; 38 var args = $"\"{sourceFolderPath}\" \"{destinationFolderPath}\" /E /R:1 /W:1";
39 return RobocopyRunner.Execute(args); 39 return RobocopyRunner.Execute(args);