Skip to content

Commit 6583e49

Browse files
Switch to module approach (#20)
* Switch to module approach * misc comment updates * resolve rebase conflicts * address dongbo's feedback * Add new line to end * use Import-Module and misc feedback * just set the module path * fully qualified, no more useLocalScopes
1 parent b6b8e0d commit 6583e49

File tree

6 files changed

+118
-124
lines changed

6 files changed

+118
-124
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Prototype for Azure Functions PowerShell Language Worker
1111
```powershell
1212
# Windows if you installed the Azure Functions Core Tools via npm
1313
Remove-Item -Recurse -Force ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell
14-
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
14+
Copy-Item src\bin\Debug\netcoreapp2.1\publish ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell -Recurse -Force
1515
1616
# macOS if you installed the Azure Functions Core Tools via brew
1717
Remove-Item -Recurse -Force /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell
18-
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
18+
Copy-Item src/bin/Debug/netcoreapp2.1/publish /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell -Recurse -Force
1919
```

examples/PSCoreApp/MyHttpTrigger/run.ps1

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
param($req, $TriggerMetadata)
2-
3-
# Write-Host $TriggerMetadata["Name"]
4-
51
# Invoked with Invoke-RestMethod:
62
# irm http://localhost:7071/api/MyHttpTrigger?Name=Tyler
7-
# Input bindings are added to the scope of the script: ex. `$req`
3+
# Input bindings are added via param block
4+
5+
param($req, $TriggerMetadata)
86

97
# If no name was passed by query parameter
108
$name = 'World'
@@ -22,7 +20,7 @@ Write-Warning "Warning $name"
2220
$name
2321

2422
# You set the value of your output bindings by assignment `$nameOfOutputBinding = 'foo'`
25-
$res = [HttpResponseContext]@{
23+
Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
2624
Body = @{ Hello = $name }
2725
ContentType = 'application/json'
28-
}
26+
})

