From f72332f4b06b51a7a6c935f0c508bc4e14f634e9 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 16:37:13 -0700 Subject: [PATCH 01/10] Pre-create the first PowerShell instance --- src/RequestProcessor.cs | 14 ++++++-------- src/Utility/Utils.cs | 18 ++++++++---------- src/Worker.cs | 7 ++++++- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index 8a15319f..bc32dc55 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -25,6 +25,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker internal class RequestProcessor { private readonly MessagingStream _msgStream; + private readonly System.Management.Automation.PowerShell _firstPwshInstance; private readonly PowerShellManagerPool _powershellPool; private DependencyManager _dependencyManager; @@ -37,9 +38,10 @@ internal class RequestProcessor private Dictionary> _requestHandlers = new Dictionary>(); - internal RequestProcessor(MessagingStream msgStream) + internal RequestProcessor(MessagingStream msgStream, System.Management.Automation.PowerShell firstPwshInstance) { _msgStream = msgStream; + _firstPwshInstance = firstPwshInstance; _powershellPool = new PowerShellManagerPool(() => new RpcLogger(msgStream)); // Host sends capabilities/init data to worker @@ -197,15 +199,11 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) // Setup the FunctionApp root path and module path. FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); - // Create the very first Runspace so the debugger has the target to attach to. - // This PowerShell instance is shared by the first PowerShellManager instance created in the pool, - // and the dependency manager (used to download dependent modules if needed). - var pwsh = Utils.NewPwshInstance(); - LogPowerShellVersion(rpcLogger, pwsh); - _powershellPool.Initialize(pwsh); + LogPowerShellVersion(rpcLogger, _firstPwshInstance); + _powershellPool.Initialize(_firstPwshInstance); // Start the download asynchronously if needed. - _dependencyManager.StartDependencyInstallationIfNeeded(request, pwsh, rpcLogger); + _dependencyManager.StartDependencyInstallationIfNeeded(request, _firstPwshInstance, rpcLogger); rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds)); } diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs index 33014556..2276c0fa 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -37,11 +37,6 @@ internal static PowerShell NewPwshInstance() { if (s_iss == null) { - if (FunctionLoader.FunctionAppRootPath == null) - { - throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved); - } - s_iss = InitialSessionState.CreateDefault(); if (!AreDurableFunctionsEnabled()) @@ -51,11 +46,14 @@ internal static PowerShell NewPwshInstance() s_iss.ThreadOptions = PSThreadOptions.UseCurrentThread; } - s_iss.EnvironmentVariables.Add( - new SessionStateVariableEntry( - "PSModulePath", - FunctionLoader.FunctionModulePath, - description: null)); + if (FunctionLoader.FunctionAppRootPath != null) + { + s_iss.EnvironmentVariables.Add( + new SessionStateVariableEntry( + "PSModulePath", + FunctionLoader.FunctionModulePath, + description: null)); + } // Setting the execution policy on macOS and Linux throws an exception so only update it on Windows if(Platform.IsWindows) diff --git a/src/Worker.cs b/src/Worker.cs index 093fcb3b..06ff3647 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -34,8 +34,13 @@ public async static Task Main(string[] args) .WithParsed(ops => arguments = ops) .WithNotParsed(err => Environment.Exit(1)); + // Create the very first Runspace so the debugger has the target to attach to. + // This PowerShell instance is shared by the first PowerShellManager instance created in the pool, + // and the dependency manager (used to download dependent modules if needed). + var pwsh = Utils.NewPwshInstance(); + var msgStream = new MessagingStream(arguments.Host, arguments.Port); - var requestProcessor = new RequestProcessor(msgStream); + var requestProcessor = new RequestProcessor(msgStream, pwsh); // Send StartStream message var startedMessage = new StreamingMessage() { From c6e42dc8244d3066ca9a773b2c65e5a51be0b5a3 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 19:59:19 -0700 Subject: [PATCH 02/10] Set $env:PSModulePath in the first PowerShell instance --- src/RequestProcessor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index bc32dc55..dcf0ba6e 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -200,6 +200,12 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); LogPowerShellVersion(rpcLogger, _firstPwshInstance); + + _firstPwshInstance.AddCommand("Set-Content") + .AddParameter("Path", "env:PSModulePath") + .AddParameter("Value", FunctionLoader.FunctionModulePath) + .InvokeAndClearCommands(); + _powershellPool.Initialize(_firstPwshInstance); // Start the download asynchronously if needed. From c9f48ebd3d5b39b073f91b5117479014e362c7fc Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 20:05:28 -0700 Subject: [PATCH 03/10] Move PowerShell version logging to Worker.Main --- src/RequestProcessor.cs | 8 -------- src/Worker.cs | 11 +++++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index dcf0ba6e..1b6a03af 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -199,8 +199,6 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) // Setup the FunctionApp root path and module path. FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); - LogPowerShellVersion(rpcLogger, _firstPwshInstance); - _firstPwshInstance.AddCommand("Set-Content") .AddParameter("Path", "env:PSModulePath") .AddParameter("Value", FunctionLoader.FunctionModulePath) @@ -497,12 +495,6 @@ private static void BindOutputFromResult(InvocationResponse response, AzFunction } } - private static void LogPowerShellVersion(RpcLogger rpcLogger, System.Management.Automation.PowerShell pwsh) - { - var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh)); - rpcLogger.Log(isUserOnlyLog: false, LogLevel.Information, message); - } - #endregion } } diff --git a/src/Worker.cs b/src/Worker.cs index 06ff3647..1f484139 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -37,10 +37,11 @@ public async static Task Main(string[] args) // Create the very first Runspace so the debugger has the target to attach to. // This PowerShell instance is shared by the first PowerShellManager instance created in the pool, // and the dependency manager (used to download dependent modules if needed). - var pwsh = Utils.NewPwshInstance(); + var firstPowerShellInstance = Utils.NewPwshInstance(); + LogPowerShellVersion(firstPowerShellInstance); var msgStream = new MessagingStream(arguments.Host, arguments.Port); - var requestProcessor = new RequestProcessor(msgStream, pwsh); + var requestProcessor = new RequestProcessor(msgStream, firstPowerShellInstance); // Send StartStream message var startedMessage = new StreamingMessage() { @@ -51,6 +52,12 @@ public async static Task Main(string[] args) msgStream.Write(startedMessage); await requestProcessor.ProcessRequestLoop(); } + + private static void LogPowerShellVersion(System.Management.Automation.PowerShell pwsh) + { + var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh)); + RpcLogger.WriteSystemLog(LogLevel.Information, message); + } } internal class WorkerArguments From 23c239a3b4163c13e97fe05db76d3adf385172fd Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 20:20:32 -0700 Subject: [PATCH 04/10] Add/remove a dummy function to warm up the runspace --- src/Worker.cs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Worker.cs b/src/Worker.cs index 1f484139..31534f10 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -4,10 +4,12 @@ // using System; +using System.Management.Automation; using System.Threading.Tasks; using CommandLine; using Microsoft.Azure.Functions.PowerShellWorker.Messaging; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -39,12 +41,14 @@ public async static Task Main(string[] args) // and the dependency manager (used to download dependent modules if needed). var firstPowerShellInstance = Utils.NewPwshInstance(); LogPowerShellVersion(firstPowerShellInstance); + WarmUpPowerShell(firstPowerShellInstance); var msgStream = new MessagingStream(arguments.Host, arguments.Port); var requestProcessor = new RequestProcessor(msgStream, firstPowerShellInstance); // Send StartStream message - var startedMessage = new StreamingMessage() { + var startedMessage = new StreamingMessage() + { RequestId = arguments.RequestId, StartStream = new StartStream() { WorkerId = arguments.WorkerId } }; @@ -53,6 +57,23 @@ public async static Task Main(string[] args) await requestProcessor.ProcessRequestLoop(); } + // Warm up the PowerShell instance so that the subsequent function load and invocation requests are faster + private static void WarmUpPowerShell(System.Management.Automation.PowerShell firstPowerShellInstance) + { + // We just need this name to be unique, so that it does not coincide with an actual function + const string DummyFunctionName = "DummyFunction-71b09c92-6bce-42d0-aba1-7b985b8c3563"; + + firstPowerShellInstance.AddCommand("New-Item") + .AddParameter("Path", "Function:") + .AddParameter("Name", DummyFunctionName) + .AddParameter("Value", ScriptBlock.Create(string.Empty)) + .InvokeAndClearCommands(); + + firstPowerShellInstance.AddCommand("Remove-Item") + .AddParameter("Path", $"Function:{DummyFunctionName}") + .InvokeAndClearCommands(); + } + private static void LogPowerShellVersion(System.Management.Automation.PowerShell pwsh) { var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh)); From 8edbdf08767788e56776f9532069c7fbbeb85aa4 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 20:42:22 -0700 Subject: [PATCH 05/10] Restore FunctionAppRootNotResolved check --- src/RequestProcessor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index 1b6a03af..e396e758 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -199,6 +199,12 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) // Setup the FunctionApp root path and module path. FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); + + if (FunctionLoader.FunctionAppRootPath == null) + { + throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved); + } + _firstPwshInstance.AddCommand("Set-Content") .AddParameter("Path", "env:PSModulePath") .AddParameter("Value", FunctionLoader.FunctionModulePath) From 8a862e60ac7e8ec847246913381dc91c7f14ab56 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 20:48:11 -0700 Subject: [PATCH 06/10] Extract SetupAppRootPathAndModulePath method --- src/RequestProcessor.cs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index e396e758..ef65c1ca 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -196,19 +196,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) _dependencyManager = new DependencyManager(request.FunctionLoadRequest.Metadata.Directory, logger: rpcLogger); var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger); - // Setup the FunctionApp root path and module path. - FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); - - - if (FunctionLoader.FunctionAppRootPath == null) - { - throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved); - } - - _firstPwshInstance.AddCommand("Set-Content") - .AddParameter("Path", "env:PSModulePath") - .AddParameter("Value", FunctionLoader.FunctionModulePath) - .InvokeAndClearCommands(); + SetupAppRootPathAndModulePath(functionLoadRequest, managedDependenciesPath); _powershellPool.Initialize(_firstPwshInstance); @@ -501,6 +489,21 @@ private static void BindOutputFromResult(InvocationResponse response, AzFunction } } + private void SetupAppRootPathAndModulePath(FunctionLoadRequest functionLoadRequest, string managedDependenciesPath) + { + FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath); + + if (FunctionLoader.FunctionAppRootPath == null) + { + throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved); + } + + _firstPwshInstance.AddCommand("Set-Content") + .AddParameter("Path", "env:PSModulePath") + .AddParameter("Value", FunctionLoader.FunctionModulePath) + .InvokeAndClearCommands(); + } + #endregion } } From 75e27955954f1e8eab612db168ea167eee6ba19e Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 20:56:07 -0700 Subject: [PATCH 07/10] Remove unnecessary using --- src/RequestProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index ef65c1ca..c8b362a1 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -19,7 +19,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { using System.Diagnostics; - using System.IO; using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; internal class RequestProcessor From 57b819e322c50981c0b449b94a05366554490c07 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 21:04:23 -0700 Subject: [PATCH 08/10] Add a comment --- src/Worker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Worker.cs b/src/Worker.cs index 31534f10..344f8aa9 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -60,7 +60,8 @@ public async static Task Main(string[] args) // Warm up the PowerShell instance so that the subsequent function load and invocation requests are faster private static void WarmUpPowerShell(System.Management.Automation.PowerShell firstPowerShellInstance) { - // We just need this name to be unique, so that it does not coincide with an actual function + // It turns out that creating/removing a function warms up the session enough. + // We just need this name to be unique, so that it does not coincide with an actual function. const string DummyFunctionName = "DummyFunction-71b09c92-6bce-42d0-aba1-7b985b8c3563"; firstPowerShellInstance.AddCommand("New-Item") From 077ca70320f590a4ff6248320d303a94631d4b2f Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Wed, 2 Sep 2020 22:38:56 -0700 Subject: [PATCH 09/10] Fix a comment --- src/Worker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker.cs b/src/Worker.cs index 344f8aa9..1b437245 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -60,7 +60,7 @@ public async static Task Main(string[] args) // Warm up the PowerShell instance so that the subsequent function load and invocation requests are faster private static void WarmUpPowerShell(System.Management.Automation.PowerShell firstPowerShellInstance) { - // It turns out that creating/removing a function warms up the session enough. + // It turns out that creating/removing a function warms up the runspace enough. // We just need this name to be unique, so that it does not coincide with an actual function. const string DummyFunctionName = "DummyFunction-71b09c92-6bce-42d0-aba1-7b985b8c3563"; From 80ad3310fbbae9ef7a8562942312d999bc7787dc Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 3 Sep 2020 13:54:01 -0700 Subject: [PATCH 10/10] Include Microsoft.PowerShell.Management module name --- src/RequestProcessor.cs | 2 +- src/Worker.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index c8b362a1..005478bf 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -497,7 +497,7 @@ private void SetupAppRootPathAndModulePath(FunctionLoadRequest functionLoadReque throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved); } - _firstPwshInstance.AddCommand("Set-Content") + _firstPwshInstance.AddCommand("Microsoft.PowerShell.Management\\Set-Content") .AddParameter("Path", "env:PSModulePath") .AddParameter("Value", FunctionLoader.FunctionModulePath) .InvokeAndClearCommands(); diff --git a/src/Worker.cs b/src/Worker.cs index 1b437245..8987086c 100644 --- a/src/Worker.cs +++ b/src/Worker.cs @@ -64,13 +64,13 @@ private static void WarmUpPowerShell(System.Management.Automation.PowerShell fir // We just need this name to be unique, so that it does not coincide with an actual function. const string DummyFunctionName = "DummyFunction-71b09c92-6bce-42d0-aba1-7b985b8c3563"; - firstPowerShellInstance.AddCommand("New-Item") + firstPowerShellInstance.AddCommand("Microsoft.PowerShell.Management\\New-Item") .AddParameter("Path", "Function:") .AddParameter("Name", DummyFunctionName) .AddParameter("Value", ScriptBlock.Create(string.Empty)) .InvokeAndClearCommands(); - firstPowerShellInstance.AddCommand("Remove-Item") + firstPowerShellInstance.AddCommand("Microsoft.PowerShell.Management\\Remove-Item") .AddParameter("Path", $"Function:{DummyFunctionName}") .InvokeAndClearCommands(); }