From ec76e8eaaf2fa6c9aa16468bfc98555d7d04ecdb Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 29 Aug 2018 19:38:46 -0700 Subject: [PATCH 1/8] Switch to module approach --- examples/PSCoreApp/MyHttpTrigger/run.ps1 | 12 +- .../PowerShell/PowerShellExtensions.cs | 27 +++ .../Requests/HandleInvocationRequest.cs | 91 ++++++++++ src/PowerShell/PowerShellManager.cs | 163 ++++++++---------- 4 files changed, 191 insertions(+), 102 deletions(-) create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 index 58a61a1c..77a7c0b0 100644 --- a/examples/PSCoreApp/MyHttpTrigger/run.ps1 +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -1,10 +1,8 @@ -param($req, $TriggerMetadata) - -# Write-Host $TriggerMetadata["Name"] - # Invoked with Invoke-RestMethod: # irm http://localhost:7071/api/MyHttpTrigger?Name=Tyler -# Input bindings are added to the scope of the script: ex. `$req` +# Input bindings are added via param block + +param($req, $TriggerMetadata) # If no name was passed by query parameter $name = 'World' @@ -22,7 +20,7 @@ Write-Warning "Warning $name" $name # You set the value of your output bindings by assignment `$nameOfOutputBinding = 'foo'` -$res = [HttpResponseContext]@{ +Push-OutputBinding -Name res -Value ([HttpResponseContext]@{ Body = @{ Hello = $name } ContentType = 'application/json' -} \ No newline at end of file +}) \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs new file mode 100644 index 00000000..6ad41a07 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.ObjectModel; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell +{ + using System.Management.Automation; + + internal static class PowerShellExtensions + { + public static void InvokeAndClearCommands(this PowerShell pwsh) + { + pwsh.Invoke(); + pwsh.Commands.Clear(); + } + + public static Collection InvokeAndClearCommands(this PowerShell pwsh) + { + var result = pwsh.Invoke(); + pwsh.Commands.Clear(); + return result; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs new file mode 100644 index 00000000..edbc661a --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + internal static class HandleInvocationRequest + { + public static StreamingMessage Invoke( + PowerShellManager powerShellManager, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + InvocationRequest invocationRequest = request.InvocationRequest; + + // Set the RequestId and InvocationId for logging purposes + logger.SetContext(request.RequestId, invocationRequest.InvocationId); + + // Load information about the function + var functionInfo = functionLoader.GetInfo(invocationRequest.FunctionId); + (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); + + // Bundle all TriggerMetadata into Hashtable to send down to PowerShell + Hashtable triggerMetadata = new Hashtable(); + foreach (var dataItem in invocationRequest.TriggerMetadata) + { + triggerMetadata.Add(dataItem.Key, dataItem.Value.ToObject()); + } + + // Assume success unless something bad happens + var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; + var response = new StreamingMessage() + { + RequestId = request.RequestId, + InvocationResponse = new InvocationResponse() + { + InvocationId = invocationRequest.InvocationId, + Result = status + } + }; + + // Invoke powershell logic and return hashtable of out binding data + Hashtable result = null; + try + { + result = powerShellManager.InvokeFunction( + scriptPath, + entryPoint, + triggerMetadata, + invocationRequest.InputData); + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = e.ToRpcException(); + return response; + } + + // Set out binding data and return response to be sent back to host + foreach (KeyValuePair binding in functionInfo.OutputBindings) + { + // TODO: How do we want to handle when a binding is not set? + ParameterBinding paramBinding = new ParameterBinding() + { + Name = binding.Key, + Data = result[binding.Key].ToTypedData() + }; + + response.InvocationResponse.OutputData.Add(paramBinding); + + // if one of the bindings is $return we need to also set the ReturnValue + if(binding.Key == "$return") + { + response.InvocationResponse.ReturnValue = paramBinding.Data; + } + } + + return response; + } + } +} diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 53a7f514..192422f6 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -17,36 +17,19 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell { using System.Management.Automation; + using System.Reflection; internal class PowerShellManager { - // This script handles when the user adds something to the pipeline. - // It logs the item that comes and stores it as the $return out binding. - // The last item stored as $return will be returned to the function host. - - readonly static string s_LogAndSetReturnValueScript = @" -param([Parameter(ValueFromPipeline=$true)]$return) - -Write-Information $return - -Set-Variable -Name '$return' -Value $return -Scope global -"; - - readonly static string s_SetExecutionPolicyOnWindowsScript = @" -if ($IsWindows) -{ - Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -} -"; - readonly static string s_TriggerMetadataParameterName = "TriggerMetadata"; + readonly static bool s_UseLocalScope = true; RpcLogger _logger; PowerShell _pwsh; - PowerShellManager(RpcLogger logger) + PowerShellManager(PowerShell pwsh, RpcLogger logger) { - _pwsh = PowerShell.Create(InitialSessionState.CreateDefault()); + _pwsh = pwsh; _logger = logger; // Setup Stream event listeners @@ -60,63 +43,33 @@ internal class PowerShellManager } public static PowerShellManager Create(RpcLogger logger) - { - var manager = new PowerShellManager(logger); - - // Add HttpResponseContext namespace so users can reference - // HttpResponseContext without needing to specify the full namespace - manager.ExecuteScriptAndClearCommands($"using namespace {typeof(HttpResponseContext).Namespace}"); - manager.ExecuteScriptAndClearCommands(s_SetExecutionPolicyOnWindowsScript); - return manager; - } - - static string BuildBindingHashtableScript(IDictionary outBindings) { - // Since all of the out bindings are stored in variables at this point, - // we must construct a script that will return those output bindings in a hashtable - StringBuilder script = new StringBuilder(); - script.AppendLine("@{"); - foreach (KeyValuePair binding in outBindings) + // Set up initial session state: set execution policy, import helper module, and using namespace + var initialSessionState = InitialSessionState.CreateDefault(); + if(Platform.IsWindows) { - script.Append("'"); - script.Append(binding.Key); - - // since $return has a dollar sign, we have to treat it differently - if (binding.Key == "$return") - { - script.Append("' = "); - } - else - { - script.Append("' = $"); - } - script.AppendLine(binding.Key); + initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; } - script.AppendLine("}"); - - return script.ToString(); - } - - void ResetRunspace() - { - // Reset the runspace to the Initial Session State - _pwsh.Runspace.ResetRunspaceState(); - } - - void ExecuteScriptAndClearCommands(string script) - { - _pwsh.AddScript(script).Invoke(); - _pwsh.Commands.Clear(); - } + var pwsh = PowerShell.Create(initialSessionState); - public Collection ExecuteScriptAndClearCommands(string script) - { - var result = _pwsh.AddScript(script).Invoke(); - _pwsh.Commands.Clear(); - return result; + // Add HttpResponseContext namespace so users can reference + // HttpResponseContext without needing to specify the full namespace + // and also import the Azure Functions binding helper module + string modulePath = System.IO.Path.Join( + AppDomain.CurrentDomain.BaseDirectory, + "Azure.Functions.PowerShell.Worker.Module", + "Azure.Functions.PowerShell.Worker.Module.psd1"); + pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}") + .AddStatement() + .AddCommand("Import-Module") + .AddParameter("Name", modulePath) + .AddParameter("Scope", "Global") + .InvokeAndClearCommands(); + + return new PowerShellManager(pwsh, logger); } - public PowerShellManager InvokeFunctionAndSetGlobalReturn( + public Hashtable InvokeFunction( string scriptPath, string entryPoint, Hashtable triggerMetadata, @@ -135,15 +88,23 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn( { if (entryPoint != "") { - ExecuteScriptAndClearCommands($@". {scriptPath}"); - parameterMetadata = ExecuteScriptAndClearCommands($@"Get-Command {entryPoint}")[0].Parameters; - _pwsh.AddScript($@". {entryPoint} @args"); + parameterMetadata = _pwsh + .AddScript($@". {scriptPath}", s_UseLocalScope) + .AddStatement() + .AddCommand("Get-Command", s_UseLocalScope).AddParameter("Name", entryPoint) + .InvokeAndClearCommands()[0].Parameters; + + _pwsh + .AddScript($@". {scriptPath}", s_UseLocalScope) + .AddStatement() + .AddCommand(entryPoint, s_UseLocalScope); } else { - parameterMetadata = ExecuteScriptAndClearCommands($@"Get-Command {scriptPath}")[0].Parameters; - _pwsh.AddScript($@". {scriptPath} @args"); + parameterMetadata = _pwsh.AddCommand("Get-Command", s_UseLocalScope).AddParameter("Name", scriptPath) + .InvokeAndClearCommands()[0].Parameters; + _pwsh.AddCommand(scriptPath, s_UseLocalScope); } } @@ -160,35 +121,47 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn( _logger.LogDebug($"TriggerMetadata found. Value:{Environment.NewLine}{triggerMetadata.ToString()}"); } - // This script handles when the user adds something to the pipeline. + PSObject returnObject = null; using (ExecutionTimer.Start(_logger, "Execution of the user's function completed.")) { - ExecuteScriptAndClearCommands(s_LogAndSetReturnValueScript); + // Log everything we received from the pipeline and set the last one to be the ReturnObject + Collection pipelineItems = _pwsh.InvokeAndClearCommands(); + foreach (var psobject in pipelineItems) + { + _logger.LogInformation(psobject.ToString()); + } + + returnObject = pipelineItems[pipelineItems.Count - 1]; } - return this; - } - catch(Exception e) - { - ResetRunspace(); - throw e; - } - } + + var result = _pwsh.AddCommand("Get-OutputBinding", s_UseLocalScope).InvokeAndClearCommands()[0]; - public Hashtable ReturnBindingHashtable(IDictionary outBindings) - { - try - { - // This script returns a hashtable that contains the - // output bindings that we will return to the function host. - var result = ExecuteScriptAndClearCommands(BuildBindingHashtableScript(outBindings))[0]; + if(returnObject != null) + { + result.Add("$return", returnObject); + } ResetRunspace(); return result; } catch(Exception e) { ResetRunspace(); - throw e; + throw; } } + + void ResetRunspace() + { + // Reset the runspace to the Initial Session State + _pwsh.Runspace.ResetRunspaceState(); + + // TODO: Change this to clearing the variable by running in the module + string modulePath = System.IO.Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Azure.Functions.PowerShell.Worker.Module", "Azure.Functions.PowerShell.Worker.Module.psd1"); + _pwsh.AddCommand("Import-Module") + .AddParameter("Name", modulePath) + .AddParameter("Scope", "Global") + .AddParameter("Force") + .InvokeAndClearCommands(); + } } } From b099d0947e11e19a08adc7f5766b951956488c3f Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 29 Aug 2018 23:03:15 -0700 Subject: [PATCH 2/8] misc comment updates --- .../PowerShell/PowerShellExtensions.cs | 2 +- src/PowerShell/PowerShellManager.cs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs index 6ad41a07..1d4e7723 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs @@ -24,4 +24,4 @@ public static Collection InvokeAndClearCommands(this PowerShell pwsh) return result; } } -} \ No newline at end of file +} diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 192422f6..1dbf9645 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -7,7 +7,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -17,7 +16,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell { using System.Management.Automation; - using System.Reflection; internal class PowerShellManager { @@ -44,7 +42,7 @@ internal class PowerShellManager public static PowerShellManager Create(RpcLogger logger) { - // Set up initial session state: set execution policy, import helper module, and using namespace + // Set up initial session state var initialSessionState = InitialSessionState.CreateDefault(); if(Platform.IsWindows) { @@ -52,15 +50,17 @@ public static PowerShellManager Create(RpcLogger logger) } var pwsh = PowerShell.Create(initialSessionState); - // Add HttpResponseContext namespace so users can reference - // HttpResponseContext without needing to specify the full namespace - // and also import the Azure Functions binding helper module + // Build path to the Azure Functions binding helper module string modulePath = System.IO.Path.Join( AppDomain.CurrentDomain.BaseDirectory, "Azure.Functions.PowerShell.Worker.Module", "Azure.Functions.PowerShell.Worker.Module.psd1"); + + // Add HttpResponseContext namespace so users can reference + // HttpResponseContext without needing to specify the full namespace pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}") .AddStatement() + // Import the Azure Functions binding helper module .AddCommand("Import-Module") .AddParameter("Name", modulePath) .AddParameter("Scope", "Global") @@ -83,7 +83,6 @@ public Hashtable InvokeFunction( // If it does, we invoke the command of that name. We also need to fetch // the ParameterMetadata so that we can tell whether or not the user is asking // for the $TriggerMetadata - using (ExecutionTimer.Start(_logger, "Parameter metadata retrieved.")) { if (entryPoint != "") From e34c6875b5932c5d3bc4f8c8a038401af3f44624 Mon Sep 17 00:00:00 2001 From: "Tyler Leonhardt (POWERSHELL)" Date: Thu, 30 Aug 2018 14:12:27 -0700 Subject: [PATCH 3/8] resolve rebase conflicts --- .../Requests/HandleInvocationRequest.cs | 91 ------------------- .../PowerShell/PowerShellExtensions.cs | 0 2 files changed, 91 deletions(-) delete mode 100644 src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs rename src/{Azure.Functions.PowerShell.Worker => }/PowerShell/PowerShellExtensions.cs (100%) diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs deleted file mode 100644 index edbc661a..00000000 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs +++ /dev/null @@ -1,91 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using System.Collections; -using System.Collections.Generic; - -using Microsoft.Azure.Functions.PowerShellWorker.Utility; -using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; - -namespace Microsoft.Azure.Functions.PowerShellWorker.Requests -{ - internal static class HandleInvocationRequest - { - public static StreamingMessage Invoke( - PowerShellManager powerShellManager, - FunctionLoader functionLoader, - StreamingMessage request, - RpcLogger logger) - { - InvocationRequest invocationRequest = request.InvocationRequest; - - // Set the RequestId and InvocationId for logging purposes - logger.SetContext(request.RequestId, invocationRequest.InvocationId); - - // Load information about the function - var functionInfo = functionLoader.GetInfo(invocationRequest.FunctionId); - (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); - - // Bundle all TriggerMetadata into Hashtable to send down to PowerShell - Hashtable triggerMetadata = new Hashtable(); - foreach (var dataItem in invocationRequest.TriggerMetadata) - { - triggerMetadata.Add(dataItem.Key, dataItem.Value.ToObject()); - } - - // Assume success unless something bad happens - var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; - var response = new StreamingMessage() - { - RequestId = request.RequestId, - InvocationResponse = new InvocationResponse() - { - InvocationId = invocationRequest.InvocationId, - Result = status - } - }; - - // Invoke powershell logic and return hashtable of out binding data - Hashtable result = null; - try - { - result = powerShellManager.InvokeFunction( - scriptPath, - entryPoint, - triggerMetadata, - invocationRequest.InputData); - } - catch (Exception e) - { - status.Status = StatusResult.Types.Status.Failure; - status.Exception = e.ToRpcException(); - return response; - } - - // Set out binding data and return response to be sent back to host - foreach (KeyValuePair binding in functionInfo.OutputBindings) - { - // TODO: How do we want to handle when a binding is not set? - ParameterBinding paramBinding = new ParameterBinding() - { - Name = binding.Key, - Data = result[binding.Key].ToTypedData() - }; - - response.InvocationResponse.OutputData.Add(paramBinding); - - // if one of the bindings is $return we need to also set the ReturnValue - if(binding.Key == "$return") - { - response.InvocationResponse.ReturnValue = paramBinding.Data; - } - } - - return response; - } - } -} diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs b/src/PowerShell/PowerShellExtensions.cs similarity index 100% rename from src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellExtensions.cs rename to src/PowerShell/PowerShellExtensions.cs From 6c0c0161443f41617f991eded20cbebcef03446d Mon Sep 17 00:00:00 2001 From: "Tyler Leonhardt (POWERSHELL)" Date: Thu, 30 Aug 2018 15:10:13 -0700 Subject: [PATCH 4/8] address dongbo's feedback --- README.md | 4 +- ...re.Functions.PowerShell.Worker.Module.psm1 | 8 +- src/PowerShell/PowerShellManager.cs | 74 ++++++------------- src/RequestProcessor.cs | 25 +++++-- 4 files changed, 51 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index b85aedd7..ce4682f6 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Prototype for Azure Functions PowerShell Language Worker ```powershell # Windows if you installed the Azure Functions Core Tools via npm Remove-Item -Recurse -Force ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell -Copy-Item src\Azure.Functions.PowerShell.Worker\bin\Debug\netcoreapp2.1\publish ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell -Recurse -Force +Copy-Item src\bin\Debug\netcoreapp2.1\publish ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell -Recurse -Force # macOS if you installed the Azure Functions Core Tools via brew Remove-Item -Recurse -Force /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell -Copy-Item src/Azure.Functions.PowerShell.Worker/bin/Debug/netcoreapp2.1/publish /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell -Recurse -Force +Copy-Item src/bin/Debug/netcoreapp2.1/publish /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell -Recurse -Force ``` \ No newline at end of file diff --git a/src/Modules/Azure.Functions.PowerShell.Worker.Module/Azure.Functions.PowerShell.Worker.Module.psm1 b/src/Modules/Azure.Functions.PowerShell.Worker.Module/Azure.Functions.PowerShell.Worker.Module.psm1 index 31e5d3bb..d4616d60 100644 --- a/src/Modules/Azure.Functions.PowerShell.Worker.Module/Azure.Functions.PowerShell.Worker.Module.psm1 +++ b/src/Modules/Azure.Functions.PowerShell.Worker.Module/Azure.Functions.PowerShell.Worker.Module.psm1 @@ -30,7 +30,10 @@ function Get-OutputBinding { param( [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [string[]] - $Name = '*' + $Name = '*', + + [switch] + $Purge ) begin { $bindings = @{} @@ -39,6 +42,9 @@ function Get-OutputBinding { $script:_OutputBindings.GetEnumerator() | Where-Object Name -Like $Name | ForEach-Object { $null = $bindings.Add($_.Name, $_.Value) } } end { + if($Purge.IsPresent) { + $script:_OutputBindings.Clear() + } return $bindings } } diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 1dbf9645..94fc4e55 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -20,14 +21,15 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell internal class PowerShellManager { readonly static string s_TriggerMetadataParameterName = "TriggerMetadata"; - readonly static bool s_UseLocalScope = true; RpcLogger _logger; PowerShell _pwsh; - PowerShellManager(PowerShell pwsh, RpcLogger logger) + internal PowerShellManager(RpcLogger logger) { - _pwsh = pwsh; + var initialSessionState = InitialSessionState.CreateDefault(); + initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; + _pwsh = PowerShell.Create(initialSessionState); _logger = logger; // Setup Stream event listeners @@ -40,36 +42,19 @@ internal class PowerShellManager _pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding; } - public static PowerShellManager Create(RpcLogger logger) + internal void InitializeRunspace() { - // Set up initial session state - var initialSessionState = InitialSessionState.CreateDefault(); - if(Platform.IsWindows) - { - initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; - } - var pwsh = PowerShell.Create(initialSessionState); - - // Build path to the Azure Functions binding helper module - string modulePath = System.IO.Path.Join( - AppDomain.CurrentDomain.BaseDirectory, - "Azure.Functions.PowerShell.Worker.Module", - "Azure.Functions.PowerShell.Worker.Module.psd1"); - // Add HttpResponseContext namespace so users can reference // HttpResponseContext without needing to specify the full namespace - pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}") - .AddStatement() - // Import the Azure Functions binding helper module - .AddCommand("Import-Module") - .AddParameter("Name", modulePath) - .AddParameter("Scope", "Global") - .InvokeAndClearCommands(); - - return new PowerShellManager(pwsh, logger); + _pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").InvokeAndClearCommands(); + + // Prepend the path to the internal Modules folder to the PSModulePath + var modulePath = Environment.GetEnvironmentVariable("PSModulePath"); + var additionalPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"); + Environment.SetEnvironmentVariable("PSModulePath", $"{additionalPath}{Path.PathSeparator}{modulePath}"); } - public Hashtable InvokeFunction( + internal Hashtable InvokeFunction( string scriptPath, string entryPoint, Hashtable triggerMetadata, @@ -88,22 +73,19 @@ public Hashtable InvokeFunction( if (entryPoint != "") { parameterMetadata = _pwsh - .AddScript($@". {scriptPath}", s_UseLocalScope) + .AddScript($@". {scriptPath}") .AddStatement() - .AddCommand("Get-Command", s_UseLocalScope).AddParameter("Name", entryPoint) + .AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint) .InvokeAndClearCommands()[0].Parameters; - _pwsh - .AddScript($@". {scriptPath}", s_UseLocalScope) - .AddStatement() - .AddCommand(entryPoint, s_UseLocalScope); + _pwsh.AddCommand(entryPoint, useLocalScope: true); } else { - parameterMetadata = _pwsh.AddCommand("Get-Command", s_UseLocalScope).AddParameter("Name", scriptPath) + parameterMetadata = _pwsh.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", scriptPath) .InvokeAndClearCommands()[0].Parameters; - _pwsh.AddCommand(scriptPath, s_UseLocalScope); + _pwsh.AddCommand(scriptPath, useLocalScope: true); } } @@ -127,40 +109,32 @@ public Hashtable InvokeFunction( Collection pipelineItems = _pwsh.InvokeAndClearCommands(); foreach (var psobject in pipelineItems) { - _logger.LogInformation(psobject.ToString()); + _logger.LogInformation($"FROM FUNCTION: {psobject.ToString()}"); } returnObject = pipelineItems[pipelineItems.Count - 1]; } - var result = _pwsh.AddCommand("Get-OutputBinding", s_UseLocalScope).InvokeAndClearCommands()[0]; + var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding", useLocalScope: true) + .AddParameter("Purge") + .InvokeAndClearCommands()[0]; if(returnObject != null) { result.Add("$return", returnObject); } - ResetRunspace(); return result; } - catch(Exception e) + finally { ResetRunspace(); - throw; } } - void ResetRunspace() + private void ResetRunspace() { // Reset the runspace to the Initial Session State _pwsh.Runspace.ResetRunspaceState(); - - // TODO: Change this to clearing the variable by running in the module - string modulePath = System.IO.Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Azure.Functions.PowerShell.Worker.Module", "Azure.Functions.PowerShell.Worker.Module.psd1"); - _pwsh.AddCommand("Import-Module") - .AddParameter("Name", modulePath) - .AddParameter("Scope", "Global") - .AddParameter("Force") - .InvokeAndClearCommands(); } } } diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index ab3a5de9..9398d235 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -26,7 +26,7 @@ internal RequestProcessor(MessagingStream msgStream) { _msgStream = msgStream; _logger = new RpcLogger(msgStream); - _powerShellManager = PowerShellManager.Create(_logger); + _powerShellManager = new PowerShellManager(_logger); _functionLoader = new FunctionLoader(); } @@ -63,15 +63,27 @@ internal async Task ProcessRequestLoop() internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request) { + StatusResult status = new StatusResult() + { + Status = StatusResult.Types.Status.Success + }; + + try + { + _powerShellManager.InitializeRunspace(); + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = e.ToRpcException(); + } + return new StreamingMessage() { RequestId = request.RequestId, WorkerInitResponse = new WorkerInitResponse() { - Result = new StatusResult() - { - Status = StatusResult.Types.Status.Success - } + Result = status } }; } @@ -143,8 +155,7 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request) try { result = _powerShellManager - .InvokeFunctionAndSetGlobalReturn(scriptPath, entryPoint, triggerMetadata, invocationRequest.InputData) - .ReturnBindingHashtable(functionInfo.OutputBindings); + .InvokeFunction(scriptPath, entryPoint, triggerMetadata, invocationRequest.InputData); } catch (Exception e) { From 221e088511198975c85c7ebb8d339bae56586ba5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 30 Aug 2018 15:17:44 -0700 Subject: [PATCH 5/8] Add new line to end --- examples/PSCoreApp/MyHttpTrigger/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 index 77a7c0b0..93ed1742 100644 --- a/examples/PSCoreApp/MyHttpTrigger/run.ps1 +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -23,4 +23,4 @@ $name Push-OutputBinding -Name res -Value ([HttpResponseContext]@{ Body = @{ Hello = $name } ContentType = 'application/json' -}) \ No newline at end of file +}) From 0097057557bae2106177543309e041ed2a7b6c12 Mon Sep 17 00:00:00 2001 From: "Tyler Leonhardt (POWERSHELL)" Date: Thu, 30 Aug 2018 16:14:03 -0700 Subject: [PATCH 6/8] use Import-Module and misc feedback --- src/PowerShell/PowerShellManager.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 94fc4e55..3b424d45 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -20,10 +20,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell internal class PowerShellManager { - readonly static string s_TriggerMetadataParameterName = "TriggerMetadata"; + private const string s_TriggerMetadataParameterName = "TriggerMetadata"; - RpcLogger _logger; - PowerShell _pwsh; + private RpcLogger _logger; + private PowerShell _pwsh; internal PowerShellManager(RpcLogger logger) { @@ -73,7 +73,8 @@ internal Hashtable InvokeFunction( if (entryPoint != "") { parameterMetadata = _pwsh - .AddScript($@". {scriptPath}") + .AddCommand("Import-Module") + .AddParameter("Name", scriptPath) .AddStatement() .AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint) .InvokeAndClearCommands()[0].Parameters; @@ -83,8 +84,9 @@ internal Hashtable InvokeFunction( } else { - parameterMetadata = _pwsh.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", scriptPath) + parameterMetadata = _pwsh.AddCommand("Get-Command").AddParameter("Name", scriptPath) .InvokeAndClearCommands()[0].Parameters; + _pwsh.AddCommand(scriptPath, useLocalScope: true); } } @@ -109,13 +111,13 @@ internal Hashtable InvokeFunction( Collection pipelineItems = _pwsh.InvokeAndClearCommands(); foreach (var psobject in pipelineItems) { - _logger.LogInformation($"FROM FUNCTION: {psobject.ToString()}"); + _logger.LogInformation($"OUTPUT: {psobject.ToString()}"); } returnObject = pipelineItems[pipelineItems.Count - 1]; } - var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding", useLocalScope: true) + var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding") .AddParameter("Purge") .InvokeAndClearCommands()[0]; @@ -127,14 +129,19 @@ internal Hashtable InvokeFunction( } finally { - ResetRunspace(); + ResetRunspace(scriptPath); } } - private void ResetRunspace() + private void ResetRunspace(string scriptPath) { // Reset the runspace to the Initial Session State _pwsh.Runspace.ResetRunspaceState(); + + // If the function had an entry point, this will remove the module that was loaded + var moduleName = Path.GetFileNameWithoutExtension(scriptPath); + _pwsh.AddCommand("Get-Module").AddParameter("Name", moduleName) + .AddCommand("Remove-Module").InvokeAndClearCommands(); } } } From 304278c532b0cb53507d5491419900d3f52a994c Mon Sep 17 00:00:00 2001 From: "Tyler Leonhardt (POWERSHELL)" Date: Thu, 30 Aug 2018 16:16:21 -0700 Subject: [PATCH 7/8] just set the module path --- src/PowerShell/PowerShellManager.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 3b424d45..ff1ff6fe 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -48,10 +48,8 @@ internal void InitializeRunspace() // HttpResponseContext without needing to specify the full namespace _pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").InvokeAndClearCommands(); - // Prepend the path to the internal Modules folder to the PSModulePath - var modulePath = Environment.GetEnvironmentVariable("PSModulePath"); - var additionalPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"); - Environment.SetEnvironmentVariable("PSModulePath", $"{additionalPath}{Path.PathSeparator}{modulePath}"); + // Set the PSModulePath + Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules")); } internal Hashtable InvokeFunction( From 71a0e0ec05ec0921e323d4861289db3fe21a1a57 Mon Sep 17 00:00:00 2001 From: "Tyler Leonhardt (POWERSHELL)" Date: Thu, 30 Aug 2018 16:57:26 -0700 Subject: [PATCH 8/8] fully qualified, no more useLocalScopes --- src/PowerShell/PowerShellManager.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index ff1ff6fe..1f30a760 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -20,7 +20,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell internal class PowerShellManager { - private const string s_TriggerMetadataParameterName = "TriggerMetadata"; + private const string _TriggerMetadataParameterName = "TriggerMetadata"; private RpcLogger _logger; private PowerShell _pwsh; @@ -71,21 +71,20 @@ internal Hashtable InvokeFunction( if (entryPoint != "") { parameterMetadata = _pwsh - .AddCommand("Import-Module") - .AddParameter("Name", scriptPath) + .AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", scriptPath) .AddStatement() - .AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint) + .AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", entryPoint) .InvokeAndClearCommands()[0].Parameters; - _pwsh.AddCommand(entryPoint, useLocalScope: true); + _pwsh.AddCommand(entryPoint); } else { - parameterMetadata = _pwsh.AddCommand("Get-Command").AddParameter("Name", scriptPath) + parameterMetadata = _pwsh.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", scriptPath) .InvokeAndClearCommands()[0].Parameters; - _pwsh.AddCommand(scriptPath, useLocalScope: true); + _pwsh.AddCommand(scriptPath); } } @@ -96,9 +95,9 @@ internal Hashtable InvokeFunction( } // Gives access to additional Trigger Metadata if the user specifies TriggerMetadata - if(parameterMetadata.ContainsKey(s_TriggerMetadataParameterName)) + if(parameterMetadata.ContainsKey(_TriggerMetadataParameterName)) { - _pwsh.AddParameter(s_TriggerMetadataParameterName, triggerMetadata); + _pwsh.AddParameter(_TriggerMetadataParameterName, triggerMetadata); _logger.LogDebug($"TriggerMetadata found. Value:{Environment.NewLine}{triggerMetadata.ToString()}"); } @@ -138,8 +137,10 @@ private void ResetRunspace(string scriptPath) // If the function had an entry point, this will remove the module that was loaded var moduleName = Path.GetFileNameWithoutExtension(scriptPath); - _pwsh.AddCommand("Get-Module").AddParameter("Name", moduleName) - .AddCommand("Remove-Module").InvokeAndClearCommands(); + _pwsh.AddCommand("Microsoft.PowerShell.Core\\Remove-Module") + .AddParameter("Name", moduleName) + .AddParameter("ErrorAction", "SilentlyContinue") + .InvokeAndClearCommands(); } } }