src/Modules/Azure.Functions.PowerShell.Worker.Module/Azure.Functions.PowerShell.Worker.Module.psm1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ function Get-OutputBinding {
3030
param(
3131
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
3232
[string[]]
33-
$Name = '*'
33+
$Name = '*',
34+
35+
[switch]
36+
$Purge
3437
)
3538
begin {
3639
$bindings = @{}
@@ -39,6 +42,9 @@ function Get-OutputBinding {
3942
$script:_OutputBindings.GetEnumerator() | Where-Object Name -Like $Name | ForEach-Object { $null = $bindings.Add($_.Name, $_.Value) }
4043
}
4144
end {
45+
if($Purge.IsPresent) {
46+
$script:_OutputBindings.Clear()
47+
}
4248
return $bindings
4349
}
4450
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System.Collections.ObjectModel;
7+
8+
namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
9+
{
10+
using System.Management.Automation;
11+
12+
internal static class PowerShellExtensions
13+
{
14+
public static void InvokeAndClearCommands(this PowerShell pwsh)
15+
{
16+
pwsh.Invoke();
17+
pwsh.Commands.Clear();
18+
}
19+
20+
public static Collection<T> InvokeAndClearCommands<T>(this PowerShell pwsh)
21+
{
22+
var result = pwsh.Invoke<T>();
23+
pwsh.Commands.Clear();
24+
return result;
25+
}
26+
}
27+
}

src/PowerShell/PowerShellManager.cs

Lines changed: 59 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using System.Collections;
88
using System.Collections.Generic;
99
using System.Collections.ObjectModel;
10-
using System.Text;
10+
using System.IO;
1111

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

2121
internal class PowerShellManager
2222
{
23-
// This script handles when the user adds something to the pipeline.
24-
// It logs the item that comes and stores it as the $return out binding.
25-
// The last item stored as $return will be returned to the function host.
23+
private const string _TriggerMetadataParameterName = "TriggerMetadata";
2624

27-
readonly static string s_LogAndSetReturnValueScript = @"
28-
param([Parameter(ValueFromPipeline=$true)]$return)
25+
private RpcLogger _logger;
26+
private PowerShell _pwsh;
2927

30-
Write-Information $return
31-
32-
Set-Variable -Name '$return' -Value $return -Scope global
33-
";
34-
35-
readonly static string s_SetExecutionPolicyOnWindowsScript = @"
36-
if ($IsWindows)
37-
{
38-
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process
39-
}
40-
";
41-
42-
readonly static string s_TriggerMetadataParameterName = "TriggerMetadata";
43-
44-
RpcLogger _logger;
45-
PowerShell _pwsh;
46-
47-
PowerShellManager(RpcLogger logger)
28+
internal PowerShellManager(RpcLogger logger)
4829
{
49-
_pwsh = PowerShell.Create(InitialSessionState.CreateDefault());
30+
var initialSessionState = InitialSessionState.CreateDefault();
31+
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
32+
_pwsh = PowerShell.Create(initialSessionState);
5033
_logger = logger;
5134

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

62-
public static PowerShellManager Create(RpcLogger logger)
63-
{
64-
var manager = new PowerShellManager(logger);
65-
45+
internal void InitializeRunspace()
46+
{
6647
// Add HttpResponseContext namespace so users can reference
6748
// HttpResponseContext without needing to specify the full namespace
68-
manager.ExecuteScriptAndClearCommands($"using namespace {typeof(HttpResponseContext).Namespace}");
69-
manager.ExecuteScriptAndClearCommands(s_SetExecutionPolicyOnWindowsScript);
70-
return manager;
71-
}
72-
73-
static string BuildBindingHashtableScript(IDictionary<string, BindingInfo> outBindings)
74-
{
75-
// Since all of the out bindings are stored in variables at this point,
76-
// we must construct a script that will return those output bindings in a hashtable
77-
StringBuilder script = new StringBuilder();
78-
script.AppendLine("@{");
79-
foreach (KeyValuePair<string, BindingInfo> binding in outBindings)
80-
{
81-
script.Append("'");
82-
script.Append(binding.Key);
83-
84-
// since $return has a dollar sign, we have to treat it differently
85-
if (binding.Key == "$return")
86-
{
87-
script.Append("' = ");
88-
}
89-
else
90-
{
91-
script.Append("' = $");
92-
}
93-
script.AppendLine(binding.Key);
94-
}
95-
script.AppendLine("}");
96-
97-
return script.ToString();
98-
}
99-
100-
void ResetRunspace()
101-
{
102-
// Reset the runspace to the Initial Session State
103-
_pwsh.Runspace.ResetRunspaceState();
49+
_pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").InvokeAndClearCommands();
50+
51+
// Set the PSModulePath
52+
Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"));
10453
}
10554

106-
void ExecuteScriptAndClearCommands(string script)
107-
{
108-
_pwsh.AddScript(script).Invoke();
109-
_pwsh.Commands.Clear();
110-
}
111-
112-
public Collection<T> ExecuteScriptAndClearCommands<T>(string script)
113-
{
114-
var result = _pwsh.AddScript(script).Invoke<T>();
115-
_pwsh.Commands.Clear();
116-
return result;
117-
}
118-
119-
public PowerShellManager InvokeFunctionAndSetGlobalReturn(
55+
internal Hashtable InvokeFunction(
12056
string scriptPath,
12157
string entryPoint,
12258
Hashtable triggerMetadata,
@@ -130,20 +66,25 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn(
13066
// If it does, we invoke the command of that name. We also need to fetch
13167
// the ParameterMetadata so that we can tell whether or not the user is asking
13268
// for the $TriggerMetadata
133-
13469
using (ExecutionTimer.Start(_logger, "Parameter metadata retrieved."))
13570
{
13671
if (entryPoint != "")
13772
{
138-
ExecuteScriptAndClearCommands($@". {scriptPath}");
139-
parameterMetadata = ExecuteScriptAndClearCommands<FunctionInfo>($@"Get-Command {entryPoint}")[0].Parameters;
140-
_pwsh.AddScript($@". {entryPoint} @args");
73+
parameterMetadata = _pwsh
74+
.AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", scriptPath)
75+
.AddStatement()
76+
.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", entryPoint)
77+
.InvokeAndClearCommands<FunctionInfo>()[0].Parameters;
78+
79+
_pwsh.AddCommand(entryPoint);
14180

14281
}
14382
else
14483
{
145-
parameterMetadata = ExecuteScriptAndClearCommands<ExternalScriptInfo>($@"Get-Command {scriptPath}")[0].Parameters;
146-
_pwsh.AddScript($@". {scriptPath} @args");
84+
parameterMetadata = _pwsh.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", scriptPath)
85+
.InvokeAndClearCommands<ExternalScriptInfo>()[0].Parameters;
86+
87+
_pwsh.AddCommand(scriptPath);
14788
}
14889
}
14990

@@ -154,41 +95,52 @@ public PowerShellManager InvokeFunctionAndSetGlobalReturn(
15495
}
15596

15697
// Gives access to additional Trigger Metadata if the user specifies TriggerMetadata
157-
if(parameterMetadata.ContainsKey(s_TriggerMetadataParameterName))
98+
if(parameterMetadata.ContainsKey(_TriggerMetadataParameterName))
15899
{
159-
_pwsh.AddParameter(s_TriggerMetadataParameterName, triggerMetadata);
100+
_pwsh.AddParameter(_TriggerMetadataParameterName, triggerMetadata);
160101
_logger.LogDebug($"TriggerMetadata found. Value:{Environment.NewLine}{triggerMetadata.ToString()}");
161102
}
162103

163-
// This script handles when the user adds something to the pipeline.
104+
PSObject returnObject = null;
164105
using (ExecutionTimer.Start(_logger, "Execution of the user's function completed."))
165106
{
166-
ExecuteScriptAndClearCommands(s_LogAndSetReturnValueScript);
107+
// Log everything we received from the pipeline and set the last one to be the ReturnObject
108+
Collection<PSObject> pipelineItems = _pwsh.InvokeAndClearCommands<PSObject>();
109+
foreach (var psobject in pipelineItems)
110+
{
111+
_logger.LogInformation($"OUTPUT: {psobject.ToString()}");
112+
}
113+
114+
returnObject = pipelineItems[pipelineItems.Count - 1];
115+
}
116+
117+
var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding")
118+
.AddParameter("Purge")
119+
.InvokeAndClearCommands<Hashtable>()[0];
120+
121+
if(returnObject != null)
122+
{
123+
result.Add("$return", returnObject);
167124
}
168-
return this;
125+
return result;
169126
}
170-
catch(Exception e)
127+
finally
171128
{
172-
ResetRunspace();
173-
throw e;
129+
ResetRunspace(scriptPath);
174130
}
175131
}
176132

177-
public Hashtable ReturnBindingHashtable(IDictionary<string, BindingInfo> outBindings)
133+
private void ResetRunspace(string scriptPath)
178134
{
179-
try
180-
{
181-
// This script returns a hashtable that contains the
182-
// output bindings that we will return to the function host.
183-
var result = ExecuteScriptAndClearCommands<Hashtable>(BuildBindingHashtableScript(outBindings))[0];
184-
ResetRunspace();
185-
return result;
186-
}
187-
catch(Exception e)
188-
{
189-
ResetRunspace();
190-
throw e;
191-
}
135+
// Reset the runspace to the Initial Session State
136+
_pwsh.Runspace.ResetRunspaceState();
137+
138+
// If the function had an entry point, this will remove the module that was loaded
139+
var moduleName = Path.GetFileNameWithoutExtension(scriptPath);
140+
_pwsh.AddCommand("Microsoft.PowerShell.Core\\Remove-Module")
141+
.AddParameter("Name", moduleName)
142+
.AddParameter("ErrorAction", "SilentlyContinue")
143+
.InvokeAndClearCommands();
192144
}
193145
}
194146
}

src/RequestProcessor.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal RequestProcessor(MessagingStream msgStream)
2626
{
2727
_msgStream = msgStream;
2828
_logger = new RpcLogger(msgStream);
29-
_powerShellManager = PowerShellManager.Create(_logger);
29+
_powerShellManager = new PowerShellManager(_logger);
3030
_functionLoader = new FunctionLoader();
3131
}
3232

@@ -63,15 +63,27 @@ internal async Task ProcessRequestLoop()
6363

6464
internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
6565
{
66+
StatusResult status = new StatusResult()
67+
{
68+
Status = StatusResult.Types.Status.Success
69+
};
70+
71+
try
72+
{
73+
_powerShellManager.InitializeRunspace();
74+
}
75+
catch (Exception e)
76+
{
77+
status.Status = StatusResult.Types.Status.Failure;
78+
status.Exception = e.ToRpcException();
79+
}
80+
6681
return new StreamingMessage()
6782
{
6883
RequestId = request.RequestId,
6984
WorkerInitResponse = new WorkerInitResponse()
7085
{
71-
Result = new StatusResult()
72-
{
73-
Status = StatusResult.Types.Status.Success
74-
}
86+
Result = status
7587
}
7688
};
7789
}
@@ -143,8 +155,7 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
143155
try
144156
{
145157
result = _powerShellManager
146-
.InvokeFunctionAndSetGlobalReturn(scriptPath, entryPoint, triggerMetadata, invocationRequest.InputData)
147-
.ReturnBindingHashtable(functionInfo.OutputBindings);
158+
.InvokeFunction(scriptPath, entryPoint, triggerMetadata, invocationRequest.InputData);
148159
}
149160
catch (Exception e)
150161
{

0 commit comments

Comments
 (0)