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