From e395476fea7d3e9ed784b2c1314faee7b7f3bd84 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 23 Aug 2018 19:25:45 -0700 Subject: [PATCH] added a bunch of comments --- .../PSCoreApp/MyHttpTrigger/function.json | 3 +- examples/PSCoreApp/MyHttpTrigger/run.ps1 | 16 ++- .../FunctionMessagingClient.cs | 3 + .../Function/FunctionInfo.cs | 4 +- .../Http/HttpRequestContext.cs | 5 - .../Http/HttpResponseContext.cs | 75 +--------- .../Host/{Host.cs => AzureFunctionsHost.cs} | 18 +-- .../PowerShell/Host/HostUserInterface.cs | 19 +-- .../PowerShell/PowerShellWorkerExtensions.cs | 131 ++++++++++++++++++ .../Requests/HandleFunctionLoadRequest.cs | 28 +++- .../Requests/HandleInvocationRequest.cs | 114 ++++----------- .../Requests/HandleWorkerInitRequest.cs | 3 +- .../StartupArguments.cs | 6 - .../Utility/RpcLogger.cs | 2 +- .../{TypeConverter.cs => TypeExtensions.cs} | 42 ++++-- .../Worker.cs | 22 ++- 16 files changed, 254 insertions(+), 237 deletions(-) rename src/Azure.Functions.PowerShell.Worker/PowerShell/Host/{Host.cs => AzureFunctionsHost.cs} (90%) create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs rename src/Azure.Functions.PowerShell.Worker/Utility/{TypeConverter.cs => TypeExtensions.cs} (69%) diff --git a/examples/PSCoreApp/MyHttpTrigger/function.json b/examples/PSCoreApp/MyHttpTrigger/function.json index 9fc9b08..18ab7de 100644 --- a/examples/PSCoreApp/MyHttpTrigger/function.json +++ b/examples/PSCoreApp/MyHttpTrigger/function.json @@ -1,6 +1,5 @@ { "disabled": false, - "entryPoint":"FunctionName", "bindings": [ { "authLevel": "function", @@ -15,7 +14,7 @@ { "type": "http", "direction": "out", - "name": "$return" + "name": "res" } ] } \ No newline at end of file diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 index 9271bc3..121371d 100644 --- a/examples/PSCoreApp/MyHttpTrigger/run.ps1 +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -1,6 +1,12 @@ -function FunctionName { - $global:res = $req.GetHttpResponseContext() - "hello verbose" - $res.Json('{"Hello":"World"}') - $res.SetHeader("foo", "bar") +$name = 'World' +if($req.Query.Name) { + $name = $req.Query.Name +} + +Write-Verbose "Hello $name" -Verbose +Write-Warning "Warning $name" + +$res = [HttpResponseContext]@{ + Body = @{ Hello = $name } + ContentType = 'application/json' } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index 4f57638..3aeb5e0 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -21,6 +21,9 @@ namespace Azure.Functions.PowerShell.Worker.Messaging public async Task WriteAsync(StreamingMessage message) { if(isDisposed) return; + + // Wait for the handle to be released because we can't have + // more than one message being sent at the same time await _writeStreamHandle.WaitAsync(); try { diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs index d16253b..617f088 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -24,12 +24,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { Bindings.Add(binding.Key, binding.Value); + // Only add Out and InOut bindings to the OutputBindings if (binding.Value.Direction != BindingInfo.Types.Direction.In) { if(binding.Value.Type == "http") - { - HttpOutputName = binding.Key; - }if(binding.Value.Type == "http") { HttpOutputName = binding.Key; } diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index ffa5565..aef2183 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -13,10 +13,5 @@ namespace Microsoft.Azure.Functions.PowerShellWorker public MapField Params {get; set;} public object Body {get; set;} public object RawBody {get; set;} - - public HttpResponseContext GetHttpResponseContext() - { - return new HttpResponseContext(); - } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 60ac368..5eef4a3 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -1,3 +1,4 @@ +using System.Collections; using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -5,78 +6,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class HttpResponseContext { -#region properties public string StatusCode {get; set;} = "200"; - public MapField Headers {get; set;} = new MapField(); - public TypedData Body {get; set;} = new TypedData { String = "" }; + public Hashtable Headers {get; set;} = new Hashtable(); + public object Body {get; set;} + public string ContentType {get; set;} = "text/plain"; public bool EnableContentNegotiation {get; set;} = false; -#endregion -#region Helper functions for user to use to set data - public HttpResponseContext Header(string field, string value) => - SetHeader(field, value); - public HttpResponseContext SetHeader(string field, string value) - { - Headers.Add(field, value); - return this; - } - - public string GetHeader(string field) => - Headers[field]; - - public HttpResponseContext RemoveHeader(string field) - { - Headers.Remove(field); - return this; - } - - public HttpResponseContext Status(int statusCode) => - SetStatus(statusCode); - public HttpResponseContext Status(string statusCode) => - SetStatus(statusCode); - public HttpResponseContext SetStatus(int statusCode) => - SetStatus(statusCode); - public HttpResponseContext SetStatus(string statusCode) - { - StatusCode = statusCode; - return this; - } - - public HttpResponseContext Type(string type) => - SetHeader("content-type", type); - public HttpResponseContext SetContentType(string type) => - SetHeader("content-type", type); - - public HttpResponseContext Send(int val) - { - Body = new TypedData - { - Int = val - }; - return this; - } - public HttpResponseContext Send(double val) - { - Body = new TypedData - { - Double = val - }; - return this; - } - public HttpResponseContext Send(string val) - { - Body = new TypedData - { - String = val - }; - return this; - } - public HttpResponseContext Json(string val) { - Body = new TypedData - { - Json = val - }; - return Type("application/json"); - } -#endregion } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs similarity index 90% rename from src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs rename to src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index 829c92a..d09eaac 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -10,8 +10,11 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// applications. Not all members are implemented. Those that aren't throw a /// NotImplementedException. /// - internal class Host : PSHost + internal class AzureFunctionsHost : PSHost { + /// + /// The private reference of the logger. + /// private RpcLogger _logger; /// @@ -80,15 +83,14 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// public override Version Version => new Version(1, 0, 0, 0); - public Host(RpcLogger logger) + public AzureFunctionsHost(RpcLogger logger) { _logger = logger; - HostUI = new HostUserInterface(logger); } /// - /// Not implemented by this example class. The call fails with an exception. + /// Not implemented by this class. The call fails with an exception. /// public override void EnterNestedPrompt() { @@ -96,7 +98,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host } /// - /// Not implemented by this example class. The call fails with an exception. + /// Not implemented by this class. The call fails with an exception. /// public override void ExitNestedPrompt() { @@ -106,7 +108,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// /// This API is called before an external application process is started. Typically /// it's used to save state that the child process may alter so the parent can - /// restore that state when the child exits. In this sample, we don't need this so + /// restore that state when the child exits. In this, we don't need this so /// the method simple returns. /// public override void NotifyBeginApplication() @@ -116,8 +118,8 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// /// This API is called after an external application process finishes. Typically - /// it's used to restore state that the child process may have altered. In this - /// sample, we don't need this so the method simple returns. + /// it's used to restore state that the child process may have altered. In this, + /// we don't need this so the method simple returns. /// public override void NotifyEndApplication() { diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs index 86565ce..b017348 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -15,6 +15,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// internal class HostUserInterface : PSHostUserInterface { + /// + /// The private reference of the logger. + /// private RpcLogger _logger; /// @@ -40,7 +43,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// The text of the prompt. /// A collection of FieldDescription objects that /// describe each field of the prompt. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) { throw new NotImplementedException("The method or operation is not implemented."); @@ -55,7 +58,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// each choice. /// The index of the label in the Choices parameter /// collection. To indicate no default choice, set to -1. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) { throw new NotImplementedException("The method or operation is not implemented."); @@ -69,7 +72,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// The text of the message. /// The user name whose credential is to be prompted for. /// The name of the target for which the credential is collected. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) { throw new NotImplementedException("The method or operation is not implemented."); @@ -88,7 +91,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// identifies the type of credentials that can be returned. /// A PSCredentialUIOptions constant that identifies the UI /// behavior when it gathers the credentials. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) { throw new NotImplementedException("The method or operation is not implemented."); @@ -98,7 +101,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// Reads characters that are entered by the user until a newline /// (carriage return) is encountered. /// - /// The characters that are entered by the user. + /// Throws a NotImplemented exception because we are in a non-interactive experience. public override string ReadLine() { throw new NotImplementedException("The method or operation is not implemented."); @@ -108,7 +111,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// Reads characters entered by the user until a newline (carriage return) /// is encountered and returns the characters as a secure string. /// - /// Throws a NotImplemented exception. + /// Throws a NotImplemented exception because we are in a non-interactive experience. public override System.Security.SecureString ReadLineAsSecureString() { throw new NotImplementedException("The method or operation is not implemented."); @@ -161,7 +164,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// public override void WriteLine() { - //do nothing + //do nothing because we don't need to log empty lines } /// @@ -203,7 +206,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// The verbose message that is displayed. public override void WriteVerboseLine(string message) { - //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); } @@ -213,7 +215,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// The warning message that is displayed. public override void WriteWarningLine(string message) { - //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); _logger.LogWarning(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); } } diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs new file mode 100644 index 0000000..a1ffbcb --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell +{ + using System.Management.Automation; + + public static class PowerShellWorkerExtensions + { + // 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. + + private static string s_LogAndSetReturnValueScript = @" +param([Parameter(ValueFromPipeline=$true)]$return) + +$return | Out-Default + +Set-Variable -Name '$return' -Value $return -Scope global +"; + + public static PowerShell SetGlobalVariables(this PowerShell ps, Hashtable triggerMetadata, IList inputData) + { + try { + // Set the global $Context variable which contains trigger metadata + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", "Context"}, + { "Scope", "Global"}, + { "Value", triggerMetadata} + }).Invoke(); + + // Sets a global variable for each input binding + foreach (ParameterBinding binding in inputData) + { + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", binding.Name}, + { "Scope", "Global"}, + { "Value", binding.Data.ToObject()} + }).Invoke(); + } + return ps; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + public static PowerShell InvokeFunctionAndSetGlobalReturn(this PowerShell ps, string scriptPath, string entryPoint) + { + try + { + // We need to take into account if the user has an entry point. + // If it does, we invoke the command of that name + if(entryPoint != "") + { + ps.AddScript($@". {scriptPath}").Invoke(); + ps.AddScript($@". {entryPoint}"); + } + else + { + ps.AddScript($@". {scriptPath}"); + } + + // This script handles when the user adds something to the pipeline. + ps.AddScript(s_LogAndSetReturnValueScript).Invoke(); + return ps; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + public static Hashtable ReturnBindingHashtable(this PowerShell ps, IDictionary outBindings) + { + try + { + // This script returns a hashtable that contains the + // output bindings that we will return to the function host. + var result = ps.AddScript(BuildBindingHashtableScript(outBindings)).Invoke()[0]; + ps.Commands.Clear(); + return result; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + private 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) + { + 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); + } + script.AppendLine("}"); + + return script.ToString(); + } + + // TODO: make sure this completely cleans up the runspace + private static void CleanupRunspace(this PowerShell ps) + { + ps.Commands.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs index 5eb18f9..7c67b63 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.Azure.Functions.PowerShellWorker.Requests { + using System; using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Utility; @@ -15,20 +16,33 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests RpcLogger logger) { FunctionLoadRequest functionLoadRequest = request.FunctionLoadRequest; - functionLoader.Load(functionLoadRequest.FunctionId, functionLoadRequest.Metadata); - var response = new StreamingMessage() + + // Assume success unless something bad happens + StatusResult status = new StatusResult() + { + Status = StatusResult.Types.Status.Success + }; + + // Try to load the functions + try + { + functionLoader.Load(functionLoadRequest.FunctionId, functionLoadRequest.Metadata); + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = e.ToRpcException(); + } + + return new StreamingMessage() { RequestId = request.RequestId, FunctionLoadResponse = new FunctionLoadResponse() { FunctionId = functionLoadRequest.FunctionId, - Result = new StatusResult() - { - Status = StatusResult.Types.Status.Success - } + Result = status } }; - return response; } } } \ 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 index 02cdbfd..5c424b5 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.Functions.PowerShellWorker.Requests @@ -20,8 +21,22 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests 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() { @@ -33,113 +48,34 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests } }; - var info = functionLoader.GetInfo(invocationRequest.FunctionId); - - // Add $Context variable, which contains trigger metadata, to the Global scope - Hashtable triggerMetadata = new Hashtable(); - foreach (var dataItem in invocationRequest.TriggerMetadata) - { - triggerMetadata.Add(dataItem.Key, TypeConverter.FromTypedData(dataItem.Value)); - } - - if (triggerMetadata.Count > 0) - { - powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", "Context"}, - { "Scope", "Global"}, - { "Value", triggerMetadata} - }); - powershell.Invoke(); - } - - foreach (ParameterBinding binding in invocationRequest.InputData) - { - powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", binding.Name}, - { "Scope", "Global"}, - { "Value", TypeConverter.FromTypedData(binding.Data)} - }); - powershell.Invoke(); - } - - // foreach (KeyValuePair binding in info.OutputBindings) - // { - // powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - // { "Name", binding.Key}, - // { "Scope", "Global"}, - // { "Value", null} - // }); - // powershell.Invoke(); - // } - - (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); - - if(entryPoint != "") - { - powershell.AddScript($@". {scriptPath}"); - powershell.Invoke(); - powershell.AddCommand(entryPoint); - } - else - { - powershell.AddCommand(scriptPath); - } - - powershell.AddScript(@" -param([Parameter(ValueFromPipeline=$true)]$return) - -$return | Out-Default - -Set-Variable -Name '$return' -Value $return -Scope global -"); - - StringBuilder script = new StringBuilder(); - script.AppendLine("@{"); - foreach (KeyValuePair binding in info.OutputBindings) - { - 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); - } - script.AppendLine("}"); - + // Invoke powershell logic and return hashtable of out binding data Hashtable result = null; try { - powershell.Invoke(); - powershell.AddScript(script.ToString()); - result = powershell.Invoke()[0]; + result = powershell + .SetGlobalVariables(triggerMetadata, invocationRequest.InputData) + .InvokeFunctionAndSetGlobalReturn(scriptPath, entryPoint) + .ReturnBindingHashtable(functionInfo.OutputBindings); } catch (Exception e) { status.Status = StatusResult.Types.Status.Failure; - status.Exception = TypeConverter.ToRpcException(e); - powershell.Commands.Clear(); + status.Exception = e.ToRpcException(); return response; } - powershell.Commands.Clear(); - foreach (KeyValuePair binding in info.OutputBindings) + // Set out binding data and return response to be sent back to host + foreach (KeyValuePair binding in functionInfo.OutputBindings) { ParameterBinding paramBinding = new ParameterBinding() { Name = binding.Key, - Data = TypeConverter.ToTypedData( - result[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; diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs index b5d21cd..70ddfc9 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs @@ -14,7 +14,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests StreamingMessage request, RpcLogger logger) { - var response = new StreamingMessage() + return new StreamingMessage() { RequestId = request.RequestId, WorkerInitResponse = new WorkerInitResponse() @@ -25,7 +25,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests } } }; - return response; } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs index c8f9976..91738ee 100644 --- a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -27,12 +27,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker } } - Console.WriteLine($"host: {arguments.Host}"); - Console.WriteLine($"port: {arguments.Port}"); - Console.WriteLine($"workerId: {arguments.WorkerId}"); - Console.WriteLine($"requestId: {arguments.RequestId}"); - Console.WriteLine($"grpcMaxMessageLength: {arguments.GrpcMaxMessageLength}"); - return arguments; } } diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs index 497ef5b..c03941e 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -42,7 +42,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility RequestId = _requestId, RpcLog = new RpcLog() { - Exception = exception == null ? null : TypeConverter.ToRpcException(exception), + Exception = exception == null ? null : exception.ToRpcException(), InvocationId = _invocationId, Level = ConvertLogLevel(logLevel), Message = formatter(state, exception) diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs similarity index 69% rename from src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs rename to src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs index a83da13..0a48cea 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -7,24 +7,29 @@ using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.TypedData; using System; using Newtonsoft.Json; using System.Collections; +using System.Collections.Generic; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { - public class TypeConverter + public static class TypeExtensions { - public static object ToObject (TypedData data) + public static object ToObject (this TypedData data) { + if (data == null) + { + return null; + } + switch (data.DataCase) { case DataOneofCase.Json: - // consider doing ConvertFrom-Json - return data.Json; + return JsonConvert.DeserializeObject(data.Json); case DataOneofCase.Bytes: return data.Bytes; case DataOneofCase.Double: return data.Double; case DataOneofCase.Http: - return ToHttpContext(data.Http); + return data.Http.ToHttpContext(); case DataOneofCase.Int: return data.Int; case DataOneofCase.Stream: @@ -38,7 +43,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility } } - public static TypedData ToTypedData(object value) + public static TypedData ToTypedData(this object value) { TypedData typedData = new TypedData(); @@ -55,7 +60,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility else if(LanguagePrimitives.TryConvertTo( value, out HttpResponseContext http)) { - typedData.Http = ToRpcHttp(http); + typedData.Http = http.ToRpcHttp(); } else if (LanguagePrimitives.TryConvertTo( value, out Hashtable hashtable)) @@ -65,6 +70,8 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility else if (LanguagePrimitives.TryConvertTo( value, out string str)) { + // Attempt to parse the string into json. If it fails, + // fallback to storing as a string try { typedData.Json = JsonConvert.SerializeObject(str); @@ -77,7 +84,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility return typedData; } - public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) + public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) { var httpRequestContext = new HttpRequestContext { @@ -91,35 +98,40 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility if (rpcHttp.Body != null) { - httpRequestContext.Body = ToObject(rpcHttp.Body); + httpRequestContext.Body = rpcHttp.Body.ToObject(); } if (rpcHttp.RawBody != null) { - httpRequestContext.Body = ToObject(rpcHttp.RawBody); + httpRequestContext.Body = rpcHttp.RawBody.ToObject(); } return httpRequestContext; } - public static RpcHttp ToRpcHttp (HttpResponseContext httpResponseContext) + public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) { var rpcHttp = new RpcHttp { - StatusCode = httpResponseContext.StatusCode?? "200" + StatusCode = httpResponseContext.StatusCode }; if (httpResponseContext.Body != null) { - rpcHttp.Body = httpResponseContext.Body; + rpcHttp.Body = httpResponseContext.Body.ToTypedData(); } - rpcHttp.Headers.Add(httpResponseContext.Headers); + // Add all the headers. ContentType is separated for convenience + foreach (DictionaryEntry item in httpResponseContext.Headers) + { + rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString()); + } + rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); return rpcHttp; } - public static RpcException ToRpcException (Exception exception) + public static RpcException ToRpcException (this Exception exception) { return new RpcException { diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 6f5ea00..110451b 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -9,7 +9,6 @@ using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Azure.Functions.PowerShell.Worker.Messaging; using Microsoft.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; -using System.Collections; using Microsoft.Azure.Functions.PowerShellWorker.Requests; using Microsoft.Extensions.Logging; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; @@ -33,10 +32,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker } StartupArguments startupArguments = StartupArguments.Parse(args); + // Initialize Rpc client, logger, and PowerShell s_client = new FunctionMessagingClient(startupArguments.Host, startupArguments.Port); s_Logger = new RpcLogger(s_client); InitPowerShell(); + // Send StartStream message var streamingMessage = new StreamingMessage() { RequestId = startupArguments.RequestId, StartStream = new StartStream() { WorkerId = startupArguments.WorkerId } @@ -49,28 +50,21 @@ namespace Microsoft.Azure.Functions.PowerShellWorker private static void InitPowerShell() { - // var events = new StreamEvents(s_Logger); - var host = new Host(s_Logger); + var host = new AzureFunctionsHost(s_Logger); s_runspace = RunspaceFactory.CreateRunspace(host); s_runspace.Open(); s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault()); s_ps.Runspace = s_runspace; - // Setup Stream event listeners - // s_ps.Streams.Debug.DataAdded += events.DebugDataAdded; - // s_ps.Streams.Error.DataAdded += events.ErrorDataAdded; - // s_ps.Streams.Information.DataAdded += events.InformationDataAdded; - // s_ps.Streams.Progress.DataAdded += events.ProgressDataAdded; - // s_ps.Streams.Verbose.DataAdded += events.VerboseDataAdded; - // s_ps.Streams.Warning.DataAdded += events.WarningDataAdded; - s_ps.AddScript("$PSHOME"); //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); - var result = s_ps.Invoke(); - s_ps.Commands.Clear(); + s_ps.Invoke(); - Console.WriteLine(result[0]); + // Add HttpResponseContext namespace so users can reference + // HttpResponseContext without needing to specify the full namespace + s_ps.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").Invoke(); + s_ps.Commands.Clear(); } private static async Task ProcessEvent()