1 using Microsoft.Win32.SafeHandles;
2 using System;
3 using System.Collections;
4 using System.IO;
5 using System.Linq;
6 using System.Runtime.ConstrainedExecution;
7 using System.Runtime.InteropServices;
8 using System.Text;
9 using System.Threading;
10 
11 namespace Ansible.Process
12 {
13     internal class NativeHelpers
14     {
15         [StructLayout(LayoutKind.Sequential)]
16         public class SECURITY_ATTRIBUTES
17         {
18             public UInt32 nLength;
19             public IntPtr lpSecurityDescriptor;
20             public bool bInheritHandle = false;
SECURITY_ATTRIBUTES()21             public SECURITY_ATTRIBUTES()
22             {
23                 nLength = (UInt32)Marshal.SizeOf(this);
24             }
25         }
26 
27         [StructLayout(LayoutKind.Sequential)]
28         public class STARTUPINFO
29         {
30             public UInt32 cb;
31             public IntPtr lpReserved;
32             [MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop;
33             [MarshalAs(UnmanagedType.LPWStr)] public string lpTitle;
34             public UInt32 dwX;
35             public UInt32 dwY;
36             public UInt32 dwXSize;
37             public UInt32 dwYSize;
38             public UInt32 dwXCountChars;
39             public UInt32 dwYCountChars;
40             public UInt32 dwFillAttribute;
41             public StartupInfoFlags dwFlags;
42             public UInt16 wShowWindow;
43             public UInt16 cbReserved2;
44             public IntPtr lpReserved2;
45             public SafeFileHandle hStdInput;
46             public SafeFileHandle hStdOutput;
47             public SafeFileHandle hStdError;
STARTUPINFO()48             public STARTUPINFO()
49             {
50                 cb = (UInt32)Marshal.SizeOf(this);
51             }
52         }
53 
54         [StructLayout(LayoutKind.Sequential)]
55         public class STARTUPINFOEX
56         {
57             public STARTUPINFO startupInfo;
58             public IntPtr lpAttributeList;
STARTUPINFOEX()59             public STARTUPINFOEX()
60             {
61                 startupInfo = new STARTUPINFO();
62                 startupInfo.cb = (UInt32)Marshal.SizeOf(this);
63             }
64         }
65 
66         [StructLayout(LayoutKind.Sequential)]
67         public struct PROCESS_INFORMATION
68         {
69             public IntPtr hProcess;
70             public IntPtr hThread;
71             public int dwProcessId;
72             public int dwThreadId;
73         }
74 
75         [Flags]
76         public enum ProcessCreationFlags : uint
77         {
78             CREATE_NEW_CONSOLE = 0x00000010,
79             CREATE_UNICODE_ENVIRONMENT = 0x00000400,
80             EXTENDED_STARTUPINFO_PRESENT = 0x00080000
81         }
82 
83         [Flags]
84         public enum StartupInfoFlags : uint
85         {
86             USESTDHANDLES = 0x00000100
87         }
88 
89         [Flags]
90         public enum HandleFlags : uint
91         {
92             None = 0,
93             INHERIT = 1
94         }
95     }
96 
97     internal class NativeMethods
98     {
99         [DllImport("kernel32.dll", SetLastError = true)]
AllocConsole()100         public static extern bool AllocConsole();
101 
102         [DllImport("shell32.dll", SetLastError = true)]
CommandLineToArgvW( [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs)103         public static extern SafeMemoryBuffer CommandLineToArgvW(
104             [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
105             out int pNumArgs);
106 
107         [DllImport("kernel32.dll", SetLastError = true)]
CreatePipe( out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes, UInt32 nSize)108         public static extern bool CreatePipe(
109             out SafeFileHandle hReadPipe,
110             out SafeFileHandle hWritePipe,
111             NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
112             UInt32 nSize);
113 
114         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
CreateProcessW( [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, NativeHelpers.ProcessCreationFlags dwCreationFlags, SafeMemoryBuffer lpEnvironment, [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory, NativeHelpers.STARTUPINFOEX lpStartupInfo, out NativeHelpers.PROCESS_INFORMATION lpProcessInformation)115         public static extern bool CreateProcessW(
116             [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
117             StringBuilder lpCommandLine,
118             IntPtr lpProcessAttributes,
119             IntPtr lpThreadAttributes,
120             bool bInheritHandles,
121             NativeHelpers.ProcessCreationFlags dwCreationFlags,
122             SafeMemoryBuffer lpEnvironment,
123             [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory,
124             NativeHelpers.STARTUPINFOEX lpStartupInfo,
125             out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
126 
127         [DllImport("kernel32.dll", SetLastError = true)]
FreeConsole()128         public static extern bool FreeConsole();
129 
130         [DllImport("kernel32.dll", SetLastError = true)]
GetConsoleWindow()131         public static extern IntPtr GetConsoleWindow();
132 
133         [DllImport("kernel32.dll", SetLastError = true)]
GetExitCodeProcess( SafeWaitHandle hProcess, out UInt32 lpExitCode)134         public static extern bool GetExitCodeProcess(
135             SafeWaitHandle hProcess,
136             out UInt32 lpExitCode);
137 
138         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
SearchPathW( [MarshalAs(UnmanagedType.LPWStr)] string lpPath, [MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [MarshalAs(UnmanagedType.LPWStr)] string lpExtension, UInt32 nBufferLength, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpBuffer, out IntPtr lpFilePart)139         public static extern uint SearchPathW(
140             [MarshalAs(UnmanagedType.LPWStr)] string lpPath,
141             [MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
142             [MarshalAs(UnmanagedType.LPWStr)] string lpExtension,
143             UInt32 nBufferLength,
144             [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpBuffer,
145             out IntPtr lpFilePart);
146 
147         [DllImport("kernel32.dll", SetLastError = true)]
SetConsoleCP( UInt32 wCodePageID)148         public static extern bool SetConsoleCP(
149             UInt32 wCodePageID);
150 
151         [DllImport("kernel32.dll", SetLastError = true)]
SetConsoleOutputCP( UInt32 wCodePageID)152         public static extern bool SetConsoleOutputCP(
153             UInt32 wCodePageID);
154 
155         [DllImport("kernel32.dll", SetLastError = true)]
SetHandleInformation( SafeFileHandle hObject, NativeHelpers.HandleFlags dwMask, NativeHelpers.HandleFlags dwFlags)156         public static extern bool SetHandleInformation(
157             SafeFileHandle hObject,
158             NativeHelpers.HandleFlags dwMask,
159             NativeHelpers.HandleFlags dwFlags);
160 
161         [DllImport("kernel32.dll")]
WaitForSingleObject( SafeWaitHandle hHandle, UInt32 dwMilliseconds)162         public static extern UInt32 WaitForSingleObject(
163             SafeWaitHandle hHandle,
164             UInt32 dwMilliseconds);
165     }
166 
167     internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
168     {
SafeMemoryBuffer()169         public SafeMemoryBuffer() : base(true) { }
SafeMemoryBuffer(int cb)170         public SafeMemoryBuffer(int cb) : base(true)
171         {
172             base.SetHandle(Marshal.AllocHGlobal(cb));
173         }
SafeMemoryBuffer(IntPtr handle)174         public SafeMemoryBuffer(IntPtr handle) : base(true)
175         {
176             base.SetHandle(handle);
177         }
178 
179         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
ReleaseHandle()180         protected override bool ReleaseHandle()
181         {
182             Marshal.FreeHGlobal(handle);
183             return true;
184         }
185     }
186 
187     public class Win32Exception : System.ComponentModel.Win32Exception
188     {
189         private string _msg;
190 
Win32Exception(string message)191         public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
Win32Exception(int errorCode, string message)192         public Win32Exception(int errorCode, string message) : base(errorCode)
193         {
194             _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
195         }
196 
197         public override string Message { get { return _msg; } }
operator Win32Exception(string message)198         public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
199     }
200 
201     public class Result
202     {
203         public string StandardOut { get; internal set; }
204         public string StandardError { get; internal set; }
205         public uint ExitCode { get; internal set; }
206     }
207 
208     public class ProcessUtil
209     {
210         /// <summary>
211         /// Parses a command line string into an argv array according to the Windows rules
212         /// </summary>
213         /// <param name="lpCommandLine">The command line to parse</param>
214         /// <returns>An array of arguments interpreted by Windows</returns>
ParseCommandLine(string lpCommandLine)215         public static string[] ParseCommandLine(string lpCommandLine)
216         {
217             int numArgs;
218             using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs))
219             {
220                 if (buf.IsInvalid)
221                     throw new Win32Exception("Error parsing command line");
222                 IntPtr[] strptrs = new IntPtr[numArgs];
223                 Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs);
224                 return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
225             }
226         }
227 
228         /// <summary>
229         /// Searches the path for the executable specified. Will throw a Win32Exception if the file is not found.
230         /// </summary>
231         /// <param name="lpFileName">The executable to search for</param>
232         /// <returns>The full path of the executable to search for</returns>
SearchPath(string lpFileName)233         public static string SearchPath(string lpFileName)
234         {
235             StringBuilder sbOut = new StringBuilder(0);
236             IntPtr filePartOut = IntPtr.Zero;
237             UInt32 res = NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut);
238             if (res == 0)
239             {
240                 int lastErr = Marshal.GetLastWin32Error();
241                 if (lastErr == 2)  // ERROR_FILE_NOT_FOUND
242                     throw new FileNotFoundException(String.Format("Could not find file '{0}'.", lpFileName));
243                 else
244                     throw new Win32Exception(String.Format("SearchPathW({0}) failed to get buffer length", lpFileName));
245             }
246 
247             sbOut.EnsureCapacity((int)res);
248             if (NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut) == 0)
249                 throw new Win32Exception(String.Format("SearchPathW({0}) failed", lpFileName));
250 
251             return sbOut.ToString();
252         }
253 
CreateProcess(string command)254         public static Result CreateProcess(string command)
255         {
256             return CreateProcess(null, command, null, null, String.Empty);
257         }
258 
CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment)259         public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
260             IDictionary environment)
261         {
262             return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, String.Empty);
263         }
264 
CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, string stdin)265         public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
266             IDictionary environment, string stdin)
267         {
268             byte[] stdinBytes;
269             if (String.IsNullOrEmpty(stdin))
270                 stdinBytes = new byte[0];
271             else
272             {
273                 if (!stdin.EndsWith(Environment.NewLine))
274                     stdin += Environment.NewLine;
275                 stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
276             }
277             return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes);
278         }
279 
280         /// <summary>
281         /// Creates a process based on the CreateProcess API call.
282         /// </summary>
283         /// <param name="lpApplicationName">The name of the executable or batch file to execute</param>
284         /// <param name="lpCommandLine">The command line to execute, typically this includes lpApplication as the first argument</param>
285         /// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
286         /// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
287         /// <param name="stdin">A byte array to send over the stdin pipe</param>
288         /// <returns>Result object that contains the command output and return code</returns>
CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, byte[] stdin)289         public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
290             IDictionary environment, byte[] stdin)
291         {
292             NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
293                 NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
294             NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
295             NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
296             si.startupInfo.dwFlags = NativeHelpers.StartupInfoFlags.USESTDHANDLES;
297 
298             SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite;
299             CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead,
300                 out stdinWrite);
301             FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write);
302 
303             // $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't
304             // make sense for these parameters
305             if (lpApplicationName == "")
306                 lpApplicationName = null;
307 
308             if (lpCurrentDirectory == "")
309                 lpCurrentDirectory = null;
310 
311             using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment))
312             {
313                 // Create console with utf-8 CP if no existing console is present
314                 bool isConsole = false;
315                 if (NativeMethods.GetConsoleWindow() == IntPtr.Zero)
316                 {
317                     isConsole = NativeMethods.AllocConsole();
318 
319                     // Set console input/output codepage to UTF-8
320                     NativeMethods.SetConsoleCP(65001);
321                     NativeMethods.SetConsoleOutputCP(65001);
322                 }
323 
324                 try
325                 {
326                     StringBuilder commandLine = new StringBuilder(lpCommandLine);
327                     if (!NativeMethods.CreateProcessW(lpApplicationName, commandLine, IntPtr.Zero, IntPtr.Zero,
328                         true, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi))
329                     {
330                         throw new Win32Exception("CreateProcessW() failed");
331                     }
332                 }
333                 finally
334                 {
335                     if (isConsole)
336                         NativeMethods.FreeConsole();
337                 }
338             }
339 
340             return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess);
341         }
342 
CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead, out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite, out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)343         internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
344             out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite,
345             out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)
346         {
347             NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES();
348             pipesec.bInheritHandle = true;
349 
350             if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0))
351                 throw new Win32Exception("STDOUT pipe setup failed");
352             if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0))
353                 throw new Win32Exception("STDOUT pipe handle setup failed");
354 
355             if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0))
356                 throw new Win32Exception("STDERR pipe setup failed");
357             if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0))
358                 throw new Win32Exception("STDERR pipe handle setup failed");
359 
360             if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0))
361                 throw new Win32Exception("STDIN pipe setup failed");
362             if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0))
363                 throw new Win32Exception("STDIN pipe handle setup failed");
364 
365             si.startupInfo.hStdOutput = stdoutWrite;
366             si.startupInfo.hStdError = stderrWrite;
367             si.startupInfo.hStdInput = stdinRead;
368         }
369 
CreateEnvironmentPointer(IDictionary environment)370         internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment)
371         {
372             IntPtr lpEnvironment = IntPtr.Zero;
373             if (environment != null && environment.Count > 0)
374             {
375                 StringBuilder environmentString = new StringBuilder();
376                 foreach (DictionaryEntry kv in environment)
377                     environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
378                 environmentString.Append('\0');
379 
380                 lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
381             }
382             return new SafeMemoryBuffer(lpEnvironment);
383         }
384 
WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead, SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess)385         internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
386             SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess)
387         {
388             // Setup the output buffers and get stdout/stderr
389             UTF8Encoding utf8Encoding = new UTF8Encoding(false);
390             FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
391             StreamReader stdout = new StreamReader(stdoutFS, utf8Encoding, true, 4096);
392             stdoutWrite.Close();
393 
394             FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
395             StreamReader stderr = new StreamReader(stderrFS, utf8Encoding, true, 4096);
396             stderrWrite.Close();
397 
398             stdinStream.Write(stdin, 0, stdin.Length);
399             stdinStream.Close();
400 
401             string stdoutStr, stderrStr = null;
402             GetProcessOutput(stdout, stderr, out stdoutStr, out stderrStr);
403             UInt32 rc = GetProcessExitCode(hProcess);
404 
405             return new Result
406             {
407                 StandardOut = stdoutStr,
408                 StandardError = stderrStr,
409                 ExitCode = rc
410             };
411         }
412 
GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)413         internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
414         {
415             var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
416             var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
417             string so = null, se = null;
418             ThreadPool.QueueUserWorkItem((s) =>
419             {
420                 so = stdoutStream.ReadToEnd();
421                 sowait.Set();
422             });
423             ThreadPool.QueueUserWorkItem((s) =>
424             {
425                 se = stderrStream.ReadToEnd();
426                 sewait.Set();
427             });
428             foreach (var wh in new WaitHandle[] { sowait, sewait })
429                 wh.WaitOne();
430             stdout = so;
431             stderr = se;
432         }
433 
GetProcessExitCode(IntPtr processHandle)434         internal static UInt32 GetProcessExitCode(IntPtr processHandle)
435         {
436             SafeWaitHandle hProcess = new SafeWaitHandle(processHandle, true);
437             NativeMethods.WaitForSingleObject(hProcess, 0xFFFFFFFF);
438 
439             UInt32 exitCode;
440             if (!NativeMethods.GetExitCodeProcess(hProcess, out exitCode))
441                 throw new Win32Exception("GetExitCodeProcess() failed");
442             return exitCode;
443         }
444     }
445 }
446