Skip to content

Commit 887c882

Browse files
Add support for the New PowerShell Programming Model
1 parent 565ea8b commit 887c882

File tree

6 files changed

+235
-16
lines changed

6 files changed

+235
-16
lines changed

src/DependencyManagement/DependencyManager.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal class DependencyManager : IDisposable
4444
#endregion
4545

4646
public DependencyManager(
47-
string requestMetadataDirectory = null,
47+
string functionAppRootPath = null,
4848
IModuleProvider moduleProvider = null,
4949
IDependencyManagerStorage storage = null,
5050
IInstalledDependenciesLocator installedDependenciesLocator = null,
@@ -54,7 +54,7 @@ public DependencyManager(
5454
IBackgroundDependencySnapshotContentLogger currentSnapshotContentLogger = null,
5555
ILogger logger = null)
5656
{
57-
_storage = storage ?? new DependencyManagerStorage(GetFunctionAppRootPath(requestMetadataDirectory));
57+
_storage = storage ?? new DependencyManagerStorage(GetFunctionAppRootPath(functionAppRootPath));
5858
_installedDependenciesLocator = installedDependenciesLocator ?? new InstalledDependenciesLocator(_storage, logger);
5959
var snapshotContentLogger = new PowerShellModuleSnapshotLogger();
6060
_installer = installer ?? new DependencySnapshotInstaller(
@@ -252,14 +252,14 @@ private bool AreAcceptableDependenciesAlreadyInstalled()
252252
return _storage.SnapshotExists(_currentSnapshotPath);
253253
}
254254

255-
private static string GetFunctionAppRootPath(string requestMetadataDirectory)
255+
private static string GetFunctionAppRootPath(string functionAppRootPath)
256256
{
257-
if (string.IsNullOrWhiteSpace(requestMetadataDirectory))
257+
if (string.IsNullOrWhiteSpace(functionAppRootPath))
258258
{
259-
throw new ArgumentException("Empty request metadata directory path", nameof(requestMetadataDirectory));
259+
throw new ArgumentException("Empty function app root path", nameof(functionAppRootPath));
260260
}
261261

262-
return Path.GetFullPath(Path.Join(requestMetadataDirectory, ".."));
262+
return functionAppRootPath;
263263
}
264264

265265
#endregion

src/RequestProcessor.cs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
namespace Microsoft.Azure.Functions.PowerShellWorker
2020
{
21+
using Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing;
22+
using Microsoft.PowerShell;
2123
using System.Diagnostics;
24+
using System.Text.Json;
2225
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
2326

2427
internal class RequestProcessor
@@ -66,6 +69,8 @@ internal RequestProcessor(MessagingStream msgStream, System.Management.Automatio
6669
// If an invocation is cancelled, host will receive an invocation response with status cancelled.
6770
_requestHandlers.Add(StreamingMessage.ContentOneofCase.InvocationCancel, ProcessInvocationCancelRequest);
6871

72+
_requestHandlers.Add(StreamingMessage.ContentOneofCase.FunctionsMetadataRequest, ProcessFunctionMetadataRequest);
73+
6974
_requestHandlers.Add(StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadRequest, ProcessFunctionEnvironmentReloadRequest);
7075
}
7176

@@ -95,6 +100,9 @@ internal async Task ProcessRequestLoop()
95100

96101
internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
97102
{
103+
var stopwatch = new Stopwatch();
104+
stopwatch.Start();
105+
98106
var workerInitRequest = request.WorkerInitRequest;
99107
Environment.SetEnvironmentVariable("AZUREPS_HOST_ENVIRONMENT", $"AzureFunctions/{workerInitRequest.HostVersion}");
100108
Environment.SetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL", $"Azure-Functions:{workerInitRequest.HostVersion}");
@@ -117,6 +125,32 @@ internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
117125
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(pipeName);
118126
}
119127

128+
// Previously, this half of the dependency management would happen just prior to the dependency download in the
129+
// first function load request. Now that we have the FunctionAppDirectory in the WorkerInitRequest,
130+
// we can do the setup of these variables in the function load request. We need these variables initialized
131+
// for the FunctionMetadataRequest, should it be sent.
132+
try
133+
{
134+
var rpcLogger = new RpcLogger(_msgStream);
135+
rpcLogger.SetContext(request.RequestId, null);
136+
137+
_dependencyManager = new DependencyManager(request.WorkerInitRequest.FunctionAppDirectory, logger: rpcLogger);
138+
139+
_powershellPool.Initialize(_firstPwshInstance);
140+
141+
rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds));
142+
}
143+
catch (Exception e)
144+
{
145+
// This is a terminating failure: we will need to return a failure response to
146+
// all subsequent 'FunctionLoadRequest'. Cache the exception so we can reuse it in future calls.
147+
_initTerminatingError = e;
148+
149+
status.Status = StatusResult.Types.Status.Failure;
150+
status.Exception = e.ToRpcException();
151+
return response;
152+
}
153+
120154
return response;
121155
}
122156

