win become: refactor and add support for passwordless become (#48082)

* win become: refactor and add support for passwordless become

* make tests more stable

* fix up dep message for Load-CommandUtils

* Add further check for System impersonation token

* re-add support for become with accounts that have no password

* doc fixes and slight code improvements

* fix doc sanity issue
This commit is contained in:
Jordan Borean 2018-12-13 11:15:25 +10:00 committed by Matt Davis
commit 190d1ed7f1
13 changed files with 2586 additions and 1105 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,445 @@
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;
}
}
}

View file

@ -1,393 +1,43 @@
# Copyright (c) 2017 Ansible Project
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
$process_util = @"
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace Ansible
{
[StructLayout(LayoutKind.Sequential)]
public class SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle = false;
public SECURITY_ATTRIBUTES()
{
nLength = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
public class STARTUPINFO
{
public Int32 cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public SafeFileHandle hStdInput;
public SafeFileHandle hStdOutput;
public SafeFileHandle hStdError;
public STARTUPINFO()
{
cb = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
public class STARTUPINFOEX
{
public STARTUPINFO startupInfo;
public IntPtr lpAttributeList;
public STARTUPINFOEX()
{
startupInfo = new STARTUPINFO();
startupInfo.cb = 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 StartupInfoFlags : uint
{
USESTDHANDLES = 0x00000100
}
public enum HandleFlags : uint
{
None = 0,
INHERIT = 1
}
class NativeWaitHandle : WaitHandle
{
public NativeWaitHandle(IntPtr handle)
{
this.SafeWaitHandle = new SafeWaitHandle(handle, false);
}
}
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 CommandUtil
{
private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400;
private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
public static extern bool CreateProcess(
[MarshalAs(UnmanagedType.LPWStr)]
string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
[MarshalAs(UnmanagedType.LPWStr)]
string lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll")]
public static extern bool CreatePipe(
out SafeFileHandle hReadPipe,
out SafeFileHandle hWritePipe,
SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetHandleInformation(
SafeFileHandle hObject,
HandleFlags dwMask,
int dwFlags);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess(
IntPtr hProcess,
out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint SearchPath(
string lpPath,
string lpFileName,
string lpExtension,
int nBufferLength,
[MarshalAs (UnmanagedType.LPTStr)]
StringBuilder lpBuffer,
out IntPtr lpFilePart);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCP(
UInt32 wCodePageID);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleOutputCP(
UInt32 wCodePageID);
[DllImport("shell32.dll", SetLastError = true)]
static extern IntPtr CommandLineToArgvW(
[MarshalAs(UnmanagedType.LPWStr)]
string lpCmdLine,
out int pNumArgs);
public static string[] ParseCommandLine(string lpCommandLine)
{
int numArgs;
IntPtr ret = CommandLineToArgvW(lpCommandLine, out numArgs);
if (ret == IntPtr.Zero)
throw new Win32Exception("Error parsing command line");
IntPtr[] strptrs = new IntPtr[numArgs];
Marshal.Copy(ret, strptrs, 0, numArgs);
string[] cmdlineParts = strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
Marshal.FreeHGlobal(ret);
return cmdlineParts;
}
public static string SearchPath(string lpFileName)
{
StringBuilder sbOut = new StringBuilder(1024);
IntPtr filePartOut;
if (SearchPath(null, lpFileName, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
throw new FileNotFoundException(String.Format("Could not locate the following executable {0}", lpFileName));
return sbOut.ToString();
}
public class CommandResult
{
public string StandardOut { get; internal set; }
public string StandardError { get; internal set; }
public uint ExitCode { get; internal set; }
}
public static CommandResult RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, IDictionary environment)
{
UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
STARTUPINFOEX si = new STARTUPINFOEX();
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES();
pipesec.bInheritHandle = true;
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write;
if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
throw new Win32Exception("STDOUT pipe setup failed");
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
throw new Win32Exception("STDOUT pipe handle setup failed");
if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0))
throw new Win32Exception("STDERR pipe setup failed");
if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0))
throw new Win32Exception("STDERR pipe handle setup failed");
if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0))
throw new Win32Exception("STDIN pipe setup failed");
if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0))
throw new Win32Exception("STDIN pipe handle setup failed");
si.startupInfo.hStdOutput = stdout_write;
si.startupInfo.hStdError = stderr_write;
si.startupInfo.hStdInput = stdin_read;
// Setup the stdin buffer
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768);
StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
// If lpCurrentDirectory is set to null in PS it will be an empty
// string here, we need to convert it
if (lpCurrentDirectory == "")
lpCurrentDirectory = null;
StringBuilder environmentString = null;
if (environment != null && environment.Count > 0)
{
environmentString = new StringBuilder();
foreach (DictionaryEntry kv in environment)
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
environmentString.Append('\0');
}
// Create the environment block if set
IntPtr lpEnvironment = IntPtr.Zero;
if (environmentString != null)
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
// Create console if needed to be inherited by child process
bool isConsole = false;
if (GetConsoleWindow() == IntPtr.Zero) {
isConsole = AllocConsole();
// Set console input/output codepage to UTF-8
SetConsoleCP(65001);
SetConsoleOutputCP(65001);
}
// Create new process and run
StringBuilder argument_string = new StringBuilder(lpCommandLine);
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
if (!CreateProcess(
lpApplicationName,
argument_string,
IntPtr.Zero,
IntPtr.Zero,
true,
startup_flags,
lpEnvironment,
lpCurrentDirectory,
si,
out pi))
{
throw new Win32Exception("Failed to create new process");
}
// Destroy console if we created it
if (isConsole) {
FreeConsole();
}
// Setup the output buffers and get stdout/stderr
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
stdout_write.Close();
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
stderr_write.Close();
stdin.WriteLine(stdinInput);
stdin.Close();
string stdout_str, stderr_str = null;
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
uint rc = GetProcessExitCode(pi.hProcess);
return new CommandResult
{
StandardOut = stdout_str,
StandardError = stderr_str,
ExitCode = rc
};
}
private 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;
}
private static uint GetProcessExitCode(IntPtr processHandle)
{
new NativeWaitHandle(processHandle).WaitOne();
uint exitCode;
if (!GetExitCodeProcess(processHandle, out exitCode))
throw new Win32Exception("Error getting process exit code");
return exitCode;
}
}
}
"@
$ErrorActionPreference = 'Stop'
#AnsibleRequires -CSharpUtil Ansible.Process
Function Load-CommandUtils {
# makes the following static functions available
# [Ansible.CommandUtil]::ParseCommandLine(string lpCommandLine)
# [Ansible.CommandUtil]::SearchPath(string lpFileName)
# [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock)
#
# there are also numerous P/Invoke methods that can be called if you are feeling adventurous
# FUTURE: find a better way to get the _ansible_remote_tmp variable
$original_tmp = $env:TMP
$remote_tmp = $original_tmp
$module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue
if ($module_params) {
if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) {
$remote_tmp = $module_params.Value["_ansible_remote_tmp"]
$remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
<#
.SYNOPSIS
No-op, as the C# types are automatically loaded.
#>
Param()
$msg = "Load-CommandUtils is deprecated and no longer needed, this cmdlet will be removed in a future version"
if ((Get-Command -Name Add-DeprecationWarning -ErrorAction SilentlyContinue) -and (Get-Variable -Name result -ErrorAction SilentlyContinue)) {
Add-DeprecationWarning -obj $result.Value -message $msg -version 2.12
} else {
$module = Get-Variable -Name module -ErrorAction SilentlyContinue
if ($null -ne $module -and $module.Value.GetType().FullName -eq "Ansible.Basic.AnsibleModule") {
$module.Value.Deprecate($msg, "2.12")
}
}
$env:TMP = $remote_tmp
Add-Type -TypeDefinition $process_util
$env:TMP = $original_tmp
}
Function Get-ExecutablePath($executable, $directory) {
# lpApplicationName requires the full path to a file, we need to find it
# ourselves.
Function Get-ExecutablePath {
<#
.SYNOPSIS
Get's the full path to an executable, will search the directory specified or ones in the PATH env var.
.PARAMETER executable
[String]The executable to seach for.
.PARAMETER directory
[String] If set, the directory to search in.
.OUTPUT
[String] The full path the executable specified.
#>
Param(
[String]$executable,
[String]$directory = $null
)
# we need to add .exe if it doesn't have an extension already
if (-not [System.IO.Path]::HasExtension($executable)) {
@ -404,21 +54,41 @@ Function Get-ExecutablePath($executable, $directory) {
if ($file -ne $null) {
$executable_path = $file.FullName
} else {
$executable_path = [Ansible.CommandUtil]::SearchPath($executable)
$executable_path = [Ansible.Process.ProcessUtil]::SearchPath($executable)
}
return $executable_path
}
Function Run-Command {
<#
.SYNOPSIS
Run a command with the CreateProcess API and return the stdout/stderr and return code.
.PARAMETER command
The full command, including the executable, to run.
.PARAMETER working_directory
The working directory to set on the new process, will default to the current working dir.
.PARAMETER stdin
A string to sent over the stdin pipe to the new process.
.PARAMETER environment
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
.OUTPUT
[Hashtable]
[String]executable - The full path to the executable that was run
[String]stdout - The stdout stream of the process
[String]stderr - The stderr stream of the process
[Int32]rc - The return code of the process
#>
Param(
[string]$command, # the full command to run including the executable
[string]$working_directory = $null, # the working directory to run under, will default to the current dir
[string]$stdin = $null, # a string to send to the stdin pipe when executing the command
[hashtable]$environment = @{} # a hashtable of environment values to run the command under, this will replace all the other environment variables with these
[string]$command,
[string]$working_directory = $null,
[string]$stdin = "",
[hashtable]$environment = @{}
)
# load the C# code we call in this function
Load-CommandUtils
# need to validate the working directory if it is set
if ($working_directory) {
@ -430,11 +100,11 @@ Function Run-Command {
# lpApplicationName needs to be the full path to an executable, we do this
# by getting the executable as the first arg and then getting the full path
$arguments = [Ansible.CommandUtil]::ParseCommandLine($command)
$arguments = [Ansible.Process.ProcessUtil]::ParseCommandLine($command)
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
# run the command and get the results
$command_result = [Ansible.CommandUtil]::RunCommand($executable, $command, $working_directory, $stdin, $environment)
$command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin)
return ,@{
executable = $executable
@ -445,4 +115,4 @@ Function Run-Command {
}
# this line must stay at the bottom to ensure all defined module parts are exported
Export-ModuleMember -Alias * -Function * -Cmdlet *
Export-ModuleMember -Function Get-ExecutablePath, Load-CommandUtils, Run-Command