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 5 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;
}
}
}
148 changes: 47 additions & 101 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.

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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be const.


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,19 @@ 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();

// 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}");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw I could add one for the user as well:

myFuncApp/Modules:workerCode/Modules:normalPSModulePath

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, adding myFunApp/Modules makes sense.

}

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 +68,24 @@ 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
.AddScript($@". {scriptPath}")
.AddStatement()
.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running c# cmdlet, there is no need to run it in a local scope, as c# cmdlet doesn't create scope anyway.

.InvokeAndClearCommands<FunctionInfo>()[0].Parameters;

_pwsh.AddCommand(entryPoint, useLocalScope: true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use useLocalScope: true when you are not dot-sourcing. Please check all occurrences.


}
else
{
parameterMetadata = ExecuteScriptAndClearCommands<ExternalScriptInfo>($@"Get-Command {scriptPath}")[0].Parameters;
_pwsh.AddScript($@". {scriptPath} @args");
parameterMetadata = _pwsh.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", scriptPath)
.InvokeAndClearCommands<ExternalScriptInfo>()[0].Parameters;
_pwsh.AddCommand(scriptPath, useLocalScope: true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are not dot-sourcing the script, it will run in its script scope, which is a local scope, so no need to specify useLocalScope: true.

}
}

Expand All @@ -160,35 +102,39 @@ 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<PSObject> pipelineItems = _pwsh.InvokeAndClearCommands<PSObject>();
foreach (var psobject in pipelineItems)
{
_logger.LogInformation($"FROM FUNCTION: {psobject.ToString()}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about OUTPUT: as the prefix?

}

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

var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding", useLocalScope: true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cmdlet will run in local scope by default when not dot-sourcing, so useLocalScope: true is not needed.

.AddParameter("Purge")
.InvokeAndClearCommands<Hashtable>()[0];

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

public Hashtable ReturnBindingHashtable(IDictionary<string, BindingInfo> outBindings)
private void ResetRunspace()
{
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();
}
}
}
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