win_exec: refactor PS exec runner (#45334)

* win_exec: refactor PS exec runner

* more changes for PSCore compatibility

* made some changes based on the recent review

* split up module exec scripts for smaller payload

* removed C# module support to focus on just error msg improvement

* cleaned up c# test classifier code
This commit is contained in:
Jordan Borean 2018-10-03 08:55:53 +10:00 committed by Matt Davis
parent aa2f3edb49
commit e972287c35
34 changed files with 2751 additions and 1676 deletions

View file

@ -0,0 +1,721 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;
// TODO: make some classes/structs private/internal before the next release
namespace Ansible.Become
{
[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;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
public byte[] _data1;
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;
}
[StructLayout(LayoutKind.Sequential)]
public struct SID_AND_ATTRIBUTES
{
public IntPtr Sid;
public int Attributes;
}
public struct TOKEN_USER
{
public SID_AND_ATTRIBUTES User;
}
[Flags]
public enum StartupInfoFlags : uint
{
USESTDHANDLES = 0x00000100
}
[Flags]
public enum CreationFlags : uint
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
}
public enum HandleFlags : uint
{
None = 0,
INHERIT = 1
}
[Flags]
public enum LogonFlags
{
LOGON_WITH_PROFILE = 0x00000001,
LOGON_NETCREDENTIALS_ONLY = 0x00000002
}
public enum LogonType
{
LOGON32_LOGON_INTERACTIVE = 2,
LOGON32_LOGON_NETWORK = 3,
LOGON32_LOGON_BATCH = 4,
LOGON32_LOGON_SERVICE = 5,
LOGON32_LOGON_UNLOCK = 7,
LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
LOGON32_LOGON_NEW_CREDENTIALS = 9
}
public enum LogonProvider
{
LOGON32_PROVIDER_DEFAULT = 0,
}
public enum TokenInformationClass
{
TokenUser = 1,
TokenType = 8,
TokenImpersonationLevel = 9,
TokenElevationType = 18,
TokenLinkedToken = 19,
}
public enum TokenElevationType
{
TokenElevationTypeDefault = 1,
TokenElevationTypeFull,
TokenElevationTypeLimited
}
[Flags]
public enum ProcessAccessFlags : uint
{
PROCESS_QUERY_INFORMATION = 0x00000400,
}
public enum SECURITY_IMPERSONATION_LEVEL
{
SecurityImpersonation,
}
public enum TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation
}
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 CommandResult
{
public string StandardOut { get; internal set; }
public string StandardError { get; internal set; }
public uint ExitCode { get; internal set; }
}
public class BecomeUtil
{
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
LogonType dwLogonType,
LogonProvider dwLogonProvider,
out IntPtr phToken);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcessWithTokenW(
IntPtr hToken,
LogonFlags dwLogonFlags,
[MarshalAs(UnmanagedType.LPTStr)]
string lpApplicationName,
StringBuilder lpCommandLine,
CreationFlags dwCreationFlags,
IntPtr lpEnvironment,
[MarshalAs(UnmanagedType.LPTStr)]
string lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll")]
private static extern bool CreatePipe(
out SafeFileHandle hReadPipe,
out SafeFileHandle hWritePipe,
SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
private 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)]
private static extern bool CloseHandle(
IntPtr hObject);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetProcessWindowStation();
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetThreadDesktop(
int dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetCurrentThreadId();
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool GetTokenInformation(
IntPtr TokenHandle,
TokenInformationClass TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("psapi.dll", SetLastError = true)]
private static extern bool EnumProcesses(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)]
[In][Out] IntPtr[] processIds,
uint cb,
[MarshalAs(UnmanagedType.U4)]
out uint pBytesReturned);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
ProcessAccessFlags processAccess,
bool bInheritHandle,
IntPtr processId);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool OpenProcessToken(
IntPtr ProcessHandle,
TokenAccessLevels DesiredAccess,
out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool ConvertSidToStringSidW(
IntPtr pSID,
[MarshalAs(UnmanagedType.LPTStr)]
out string StringSid);
[DllImport("advapi32", SetLastError = true)]
private static extern bool DuplicateTokenEx(
IntPtr hExistingToken,
TokenAccessLevels dwDesiredAccess,
IntPtr lpTokenAttributes,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
TOKEN_TYPE TokenType,
out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool ImpersonateLoggedOnUser(
IntPtr hToken);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool RevertToSelf();
public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
{
SecurityIdentifier account = null;
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
{
account = GetBecomeSid(username);
}
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);
// Create the environment block if set
IntPtr lpEnvironment = IntPtr.Zero;
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
// Get the user tokens to try running processes with
List<IntPtr> tokens = GetUserTokens(account, username, password, logonType);
bool launch_success = false;
foreach (IntPtr token in tokens)
{
if (CreateProcessWithTokenW(
token,
logonFlags,
null,
new StringBuilder(lpCommandLine),
startup_flags,
lpEnvironment,
lpCurrentDirectory,
si,
out pi))
{
launch_success = true;
break;
}
}
if (!launch_success)
throw new Win32Exception("Failed to start become process");
CommandResult result = new CommandResult();
// 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);
UInt32 rc = GetProcessExitCode(pi.hProcess);
result.StandardOut = stdout_str;
result.StandardError = stderr_str;
result.ExitCode = rc;
return result;
}
private static SecurityIdentifier GetBecomeSid(string username)
{
NTAccount account = new NTAccount(username);
try
{
SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
return security_identifier;
}
catch (IdentityNotMappedException ex)
{
throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message));
}
}
private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
{
List<IntPtr> tokens = new List<IntPtr>();
List<String> service_sids = new List<String>()
{
"S-1-5-18", // NT AUTHORITY\SYSTEM
"S-1-5-19", // NT AUTHORITY\LocalService
"S-1-5-20" // NT AUTHORITY\NetworkService
};
IntPtr hSystemToken = IntPtr.Zero;
string account_sid = "";
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
{
GrantAccessToWindowStationAndDesktop(account);
// Try to get SYSTEM token handle so we can impersonate to get full admin token
hSystemToken = GetSystemUserHandle();
account_sid = account.ToString();
}
bool impersonated = false;
try
{
IntPtr hSystemTokenDup = IntPtr.Zero;
if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
{
// We need the SYSTEM token if we want to become one of those accounts, fail here
throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
}
else if (hSystemToken != IntPtr.Zero)
{
// We have the token, need to duplicate and impersonate
bool dupResult = DuplicateTokenEx(
hSystemToken,
TokenAccessLevels.MaximumAllowed,
IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenPrimary,
out hSystemTokenDup);
int lastError = Marshal.GetLastWin32Error();
CloseHandle(hSystemToken);
if (!dupResult && service_sids.Contains(account_sid))
throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
else if (dupResult && account_sid != "S-1-5-18")
{
if (ImpersonateLoggedOnUser(hSystemTokenDup))
impersonated = true;
else if (service_sids.Contains(account_sid))
throw new Win32Exception("Failed to impersonate as SYSTEM account");
}
// If SYSTEM impersonation failed but we're trying to become a regular user, just proceed;
// might get a limited token in UAC-enabled cases, but better than nothing...
}
string domain = null;
if (service_sids.Contains(account_sid))
{
// We're using a well-known service account, do a service logon instead of the actual flag set
logonType = LogonType.LOGON32_LOGON_SERVICE;
domain = "NT AUTHORITY";
password = null;
switch (account_sid)
{
case "S-1-5-18":
tokens.Add(hSystemTokenDup);
return tokens;
case "S-1-5-19":
username = "LocalService";
break;
case "S-1-5-20":
username = "NetworkService";
break;
}
}
else
{
// We are trying to become a local or domain account
if (username.Contains(@"\"))
{
var user_split = username.Split(Convert.ToChar(@"\"));
domain = user_split[0];
username = user_split[1];
}
else if (username.Contains("@"))
domain = null;
else
domain = ".";
}
IntPtr hToken = IntPtr.Zero;
if (!LogonUser(
username,
domain,
password,
logonType,
LogonProvider.LOGON32_PROVIDER_DEFAULT,
out hToken))
{
throw new Win32Exception("LogonUser failed");
}
if (!service_sids.Contains(account_sid))
{
// Try and get the elevated token for local/domain account
IntPtr hTokenElevated = GetElevatedToken(hToken);
tokens.Add(hTokenElevated);
}
// add the original token as a fallback
tokens.Add(hToken);
}
finally
{
if (impersonated)
RevertToSelf();
}
return tokens;
}
private static IntPtr GetSystemUserHandle()
{
uint array_byte_size = 1024 * sizeof(uint);
IntPtr[] pids = new IntPtr[1024];
uint bytes_copied;
if (!EnumProcesses(pids, array_byte_size, out bytes_copied))
{
throw new Win32Exception("Failed to enumerate processes");
}
// TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array
uint num_processes = bytes_copied / sizeof(uint);
for (uint i = 0; i < num_processes; i++)
{
IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]);
if (hProcess != IntPtr.Zero)
{
IntPtr hToken = IntPtr.Zero;
// According to CreateProcessWithTokenW we require a token with
// TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
// Also add in TOKEN_IMPERSONATE so we can get an impersontated token
TokenAccessLevels desired_access = TokenAccessLevels.Query |
TokenAccessLevels.Duplicate |
TokenAccessLevels.AssignPrimary |
TokenAccessLevels.Impersonate;
if (OpenProcessToken(hProcess, desired_access, out hToken))
{
string sid = GetTokenUserSID(hToken);
if (sid == "S-1-5-18")
{
CloseHandle(hProcess);
return hToken;
}
}
CloseHandle(hToken);
}
CloseHandle(hProcess);
}
return IntPtr.Zero;
}
private static string GetTokenUserSID(IntPtr hToken)
{
uint token_length;
string sid;
if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length))
{
int last_err = Marshal.GetLastWin32Error();
if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER
throw new Win32Exception(last_err, "Failed to get TokenUser length");
}
IntPtr token_information = Marshal.AllocHGlobal((int)token_length);
try
{
if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length))
throw new Win32Exception("Failed to get TokenUser information");
TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER));
if (!ConvertSidToStringSidW(token_user.User.Sid, out sid))
throw new Win32Exception("Failed to get user SID");
}
finally
{
Marshal.FreeHGlobal(token_information);
}
return sid;
}
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;
}
private static IntPtr GetElevatedToken(IntPtr hToken)
{
uint requestedLength;
IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int));
try
{
if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength))
throw new Win32Exception("Unable to get TokenElevationType");
var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo);
// we already have the best token we can get, just use it
if (tet != TokenElevationType.TokenElevationTypeLimited)
return hToken;
GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength);
IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength);
if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength))
throw new Win32Exception("Unable to get linked token");
IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken);
Marshal.FreeHGlobal(pLinkedToken);
return linkedToken;
}
finally
{
Marshal.FreeHGlobal(pTokenInfo);
}
}
private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)
{
const int WindowStationAllAccess = 0x000f037f;
GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess);
const int DesktopRightsAllAccess = 0x000f01ff;
GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
}
private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask)
{
SafeHandle safeHandle = new NoopSafeHandle(handle);
GenericSecurity security =
new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access);
security.AddAccessRule(
new GenericAccessRule(account, accessMask, AccessControlType.Allow));
security.Persist(safeHandle, AccessControlSections.Access);
}
private class GenericSecurity : NativeObjectSecurity
{
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
: base(isContainer, resType, objectHandle, sectionsRequested) { }
public new void Persist(SafeHandle handle, AccessControlSections includeSections) { base.Persist(handle, includeSections); }
public new void AddAccessRule(AccessRule rule) { base.AddAccessRule(rule); }
public override Type AccessRightType { get { throw new NotImplementedException(); } }
public override AccessRule AccessRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AccessControlType type)
{ throw new NotImplementedException(); }
public override Type AccessRuleType { get { return typeof(AccessRule); } }
public override AuditRule AuditRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AuditFlags flags)
{ throw new NotImplementedException(); }
public override Type AuditRuleType { get { return typeof(AuditRule); } }
}
private class NoopSafeHandle : SafeHandle
{
public NoopSafeHandle(IntPtr handle) : base(handle, false) { }
public override bool IsInvalid { get { return false; } }
protected override bool ReleaseHandle() { return true; }
}
private class GenericAccessRule : AccessRule
{
public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
{ }
}
}
}

