using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Management.Automation;
using System.Globalization;
namespace Amazon.Lambda.PowerShellHost
{
///
/// Handles converting the errors coming from PowerShell to .NET Exceptions
///
public class ExceptionManager
{
private const string CUSTOM_EXCEPTION_FIELD = "Exception";
private const string CUSTOM_MESSAGE_FIELD = "Message";
private IDictionary _customExceptions;
private Lazy _assemblyBuilder;
private Lazy _moduleBuilder;
///
/// Default Constructor
///
public ExceptionManager()
{
_customExceptions = new Dictionary();
_assemblyBuilder = new Lazy(
() => AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run));
_moduleBuilder = new Lazy(
() => _assemblyBuilder.Value.DefineDynamicModule("CustomExceptions"));
}
///
/// Given the exception from the PowerShell host that ran a PowerShell script, this method will determine what type of exception
/// should be thrown back to Lambda. This can involve creating a dynamic exception if an exception type string is
/// given from PowerShell.
///
/// For example if the PS script executes the following command
///
/// throw @{'Exception'='AccountNotFound';'Message'='The account was not found'}
///
/// Then this method will create a new .NET exception type called AccountNotFound with a message of "The Acount was not found".
/// The PS host can then throw this new .NET exception type. If the PowerShell-based Lambda functions is used in AWS StepFunctions
/// the state machine can choose to make a choice based on the AccountNotFound error.
///
///
///
public Exception DetermineExceptionToThrow(Exception e)
{
var runtimeException = e as RuntimeException;
if (runtimeException == null)
return e;
if(runtimeException.ErrorRecord?.TargetObject != null)
{
string exceptionTypeName = null;
string exceptionMessage = null;
if(runtimeException.ErrorRecord.TargetObject is System.Collections.Hashtable fields && fields.ContainsKey(CUSTOM_EXCEPTION_FIELD))
{
exceptionTypeName = fields[CUSTOM_EXCEPTION_FIELD] as string;
exceptionMessage = fields[CUSTOM_MESSAGE_FIELD] as string ?? exceptionTypeName;
}
else if (runtimeException.ErrorRecord.TargetObject is string str && IsErrorCode(str))
{
exceptionTypeName = str;
exceptionMessage = str;
}
if (!string.IsNullOrEmpty(exceptionTypeName))
{
var exceptionType = GetCustomExceptionType(exceptionTypeName);
var constructor = exceptionType.GetConstructor(new Type[] { typeof(string) });
var obj = constructor.Invoke(new object[] { exceptionMessage });
var customException = obj as Exception;
return customException;
}
}
if(runtimeException.InnerException != null)
{
return runtimeException.InnerException;
}
if(runtimeException.ErrorRecord?.Exception != null)
{
return runtimeException.ErrorRecord.Exception;
}
return runtimeException;
}
///
/// For a given type name dynamically create an exception type using System.Reflection.Emit. The types are
/// cached so that if the same type name is request again the type is only created once.
///
///
///
public Type GetCustomExceptionType(string typeName)
{
// If the typeName has been request in the past return the already generated type.
if(_customExceptions.TryGetValue(typeName, out var type))
{
return type;
}
try
{
var baseExceptionType = typeof(Exception);
var baseConstructor = baseExceptionType.GetConstructor(new Type[] { typeof(string) });
var tb = _moduleBuilder.Value.DefineType(typeName,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
baseExceptionType);
// Create constructor taking in a string parameter that is then passed to the base constructor
var constructor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(string) });
var ilGenerator = constructor.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0); // Adds the "this" argument
ilGenerator.Emit(OpCodes.Ldarg_1); // Adds the string argument
ilGenerator.Emit(OpCodes.Call, baseConstructor); // Call the base constructor using the "this" instance taking in the string argument.
ilGenerator.Emit(OpCodes.Ret);
var newExceptionType = tb.CreateType();
// Cache exception type
_customExceptions[typeName] = newExceptionType;
return newExceptionType;
}
catch(Exception e)
{
throw new LambdaPowerShellException($"Error creating customer error type for {typeName}: {e.Message}", e);
}
}
///
/// Check to see if the string coming from a PS throw statement is an error code. That means the string
/// could be used as a class name for an Exception class.
///
///
///
public static bool IsErrorCode(string value)
{
return IsValidTypeNameOrIdentifier(value, true);
}
// Utility methods taken from CSharpHelpers from the corefx repo
// https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/Common/src/System/CSharpHelpers.cs
#region Type name validation utilities
internal static bool IsValidTypeNameOrIdentifier(string value, bool isTypeName)
{
bool nextMustBeStartChar = true;
if (value.Length == 0)
return false;
// each char must be Lu, Ll, Lt, Lm, Lo, Nd, Mn, Mc, Pc
//
for (int i = 0; i < value.Length; i++)
{
char ch = value[i];
UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(ch);
switch (uc)
{
case UnicodeCategory.UppercaseLetter: // Lu
case UnicodeCategory.LowercaseLetter: // Ll
case UnicodeCategory.TitlecaseLetter: // Lt
case UnicodeCategory.ModifierLetter: // Lm
case UnicodeCategory.LetterNumber: // Lm
case UnicodeCategory.OtherLetter: // Lo
nextMustBeStartChar = false;
break;
case UnicodeCategory.NonSpacingMark: // Mn
case UnicodeCategory.SpacingCombiningMark: // Mc
case UnicodeCategory.ConnectorPunctuation: // Pc
case UnicodeCategory.DecimalDigitNumber: // Nd
// Underscore is a valid starting character, even though it is a ConnectorPunctuation.
if (nextMustBeStartChar && ch != '_')
return false;
nextMustBeStartChar = false;
break;
default:
// We only check the special Type chars for type names.
if (isTypeName && IsSpecialTypeChar(ch, ref nextMustBeStartChar))
{
break;
}
return false;
}
}
return true;
}
// This can be a special character like a separator that shows up in a type name
// This is an odd set of characters. Some come from characters that are allowed by C++, like < and >.
// Others are characters that are specified in the type and assembly name grammar.
internal static bool IsSpecialTypeChar(char ch, ref bool nextMustBeStartChar)
{
switch (ch)
{
case ':':
case '.':
case '$':
case '+':
case '<':
case '>':
case '-':
case '[':
case ']':
case ',':
case '&':
case '*':
nextMustBeStartChar = true;
return true;
case '`':
return true;
}
return false;
}
#endregion
}
}