@@ -189,26 +223,20 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
189223
{
190224
try
191225
{
192-
_isFunctionAppInitialized = true;
193-
194226
var rpcLogger = new RpcLogger(_msgStream);
195227
rpcLogger.SetContext(request.RequestId, null);
196228

197-
_dependencyManager = new DependencyManager(request.FunctionLoadRequest.Metadata.Directory, logger: rpcLogger);
198-
var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger);
199-
200-
SetupAppRootPathAndModulePath(functionLoadRequest, managedDependenciesPath);
229+
_isFunctionAppInitialized = true;
201230

202-
_powershellPool.Initialize(_firstPwshInstance);
231+
var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger);
203232

233+
SetupAppRootPathAndModulePath(request.FunctionLoadRequest, managedDependenciesPath);
204234
// Start the download asynchronously if needed.
205235
_dependencyManager.StartDependencyInstallationIfNeeded(request, _firstPwshInstance, rpcLogger);
206-
207-
rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds));
208236
}
209237
catch (Exception e)
210238
{
211-
// Failure that happens during this step is terminating and we will need to return a failure response to
239+
// This is a terminating failure: we will need to return a failure response to
212240
// all subsequent 'FunctionLoadRequest'. Cache the exception so we can reuse it in future calls.
213241
_initTerminatingError = e;
214242

@@ -341,6 +369,18 @@ internal StreamingMessage ProcessInvocationCancelRequest(StreamingMessage reques
341369
return null;
342370
}
343371

372+
private StreamingMessage ProcessFunctionMetadataRequest(StreamingMessage request)
373+
{
374+
StreamingMessage response = NewStreamingMessageTemplate(
375+
request.RequestId,
376+
StreamingMessage.ContentOneofCase.FunctionMetadataResponse,
377+
out StatusResult status);
378+
379+
response.FunctionMetadataResponse.FunctionMetadataResults.AddRange(WorkerIndexingHelper.IndexFunctions(request.FunctionsMetadataRequest.FunctionAppDirectory));
380+
381+
return response;
382+
}
383+
344384
internal StreamingMessage ProcessFunctionEnvironmentReloadRequest(StreamingMessage request)
345385
{
346386
var stopwatch = new Stopwatch();
@@ -394,6 +434,9 @@ private StreamingMessage NewStreamingMessageTemplate(string requestId, Streaming
394434
case StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadResponse:
395435
response.FunctionEnvironmentReloadResponse = new FunctionEnvironmentReloadResponse() { Result = status };
396436
break;
437+
case StreamingMessage.ContentOneofCase.FunctionMetadataResponse:
438+
response.FunctionMetadataResponse = new FunctionMetadataResponse() { Result = status };
439+
break;
397440
default:
398441
throw new InvalidOperationException("Unreachable code.");
399442
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 Microsoft.Azure.WebJobs.Script.Grpc.Messages;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
using System;
10+
using System.Collections.Generic;
11+
12+
namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
13+
{
14+
internal class BindingInformation
15+
{
16+
private const string BindingNameKey = "name";
17+
private const string BindingDirectionKey = "direction";
18+
private const string BindingTypeKey = "type";
19+
public enum Directions
20+
{
21+
Unknown = -1,
22+
In = 0,
23+
Out = 1,
24+
Inout = 2
25+
}
26+
27+
public Directions Direction { get; set; } = Directions.Unknown;
28+
public string Type { get; set; } = "";
29+
public string Name { get; set; } = "";
30+
public Dictionary<string, Object> otherInformation { get; set; } = new Dictionary<string, Object>();
31+
32+
internal string ConvertToRpcRawBinding(out BindingInfo bindingInfo)
33+
{
34+
string rawBinding = string.Empty;
35+
JObject rawBindingObject = new JObject();
36+
rawBindingObject.Add(BindingNameKey, Name);
37+
BindingInfo outInfo = new BindingInfo();
38+
39+
40+
if (Direction == Directions.Unknown)
41+
{
42+
throw new Exception(string.Format(PowerShellWorkerStrings.InvalidBindingInfoDirection, Name));
43+
}
44+
outInfo.Direction = (BindingInfo.Types.Direction)Direction;
45+
rawBindingObject.Add(BindingDirectionKey, Enum.GetName(typeof(BindingInfo.Types.Direction), outInfo.Direction).ToLower());
46+
outInfo.Type = Type;
47+
rawBindingObject.Add(BindingTypeKey, Type);
48+
49+
foreach (KeyValuePair<string, Object> pair in otherInformation)
50+
{
51+
rawBindingObject.Add(pair.Key, JToken.FromObject(pair.Value));
52+
}
53+
54+
rawBinding = JsonConvert.SerializeObject(rawBindingObject);
55+
bindingInfo = outInfo;
56+
return rawBinding;
57+
}
58+
}
59+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 Microsoft.Azure.WebJobs.Script.Grpc.Messages;
7+
using System.Collections.Generic;
8+
9+
namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
10+
{
11+
internal class FunctionInformation
12+
{
13+
private const string FunctionLanguagePowerShell = "powershell";
14+
15+
public string Directory { get; set; } = "";
16+
public string ScriptFile { get; set; } = "";
17+
public string Name { get; set; } = "";
18+
public string EntryPoint { get; set; } = "";
19+
public string FunctionId { get; set; } = "";
20+
public List<BindingInformation> Bindings { get; set; } = new List<BindingInformation>();
21+
22+
internal RpcFunctionMetadata ConvertToRpc()
23+
{
24+
RpcFunctionMetadata returnMetadata = new RpcFunctionMetadata();
25+
returnMetadata.FunctionId = FunctionId;
26+
returnMetadata.Directory = Directory;
27+
returnMetadata.EntryPoint = EntryPoint;
28+
returnMetadata.Name = Name;
29+
returnMetadata.ScriptFile = ScriptFile;
30+
returnMetadata.Language = FunctionLanguagePowerShell;
31+
foreach(BindingInformation binding in Bindings)
32+
{
33+
string rawBinding = binding.ConvertToRpcRawBinding(out BindingInfo bindingInfo);
34+
returnMetadata.Bindings.Add(binding.Name, bindingInfo);
35+
returnMetadata.RawBindings.Add(rawBinding);
36+
}
37+
return returnMetadata;
38+
}
39+
}
40+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 Microsoft.Azure.WebJobs.Script.Grpc.Messages;
7+
using Newtonsoft.Json;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Management.Automation;
11+
using System.Management.Automation.Runspaces;
12+
13+
namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
14+
{
15+
internal class WorkerIndexingHelper
16+
{
17+
const string GetFunctionsMetadataCmdletName = "AzureFunctions.PowerShell.SDK\\Get-FunctionsMetadata";
18+
internal static IEnumerable<RpcFunctionMetadata> IndexFunctions(string baseDir)
19+
{
20+
List<RpcFunctionMetadata> indexedFunctions = new List<RpcFunctionMetadata>();
21+
22+
// This is not the correct way to deal with getting a runspace for the cmdlet.
23+
24+
// Firstly, creating a runspace is expensive. If we are going to generate a runspace, it should be done on
25+
// the function load request so that it can be created while the host is processing.
26+
27+
// Secondly, this assumes that the AzureFunctions.PowerShell.SDK module is present on the machine/VM's
28+
// PSModulePath. On an Azure instance, it will not be. What we need to do here is move the call
29+
// to SetupAppRootPathAndModulePath in RequestProcessor to the init request, and then use the
30+
// _firstPwshInstance to invoke the Get-FunctionsMetadata command. The only issue with this is that
31+
// SetupAppRootPathAndModulePath needs the initial function init request in order to know if managed
32+
// dependencies are enabled in this function app.
33+
34+
// Proposed solutions:
35+
// 1. Pass ManagedDependencyEnabled flag in the worker init request
36+
// 2. Change the flow, so that _firstPwshInstance is initialized in worker init with the PSModulePath
37+
// assuming that managed dependencies are enabled, and then revert the PSModulePath in the first function
38+
// init request should the managed dependencies not be enabled.
39+
// 3. Continue using a new runspace for invoking Get-FunctionsMetadata, but initialize it in worker init and
40+
// point the PsModulePath to the module path bundled with the worker.
41+
42+
43+
InitialSessionState initial = InitialSessionState.CreateDefault();
44+
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
45+
runspace.Open();
46+
System.Management.Automation.PowerShell _powershell = System.Management.Automation.PowerShell.Create();
47+
_powershell.Runspace = runspace;
48+
49+
_powershell.AddCommand(GetFunctionsMetadataCmdletName).AddArgument(baseDir);
50+
string outputString = string.Empty;
51+
foreach (PSObject rawMetadata in _powershell.Invoke())
52+
{
53+
if (outputString != string.Empty)
54+
{
55+
throw new Exception(PowerShellWorkerStrings.GetFunctionsMetadataMultipleResultsError);
56+
}
57+
outputString = rawMetadata.ToString();
58+
}
59+
_powershell.Commands.Clear();
60+
61+
List<FunctionInformation> functionInformations = JsonConvert.DeserializeObject<List<FunctionInformation>>(outputString);
62+
63+
foreach(FunctionInformation fi in functionInformations)
64+
{
65+
indexedFunctions.Add(fi.ConvertToRpc());
66+
}
67+
68+
return indexedFunctions;
69+
}
70+
}
71+
}

src/resources/PowerShellWorkerStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,4 +352,10 @@
352352
<data name="DependencySnapshotDoesNotContainAcceptableModuleVersions" xml:space="preserve">
353353
<value>Dependency snapshot '{0}' does not contain acceptable module versions.</value>
354354
</data>
355+
<data name="GetFunctionsMetadataMultipleResultsError" xml:space="preserve">
356+
<value>Multiple results from metadata cmdlet.</value>
357+
</data>
358+
<data name="InvalidBindingInfoDirection" xml:space="preserve">
359+
<value>Invalid binding direction. Binding name: {0}</value>
360+
</data>
355361
</root>

0 commit comments

Comments
 (0)