using Amazon.Lambda.Core; using System; using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace Amazon.Lambda.PowerShellHost { /// /// Base class for Lambda functions hosting PowerShell Core runtime /// public abstract class PowerShellFunctionHost { readonly ExceptionManager _exceptionManager = new ExceptionManager(); /// /// When using a PowerShell function handler the function is identified in this environment variable. /// public const string POWERSHELL_FUNCTION_ENV = "AWS_POWERSHELL_FUNCTION_HANDLER"; /// /// An optional property that identifies the PowerShell function to execute once the script is loaded. /// Otherwise just the script will be executed. /// public virtual string PowerShellFunctionName {get;set;} // Resource file included with the Lambda package bundle that will be executed. private readonly string _powerShellScriptFileName; private string _powerShellScriptFileContent; // The PowerShell Object for executing PowerShell code private readonly PowerShell _ps; // Holds the PSObject Standard Output from the PowerShell execution private PSDataCollection _output; // Holds the exception captured from the stream capture of the PowerShell execution. This is the exception returned to Lambda // when the script execution stops. private Exception _lastException; private bool _runFirstTimeInitialization = true; // Logging messages that happen during the constructor are saved in a buffer to be written out once we have an instance of // ILambdaLogger. The preference for using ILambdaLogger over Console.WriteLine is so that tools like the console or the VS toolkit // that invoke the functions and request the tail will also get these logging messages. private StringBuilder _constructorLoggingBuffer = new StringBuilder(); private ILambdaLogger _logger; /// /// Creates an instances of the class. As part of creation it will initiate the PowerShell Core runtime and load any required PowerShell modules. /// protected PowerShellFunctionHost() { // This will only be true for local testing. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var state = InitialSessionState.CreateDefault(); state.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; this._ps = PowerShell.Create(state); } else { this._ps = PowerShell.Create(); } this.SetupStreamHandlers(); this.LoadModules(); // Can be true if there was an exception importing modules packaged with the function. if(this._lastException != null) { Console.WriteLine(this._constructorLoggingBuffer.ToString()); throw this._lastException; } this.PowerShellFunctionName = Environment.GetEnvironmentVariable(POWERSHELL_FUNCTION_ENV); if(!string.IsNullOrEmpty(this.PowerShellFunctionName)) { this._constructorLoggingBuffer.AppendLine($"Configured to call function {this.PowerShellFunctionName} from the PowerShell script."); } } /// /// /// Creates an instances of the class. As part of creation it will initiated the PowerShell Core runtime and load any required PowerShell modules. /// /// The PowerShell script that will run as part of every Lambda invocation protected PowerShellFunctionHost(string powerShellScriptFileName) : this() { this._powerShellScriptFileName = powerShellScriptFileName; } /// /// AWS Lambda function handler that will execute the PowerShell script with the PowerShell Core runtime initiated during the construction of the class. /// /// /// /// public Stream ExecuteFunction(Stream inputStream, ILambdaContext context) { this._lastException = null; if (this._runFirstTimeInitialization) { this._logger = context.Logger; if (this._constructorLoggingBuffer?.Length > 0) { context.Logger.Log(this._constructorLoggingBuffer.ToString()); this._constructorLoggingBuffer = null; } this._runFirstTimeInitialization = false; } string inputString; using (var reader = new StreamReader(inputStream)) { inputString = reader.ReadToEnd(); } var result = this.BeginInvoke(inputString, context); this.WaitPowerShellExecution(result); if (this._lastException != null || this._ps.InvocationStateInfo.State == PSInvocationState.Failed) { var exception = this._exceptionManager.DetermineExceptionToThrow(this._lastException ?? this._ps.InvocationStateInfo.Reason); throw exception; } return new MemoryStream(Encoding.UTF8.GetBytes(this.GetExecutionOutput())); } /// /// Begin the async PowerShell execution /// /// /// /// private IAsyncResult BeginInvoke(string input, ILambdaContext context) { // Clear all previous PowerShell executions, variables and outputs this._ps.Commands?.Clear(); this._ps.Streams.Verbose?.Clear(); this._ps.Streams.Error?.Clear(); this._ps.Runspace?.ResetRunspaceState(); this._output.Clear(); var providedScript = LoadScript(input, context); string executingScript = @" Param( [string]$LambdaInputString, [Amazon.Lambda.Core.ILambdaContext]$LambdaContext ) $LambdaInput = ConvertFrom-Json -InputObject $LambdaInputString "; var isLambda = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LAMBDA_TASK_ROOT")); var tempFolder = isLambda ? "/tmp" : Path.GetTempPath(); executingScript += $"{Environment.NewLine}$env:TEMP=\"{tempFolder}\""; executingScript += $"{Environment.NewLine}$env:TMP=\"{tempFolder}\""; executingScript += $"{Environment.NewLine}$env:TMPDIR=\"{tempFolder}\"{Environment.NewLine}"; if(isLambda && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HOME"))) { // Make sure to set HOME directory to avoid issue with using the -Parallel PowerShell feature. This works around // a reported issue to the PowerShell team. // https://github.com/PowerShell/PowerShell/issues/13189 Environment.SetEnvironmentVariable("HOME", $"{tempFolder}/home"); } executingScript += providedScript; if (!string.IsNullOrEmpty(this.PowerShellFunctionName)) { executingScript += $"{Environment.NewLine}{this.PowerShellFunctionName} $LambdaInput $LambdaContext{Environment.NewLine}"; } this._ps.AddScript(executingScript); this._ps.AddParameter("LambdaInputString", input); this._ps.AddParameter("LambdaContext", context); return this._ps.BeginInvoke(null, this._output); } /// /// Reads the script from disk /// /// /// /// protected virtual string LoadScript(string input, ILambdaContext context) { // Check to see if the file contents have already been read. if(this._powerShellScriptFileContent != null) { return this._powerShellScriptFileContent; } if(string.IsNullOrEmpty(this._powerShellScriptFileName)) { throw new LambdaPowerShellException("No PowerShell script specified to be executed. Either specify a script in the constructor or override the LoadScript method."); } if(!File.Exists(this._powerShellScriptFileName)) { throw new LambdaPowerShellException($"Failed to find PowerShell script {this._powerShellScriptFileName}. Make sure the script is included with the package bundle."); } this._powerShellScriptFileContent = File.ReadAllText(this._powerShellScriptFileName); return this._powerShellScriptFileContent; } /// /// Waits for the PowerShell execution to be completed /// private void WaitPowerShellExecution(IAsyncResult result) { while (!result.IsCompleted) { result.AsyncWaitHandle.WaitOne(500); } } /// /// Returns the string output from the PowerShell execution, or an empty string /// private string GetExecutionOutput() { var responseObject = this._output?.LastOrDefault(); if (responseObject == null) { return string.Empty; } else if(responseObject.BaseObject is string baseObj) { return baseObj; } this._ps.Commands?.Clear(); this._ps.Runspace?.ResetRunspaceState(); string executingScript = @" Param( [PSObject]$Response ) ConvertTo-Json $Response "; this._ps.AddScript(executingScript); this._ps.AddParameter("Response", responseObject); var marshalled = this._ps.Invoke(); return marshalled.FirstOrDefault()?.BaseObject as string; } private void SetupStreamHandlers() { this._output = new PSDataCollection(); Func> _loggerFactory = (prefix) => { EventHandler handler = (sender, e) => { var message = e?.ItemAdded?.ToString(); LogMessage(prefix, message); var errorRecord = e?.ItemAdded as ErrorRecord; if (errorRecord?.Exception != null) { this._lastException = errorRecord.Exception; } }; return handler; }; this._ps.Streams.Verbose.DataAdding += _loggerFactory("Verbose"); this._ps.Streams.Information.DataAdding += _loggerFactory("Information"); this._ps.Streams.Warning.DataAdding += _loggerFactory("Warning"); this._ps.Streams.Error.DataAdding += _loggerFactory("Error"); } private void LogMessage(string prefix, string message) { if (string.IsNullOrEmpty(message)) { return; } if(!string.IsNullOrEmpty(prefix)) { message = $"[{prefix}] - {message}"; } if (this._logger != null) { this._logger.LogLine(message); } else { this._constructorLoggingBuffer.AppendLine(message); } } /// /// Import all the bundled PowerShell modules. Bundle modules are stored under the Modules folder with the structure Module-Name/Version-Number/Module-Name.psd1. /// It is possible that developer accidently includes multiple versions of the same module. If so we default to loading the latest version. /// private void LoadModules() { if (!Directory.Exists("./Modules")) return; foreach (var moduleDir in Directory.GetDirectories("./Modules")) { var versionDir = Directory.GetDirectories(moduleDir).OrderByDescending(x => Version.TryParse(new DirectoryInfo(x).Name, out var version) ? version : new Version("0.0.0")).FirstOrDefault(); if (string.IsNullOrEmpty(versionDir)) continue; var module = new DirectoryInfo(moduleDir).Name; var psd1Path = Path.Combine(versionDir, $"{module}.psd1"); if (!File.Exists(psd1Path)) { var files = Directory.GetFiles(versionDir, "*.psd1", SearchOption.TopDirectoryOnly); if (files.Length == 1) { psd1Path = files[0]; } } if (!File.Exists(psd1Path)) { _constructorLoggingBuffer.AppendLine($"Unable to determine psd1 file for {module} ({new DirectoryInfo(versionDir).Name})"); continue; } _constructorLoggingBuffer.AppendLine($"Importing module {psd1Path}"); var result = this._ps.AddScript($"Import-Module \"{psd1Path}\"").BeginInvoke(); WaitPowerShellExecution(result); } } static PowerShellFunctionHost() { const string envName = "AWS_EXECUTION_ENV"; const string powershellEnv = "powershell"; var envValue = Environment.GetEnvironmentVariable(envName); if(!string.IsNullOrEmpty(envValue) && !envValue.Contains(powershellEnv)) { var assemblyVersion = typeof(PowerShellFunctionHost).Assembly .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) .FirstOrDefault() as AssemblyInformationalVersionAttribute; Environment.SetEnvironmentVariable(envName, $"{envValue}_{powershellEnv}_{assemblyVersion?.InformationalVersion}"); } } } }