446 lines
18 KiB
C#
446 lines
18 KiB
C#
using Microsoft.Win32.SafeHandles;
|
|
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.ConstrainedExecution;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace Ansible.Process
|
|
{
|
|
internal class NativeHelpers
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public class SECURITY_ATTRIBUTES
|
|
{
|
|
public UInt32 nLength;
|
|
public IntPtr lpSecurityDescriptor;
|
|
public bool bInheritHandle = false;
|
|
public SECURITY_ATTRIBUTES()
|
|
{
|
|
nLength = (UInt32)Marshal.SizeOf(this);
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public class STARTUPINFO
|
|
{
|
|
public UInt32 cb;
|
|
public IntPtr lpReserved;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpTitle;
|
|
public UInt32 dwX;
|
|
public UInt32 dwY;
|
|
public UInt32 dwXSize;
|
|
public UInt32 dwYSize;
|
|
public UInt32 dwXCountChars;
|
|
public UInt32 dwYCountChars;
|
|
public UInt32 dwFillAttribute;
|
|
public StartupInfoFlags dwFlags;
|
|
public UInt16 wShowWindow;
|
|
public UInt16 cbReserved2;
|
|
public IntPtr lpReserved2;
|
|
public SafeFileHandle hStdInput;
|
|
public SafeFileHandle hStdOutput;
|
|
public SafeFileHandle hStdError;
|
|
public STARTUPINFO()
|
|
{
|
|
cb = (UInt32)Marshal.SizeOf(this);
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public class STARTUPINFOEX
|
|
{
|
|
public STARTUPINFO startupInfo;
|
|
public IntPtr lpAttributeList;
|
|
public STARTUPINFOEX()
|
|
{
|
|
startupInfo = new STARTUPINFO();
|
|
startupInfo.cb = (UInt32)Marshal.SizeOf(this);
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct PROCESS_INFORMATION
|
|
{
|
|
public IntPtr hProcess;
|
|
public IntPtr hThread;
|
|
public int dwProcessId;
|
|
public int dwThreadId;
|
|
}
|
|
|
|
[Flags]
|
|
public enum ProcessCreationFlags : uint
|
|
{
|
|
CREATE_NEW_CONSOLE = 0x00000010,
|
|
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
|
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
|
}
|
|
|
|
[Flags]
|
|
public enum StartupInfoFlags : uint
|
|
{
|
|
USESTDHANDLES = 0x00000100
|
|
}
|
|
|
|
[Flags]
|
|
public enum HandleFlags : uint
|
|
{
|
|
None = 0,
|
|
INHERIT = 1
|
|
}
|
|
}
|
|
|
|
internal class NativeMethods
|
|
{
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool AllocConsole();
|
|
|
|
[DllImport("shell32.dll", SetLastError = true)]
|
|
public static extern SafeMemoryBuffer CommandLineToArgvW(
|
|
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
|
|
out int pNumArgs);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool CreatePipe(
|
|
out SafeFileHandle hReadPipe,
|
|
out SafeFileHandle hWritePipe,
|
|
NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
|
|
UInt32 nSize);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool 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);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool FreeConsole();
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern IntPtr GetConsoleWindow();
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool GetExitCodeProcess(
|
|
SafeWaitHandle hProcess,
|
|
out UInt32 lpExitCode);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern uint 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);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool SetConsoleCP(
|
|
UInt32 wCodePageID);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool SetConsoleOutputCP(
|
|
UInt32 wCodePageID);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern bool SetHandleInformation(
|
|
SafeFileHandle hObject,
|
|
NativeHelpers.HandleFlags dwMask,
|
|
NativeHelpers.HandleFlags dwFlags);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
public static extern UInt32 WaitForSingleObject(
|
|
SafeWaitHandle hHandle,
|
|
UInt32 dwMilliseconds);
|
|
}
|
|
|
|
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
|
{
|
|
public SafeMemoryBuffer() : base(true) { }
|
|
public SafeMemoryBuffer(int cb) : base(true)
|
|
{
|
|
base.SetHandle(Marshal.AllocHGlobal(cb));
|
|
}
|
|
public SafeMemoryBuffer(IntPtr handle) : base(true)
|
|
{
|
|
base.SetHandle(handle);
|
|
}
|
|
|
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
|
protected override bool ReleaseHandle()
|
|
{
|
|
Marshal.FreeHGlobal(handle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public class Win32Exception : System.ComponentModel.Win32Exception
|
|
{
|
|
private string _msg;
|
|
|
|
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
|
public Win32Exception(int errorCode, string message) : base(errorCode)
|
|
{
|
|
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
|
}
|
|
|
|
public override string Message { get { return _msg; } }
|
|
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
|
}
|
|
|
|
public class Result
|
|
{
|
|
public string StandardOut { get; internal set; }
|
|
public string StandardError { get; internal set; }
|
|
public uint ExitCode { get; internal set; }
|
|
}
|
|
|
|
public class ProcessUtil
|
|
{
|
|
/// <summary>
|
|
/// Parses a command line string into an argv array according to the Windows rules
|
|
/// </summary>
|
|
/// <param name="lpCommandLine">The command line to parse</param>
|
|
/// <returns>An array of arguments interpreted by Windows</returns>
|
|
public static string[] ParseCommandLine(string lpCommandLine)
|
|
{
|
|
int numArgs;
|
|
using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs))
|
|
{
|
|
if (buf.IsInvalid)
|
|
throw new Win32Exception("Error parsing command line");
|
|
IntPtr[] strptrs = new IntPtr[numArgs];
|
|
Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs);
|
|
return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches the path for the executable specified. Will throw a Win32Exception if the file is not found.
|
|
/// </summary>
|
|
/// <param name="lpFileName">The executable to search for</param>
|
|
/// <returns>The full path of the executable to search for</returns>
|
|
public static string SearchPath(string lpFileName)
|
|
{
|
|
StringBuilder sbOut = new StringBuilder(0);
|
|
IntPtr filePartOut = IntPtr.Zero;
|
|
UInt32 res = NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut);
|
|
if (res == 0)
|
|
{
|
|
int lastErr = Marshal.GetLastWin32Error();
|
|
if (lastErr == 2) // ERROR_FILE_NOT_FOUND
|
|
throw new FileNotFoundException(String.Format("Could not find file '{0}'.", lpFileName));
|
|
else
|
|
throw new Win32Exception(String.Format("SearchPathW({0}) failed to get buffer length", lpFileName));
|
|
}
|
|
|
|
sbOut.EnsureCapacity((int)res);
|
|
if (NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut) == 0)
|
|
throw new Win32Exception(String.Format("SearchPathW({0}) failed", lpFileName));
|
|
|
|
return sbOut.ToString();
|
|
}
|
|
|
|
public static Result CreateProcess(string command)
|
|
{
|
|
return CreateProcess(null, command, null, null, String.Empty);
|
|
}
|
|
|
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
|
IDictionary environment)
|
|
{
|
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, String.Empty);
|
|
}
|
|
|
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
|
IDictionary environment, string stdin)
|
|
{
|
|
byte[] stdinBytes;
|
|
if (String.IsNullOrEmpty(stdin))
|
|
stdinBytes = new byte[0];
|
|
else
|
|
{
|
|
if (!stdin.EndsWith(Environment.NewLine))
|
|
stdin += Environment.NewLine;
|
|
stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
|
|
}
|
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a process based on the CreateProcess API call.
|
|
/// </summary>
|
|
/// <param name="lpApplicationName">The name of the executable or batch file to execute</param>
|
|
/// <param name="lpCommandLine">The command line to execute, typically this includes lpApplication as the first argument</param>
|
|
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
|
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
|
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
|
/// <returns>Result object that contains the command output and return code</returns>
|
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
|
IDictionary environment, byte[] stdin)
|
|
{
|
|
NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
|
|
NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
|
|
NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
|
|
NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
|
|
si.startupInfo.dwFlags = NativeHelpers.StartupInfoFlags.USESTDHANDLES;
|
|
|
|
SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite;
|
|
CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead,
|
|
out stdinWrite);
|
|
FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write);
|
|
|
|
// $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't
|
|
// make sense for these parameters
|
|
if (lpApplicationName == "")
|
|
lpApplicationName = null;
|
|
|
|
if (lpCurrentDirectory == "")
|
|
lpCurrentDirectory = null;
|
|
|
|
using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment))
|
|
{
|
|
// Create console with utf-8 CP if no existing console is present
|
|
bool isConsole = false;
|
|
if (NativeMethods.GetConsoleWindow() == IntPtr.Zero)
|
|
{
|
|
isConsole = NativeMethods.AllocConsole();
|
|
|
|
// Set console input/output codepage to UTF-8
|
|
NativeMethods.SetConsoleCP(65001);
|
|
NativeMethods.SetConsoleOutputCP(65001);
|
|
}
|
|
|
|
try
|
|
{
|
|
StringBuilder commandLine = new StringBuilder(lpCommandLine);
|
|
if (!NativeMethods.CreateProcessW(lpApplicationName, commandLine, IntPtr.Zero, IntPtr.Zero,
|
|
true, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi))
|
|
{
|
|
throw new Win32Exception("CreateProcessW() failed");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (isConsole)
|
|
NativeMethods.FreeConsole();
|
|
}
|
|
}
|
|
|
|
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess);
|
|
}
|
|
|
|
internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
|
|
out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite,
|
|
out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)
|
|
{
|
|
NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES();
|
|
pipesec.bInheritHandle = true;
|
|
|
|
if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0))
|
|
throw new Win32Exception("STDOUT pipe setup failed");
|
|
if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
|
throw new Win32Exception("STDOUT pipe handle setup failed");
|
|
|
|
if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0))
|
|
throw new Win32Exception("STDERR pipe setup failed");
|
|
if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
|
throw new Win32Exception("STDERR pipe handle setup failed");
|
|
|
|
if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0))
|
|
throw new Win32Exception("STDIN pipe setup failed");
|
|
if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0))
|
|
throw new Win32Exception("STDIN pipe handle setup failed");
|
|
|
|
si.startupInfo.hStdOutput = stdoutWrite;
|
|
si.startupInfo.hStdError = stderrWrite;
|
|
si.startupInfo.hStdInput = stdinRead;
|
|
}
|
|
|
|
internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment)
|
|
{
|
|
IntPtr lpEnvironment = IntPtr.Zero;
|
|
if (environment != null && environment.Count > 0)
|
|
{
|
|
StringBuilder environmentString = new StringBuilder();
|
|
foreach (DictionaryEntry kv in environment)
|
|
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
|
|
environmentString.Append('\0');
|
|
|
|
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
|
|
}
|
|
return new SafeMemoryBuffer(lpEnvironment);
|
|
}
|
|
|
|
internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
|
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess)
|
|
{
|
|
// Setup the output buffers and get stdout/stderr
|
|
UTF8Encoding utf8Encoding = new UTF8Encoding(false);
|
|
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
|
StreamReader stdout = new StreamReader(stdoutFS, utf8Encoding, true, 4096);
|
|
stdoutWrite.Close();
|
|
|
|
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
|
StreamReader stderr = new StreamReader(stderrFS, utf8Encoding, true, 4096);
|
|
stderrWrite.Close();
|
|
|
|
stdinStream.Write(stdin, 0, stdin.Length);
|
|
stdinStream.Close();
|
|
|
|
string stdoutStr, stderrStr = null;
|
|
GetProcessOutput(stdout, stderr, out stdoutStr, out stderrStr);
|
|
UInt32 rc = GetProcessExitCode(hProcess);
|
|
|
|
return new Result
|
|
{
|
|
StandardOut = stdoutStr,
|
|
StandardError = stderrStr,
|
|
ExitCode = rc
|
|
};
|
|
}
|
|
|
|
internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
|
{
|
|
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
string so = null, se = null;
|
|
ThreadPool.QueueUserWorkItem((s) =>
|
|
{
|
|
so = stdoutStream.ReadToEnd();
|
|
sowait.Set();
|
|
});
|
|
ThreadPool.QueueUserWorkItem((s) =>
|
|
{
|
|
se = stderrStream.ReadToEnd();
|
|
sewait.Set();
|
|
});
|
|
foreach (var wh in new WaitHandle[] { sowait, sewait })
|
|
wh.WaitOne();
|
|
stdout = so;
|
|
stderr = se;
|
|
}
|
|
|
|
internal static UInt32 GetProcessExitCode(IntPtr processHandle)
|
|
{
|
|
SafeWaitHandle hProcess = new SafeWaitHandle(processHandle, true);
|
|
NativeMethods.WaitForSingleObject(hProcess, 0xFFFFFFFF);
|
|
|
|
UInt32 exitCode;
|
|
if (!NativeMethods.GetExitCodeProcess(hProcess, out exitCode))
|
|
throw new Win32Exception("GetExitCodeProcess() failed");
|
|
return exitCode;
|
|
}
|
|
}
|
|
}
|