View file

@ -0,0 +1,261 @@
# Copyright (c) 2018 Ansible Project
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
Function Add-CSharpType {
<#
.SYNOPSIS
Compiles one or more C# scripts similar to Add-Type. This exposes
more configuration options that are useable within Ansible and it
also allows multiple C# sources to be compiled together.
.PARAMETER References
[String[]] A collection of C# scripts to compile together.
.PARAMETER IgnoreWarnings
[Switch] Whether to compile code that contains compiler warnings, by
default warnings will cause a compiler error.
.PARAMETER PassThru
[Switch] Whether to return the loaded Assembly
.PARAMETER AnsibleModule
TODO - This is an AnsibleModule object that is used to derive the
TempPath and Debug values.
TempPath is set to the TmpDir property of the class
IncludeDebugInfo is set when the Ansible verbosity is >= 3
.PARAMETER TempPath
[String] The temporary directory in which the dynamic assembly is
compiled to. This file is deleted once compilation is complete.
Cannot be used when AnsibleModule is set. This is a no-op when
running on PSCore.
.PARAMETER IncludeDebugInfo
[Switch] Whether to include debug information in the compiled
assembly. Cannot be used when AnsibleModule is set. This is a no-op
when running on PSCore.
#>
param(
[Parameter(Mandatory=$true)][AllowEmptyCollection()][String[]]$References,
[Switch]$IgnoreWarnings,
[Switch]$PassThru,
[Parameter(Mandatory=$true, ParameterSetName="Module")][Object]$AnsibleModule,
[Parameter(ParameterSetName="Manual")][String]$TempPath = $env:TMP,
[Parameter(ParameterSetName="Manual")][Switch]$IncludeDebugInfo
)
if ($null -eq $References -or $References.Length -eq 0) {
return
}
# define special symbols CORECLR, WINDOWS, UNIX if required
# the Is* variables are defined on PSCore, if absent we assume an
# older version of PowerShell under .NET Framework and Windows
$defined_symbols = [System.Collections.ArrayList]@()
$is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue
if ($null -ne $is_coreclr) {
if ($is_coreclr.Value) {
$defined_symbols.Add("CORECLR") > $null
}
}
$is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue
if ($null -ne $is_windows) {
if ($is_windows.Value) {
$defined_symbols.Add("WINDOWS") > $null
} else {
$defined_symbols.Add("UNIX") > $null
}
} else {
$defined_symbols.Add("WINDOWS") > $null
}
# pattern used to find referenced assemblies in the code
$assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
# PSCore vs PSDesktop use different methods to compile the code,
# PSCore uses Roslyn and can compile the code purely in memory
# without touching the disk while PSDesktop uses CodeDom and csc.exe
# to compile the code. We branch out here and run each
# distribution's method to add our C# code.
if ($is_coreclr) {
# compile the code using Roslyn on PSCore
# Include the default assemblies using the logic in Add-Type
# https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs
$assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@(
[Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location)
)
$netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref")
foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) {
$assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null
}
# loop through the references, parse as a SyntaxTree and get
# referenced assemblies
$parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols)
$syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@()
foreach ($reference in $References) {
# scan through code and add any assemblies that match
# //AssemblyReference -Name ... [-CLR Core]
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
try {
while ($null -ne ($line = $sr.ReadLine())) {
if ($line -imatch $assembly_pattern) {
# verify the reference is not for .NET Framework
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
continue
}
$assemblies.Add($Matches.Name) > $null
}
}
} finally {
$sr.Close()
}
$syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
}
# Release seems to contain the correct line numbers compared to
# debug,may need to keep a closer eye on this in the future
$compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @(
[Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary
)).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release)
# set warnings to error out if IgnoreWarnings is not set
if (-not $IgnoreWarnings.IsPresent) {
$compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error)
}
# create compilation object
$compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create(
[System.Guid]::NewGuid().ToString(),
$syntax_trees,
$assemblies,
$compiler_options
)
# Load the compiled code and pdb info, we do this so we can
# include line number in a stracktrace
$code_ms = New-Object -TypeName System.IO.MemoryStream
$pdb_ms = New-Object -TypeName System.IO.MemoryStream
try {
$emit_result = $compilation.Emit($code_ms, $pdb_ms)
if (-not $emit_result.Success) {
$errors = [System.Collections.ArrayList]@()
foreach ($e in $emit_result.Diagnostics) {
# builds the error msg, based on logic in Add-Type
# https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239
if ($null -eq $e.Location.SourceTree) {
$errors.Add($e.ToString()) > $null
continue
}
$cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false
$text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines
$line_span = $e.Location.GetLineSpan()
$diagnostic_message = $e.ToString()
$error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString()
$error_position = $line_span.StartLinePosition.Character
$sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4)
$sb.AppendLine($diagnostic_message)
$sb.AppendLine($error_line_string)
for ($i = 0; $i -lt $error_line_string.Length; $i++) {
if ([System.Char]::IsWhiteSpace($error_line_string[$i])) {
continue
}
$sb.Append($error_line_string, 0, $i)
$sb.Append(' ', [Math]::Max(0, $error_position - $i))
$sb.Append("^")
break
}
$errors.Add($sb.ToString()) > $null
}
throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")"
}
$code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
$pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
$compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms)
} finally {
$code_ms.Close()
$pdb_ms.Close()
}
} else {
# compile the code using CodeDom on PSDesktop
# configure compile options based on input
if ($PSCmdlet.ParameterSetName -eq "Module") {
$temp_path = $AnsibleModule.TmpDir
$include_debug = $AnsibleModule.Verbosity -ge 3
} else {
$temp_path = $TempPath
$include_debug = $IncludeDebugInfo.IsPresent
}
$compiler_options = [System.Collections.ArrayList]@("/optimize")
if ($defined_symbols.Count -gt 0) {
$compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null
}
$compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters
$compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray())
$compile_parameters.GenerateExecutable = $false
$compile_parameters.GenerateInMemory = $true
$compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent)
$compile_parameters.IncludeDebugInformation = $include_debug
$compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false)
# Add-Type automatically references System.dll, System.Core.dll,
# and System.Management.Automation.dll which we replicate here
$assemblies = [System.Collections.Generic.HashSet`1[String]]@(
"System.dll",
"System.Core.dll",
([System.Reflection.Assembly]::GetAssembly([PSObject])).Location
)
# create a code snippet for each reference and check if we need
# to reference any extra assemblies
# //AssemblyReference -Name ... [-CLR Framework]
$compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
foreach ($reference in $References) {
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
try {
while ($null -ne ($line = $sr.ReadLine())) {
if ($line -imatch $assembly_pattern) {
# verify the reference is not for .NET Core
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
continue
}
$assemblies.Add($Matches.Name) > $null
}
}
} finally {
$sr.Close()
}
$compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
}
$compile_parameters.ReferencedAssemblies.AddRange($assemblies)
# compile the code together and check for errors
$provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
$compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units.ToArray())
if ($compile.Errors.HasErrors) {
$msg = "Failed to compile C# code: "
foreach ($e in $compile.Errors) {
$msg += "`r`n" + $e.ToString()
}
throw [InvalidOperationException]$msg
}
$compiled_assembly = $compile.CompiledAssembly
}
# return the compiled assembly if PassThru is set.
if ($PassThru) {
return $compiled_assembly
}
}
Export-ModuleMember -Function Add-CSharpType