Skip to content

Switch to module approach #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
12 changes: 5 additions & 7 deletions examples/PSCoreApp/MyHttpTrigger/run.ps1
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ function Get-OutputBinding {
param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[string[]]
$Name = '*'
$Name = '*',

[switch]
$Purge
)
begin {
$bindings = @{}
Expand All @@ -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
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/PowerShell/PowerShellExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<T> InvokeAndClearCommands<T>(this PowerShell pwsh)
{
var result = pwsh.Invoke<T>();
pwsh.Commands.Clear();
return result;
}
}
}
166 changes: 59 additions & 107 deletions src/PowerShell/PowerShellManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.IO;

using Microsoft.Azure.Functions.PowerShellWorker.Utility;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
Expand All @@ -20,33 +20,16 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell

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.
private const string _TriggerMetadataParameterName = "TriggerMetadata";

readonly static string s_LogAndSetReturnValueScript = @"
param([Parameter(ValueFromPipeline=$true)]$return)
private RpcLogger _logger;
private PowerShell _pwsh;

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";

RpcLogger _logger;
PowerShell _pwsh;

PowerShellManager(RpcLogger logger)
internal PowerShellManager(RpcLogger logger)
{
_pwsh = PowerShell.Create(InitialSessionState.CreateDefault());
var initialSessionState = InitialSessionState.CreateDefault();
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
_pwsh = PowerShell.Create(initialSessionState);
_logger = logger;

// Setup Stream event listeners
Expand All @@ -59,64 +42,17 @@ internal class PowerShellManager
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
}

public static PowerShellManager Create(RpcLogger logger)
{
var manager = new PowerShellManager(logger);

internal void InitializeRunspace()
{
// 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<string, BindingInfo> 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<string, BindingInfo> 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();
}

void ResetRunspace()
{
// Reset the runspace to the Initial Session State
_pwsh.Runspace.ResetRunspaceState();
_pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").InvokeAndClearCommands();

// Set the PSModulePath
Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"));
}

void ExecuteScriptAndClearCommands(string script)
{
_pwsh.AddScript(script).Invoke();
_pwsh.Commands.Clear();
}

public Collection<T> ExecuteScriptAndClearCommands<T>(string script)
{
var result = _pwsh.AddScript(script).Invoke<T>();
_pwsh.Commands.Clear();
return result;
}

public PowerShellManager InvokeFunctionAndSetGlobalReturn(
internal Hashtable InvokeFunction(
string scriptPath,
string entryPoint,
Hashtable triggerMetadata,
Expand All @@ -130,20 +66,25 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn(
// 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 != "")
{
ExecuteScriptAndClearCommands($@". {scriptPath}");
parameterMetadata = ExecuteScriptAndClearCommands<FunctionInfo>($@"Get-Command {entryPoint}")[0].Parameters;
_pwsh.AddScript($@". {entryPoint} @args");
parameterMetadata = _pwsh
.AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", scriptPath)
.AddStatement()
.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", entryPoint)
.InvokeAndClearCommands<FunctionInfo>()[0].Parameters;

_pwsh.AddCommand(entryPoint);

}
else
{
parameterMetadata = ExecuteScriptAndClearCommands<ExternalScriptInfo>($@"Get-Command {scriptPath}")[0].Parameters;
_pwsh.AddScript($@". {scriptPath} @args");
parameterMetadata = _pwsh.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", scriptPath)
.InvokeAndClearCommands<ExternalScriptInfo>()[0].Parameters;

_pwsh.AddCommand(scriptPath);
}
}

Expand All @@ -154,41 +95,52 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn(
}

// 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()}");
}

// 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<PSObject> pipelineItems = _pwsh.InvokeAndClearCommands<PSObject>();
foreach (var psobject in pipelineItems)
{
_logger.LogInformation($"OUTPUT: {psobject.ToString()}");
}

returnObject = pipelineItems[pipelineItems.Count - 1];
}

var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding")
.AddParameter("Purge")
.InvokeAndClearCommands<Hashtable>()[0];

if(returnObject != null)
{
result.Add("$return", returnObject);
}
return this;
return result;
}
catch(Exception e)
finally
{
ResetRunspace();
throw e;
ResetRunspace(scriptPath);
}
}

public Hashtable ReturnBindingHashtable(IDictionary<string, BindingInfo> outBindings)
private void ResetRunspace(string scriptPath)
{
try
{
// This script returns a hashtable that contains the
// output bindings that we will return to the function host.
var result = ExecuteScriptAndClearCommands<Hashtable>(BuildBindingHashtableScript(outBindings))[0];
ResetRunspace();
return result;
}
catch(Exception e)
{
ResetRunspace();
throw e;
}
// 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("Microsoft.PowerShell.Core\\Remove-Module")
.AddParameter("Name", moduleName)
.AddParameter("ErrorAction", "SilentlyContinue")
.InvokeAndClearCommands();
}
}
}
25 changes: 18 additions & 7 deletions src/RequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
}
};
}
Expand Down Expand Up @@ -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)
{
Expand Down