diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs
index 112ed48f..8210e89d 100644
--- a/src/DependencyManagement/DependencyManager.cs
+++ b/src/DependencyManagement/DependencyManager.cs
@@ -79,7 +79,8 @@ internal DependencyManager()
///
/// The protobuf messaging stream
/// The StreamingMessage request for function load
- internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMessage request)
+ /// The PowerShell instance used to download modules
+ internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMessage request, PowerShell pwsh)
{
if (request.FunctionLoadRequest.ManagedDependencyEnabled)
{
@@ -100,7 +101,7 @@ internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMess
}
//Start dependency download on a separate thread
- _dependencyDownloadTask = Task.Run(() => ProcessDependencies(rpcLogger));
+ _dependencyDownloadTask = Task.Run(() => InstallFunctionAppDependencies(pwsh, rpcLogger));
}
}
@@ -117,22 +118,6 @@ internal void WaitOnDependencyDownload()
}
}
- private void ProcessDependencies(RpcLogger rpcLogger)
- {
- try
- {
- _dependencyError = null;
- using (PowerShell pwsh = PowerShell.Create(Utils.SingletonISS.Value))
- {
- InstallFunctionAppDependencies(pwsh, rpcLogger);
- }
- }
- catch (Exception e)
- {
- _dependencyError = e;
- }
- }
-
///
/// Initializes the dependency manger and performs the following:
/// - Parse functionAppRoot\requirements.psd1 file and create a list of dependencies to install.
@@ -251,7 +236,7 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger)
catch (Exception e)
{
var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependencies, e.Message);
- throw new DependencyInstallationException(errorMsg, e);
+ _dependencyError = new DependencyInstallationException(errorMsg, e);
}
}
diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs
index d753ffa2..19ec6f19 100644
--- a/src/PowerShell/PowerShellManager.cs
+++ b/src/PowerShell/PowerShellManager.cs
@@ -22,6 +22,7 @@ internal class PowerShellManager
{
private readonly ILogger _logger;
private readonly PowerShell _pwsh;
+ private bool _runspaceInited;
///
/// Gets the Runspace InstanceId.
@@ -45,27 +46,64 @@ static PowerShellManager()
addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) });
}
- internal PowerShellManager(ILogger logger)
+ ///
+ /// Constructor for setting the basic fields.
+ ///
+ private PowerShellManager(ILogger logger, PowerShell pwsh, int id)
+ {
+ _logger = logger;
+ _pwsh = pwsh;
+ _pwsh.Runspace.Name = $"PowerShellManager{id}";
+ }
+
+ ///
+ /// Create a PowerShellManager instance but defer the Initialization.
+ ///
+ ///
+ /// This constructor is only for creating the very first PowerShellManager instance.
+ /// The initialization work is deferred until all prerequisites are ready, such as
+ /// the dependent modules are downloaded and all Az functions are loaded.
+ ///
+ internal PowerShellManager(ILogger logger, PowerShell pwsh)
+ : this(logger, pwsh, id: 1)
{
- if (FunctionLoader.FunctionAppRootPath == null)
+ }
+
+ ///
+ /// Create a PowerShellManager instance and initialize it.
+ ///
+ internal PowerShellManager(ILogger logger, int id)
+ : this(logger, Utils.NewPwshInstance(), id)
+ {
+ // Initialize the Runspace
+ Initialize();
+ }
+
+ ///
+ /// Extra initialization of the Runspace.
+ ///
+ internal void Initialize()
+ {
+ if (!_runspaceInited)
{
- throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved);
+ RegisterStreamEvents();
+ InvokeProfile(FunctionLoader.FunctionAppProfilePath);
+ _runspaceInited = true;
}
+ }
- _logger = logger;
- _pwsh = PowerShell.Create(Utils.SingletonISS.Value);
-
- // Setup Stream event listeners
- var streamHandler = new StreamHandler(logger);
+ ///
+ /// Setup Stream event listeners.
+ ///
+ private void RegisterStreamEvents()
+ {
+ var streamHandler = new StreamHandler(_logger);
_pwsh.Streams.Debug.DataAdding += streamHandler.DebugDataAdding;
_pwsh.Streams.Error.DataAdding += streamHandler.ErrorDataAdding;
_pwsh.Streams.Information.DataAdding += streamHandler.InformationDataAdding;
_pwsh.Streams.Progress.DataAdding += streamHandler.ProgressDataAdding;
_pwsh.Streams.Verbose.DataAdding += streamHandler.VerboseDataAdding;
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
-
- // Initialize the Runspace
- InvokeProfile(FunctionLoader.FunctionAppProfilePath);
}
///
@@ -76,7 +114,8 @@ internal void InvokeProfile(string profilePath)
Exception exception = null;
if (profilePath == null)
{
- RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.FileNotFound, "profile.ps1", FunctionLoader.FunctionAppRootPath));
+ string noProfileMsg = string.Format(PowerShellWorkerStrings.FileNotFound, "profile.ps1", FunctionLoader.FunctionAppRootPath);
+ _logger.Log(LogLevel.Trace, noProfileMsg);
return;
}
diff --git a/src/PowerShell/PowerShellManagerPool.cs b/src/PowerShell/PowerShellManagerPool.cs
index f5226434..d2fb13ab 100644
--- a/src/PowerShell/PowerShellManagerPool.cs
+++ b/src/PowerShell/PowerShellManagerPool.cs
@@ -46,6 +46,18 @@ internal PowerShellManagerPool(MessagingStream msgStream)
RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.LogConcurrencyUpperBound, _upperBound.ToString()));
}
+ ///
+ /// Populate the pool with the very first PowerShellManager instance.
+ /// We instantiate PowerShellManager instances in a lazy way, starting from size 1 and increase the number of workers as needed.
+ ///
+ internal void Initialize(PowerShell pwsh)
+ {
+ var logger = new RpcLogger(_msgStream);
+ var psManager = new PowerShellManager(logger, pwsh);
+ _pool.Add(psManager);
+ _poolSize = 1;
+ }
+
///
/// Checkout an idle PowerShellManager instance in a non-blocking asynchronous way.
///
@@ -59,18 +71,22 @@ internal PowerShellManager CheckoutIdleWorker(StreamingMessage request, AzFuncti
if (!_pool.TryTake(out psManager))
{
// The pool doesn't have an idle one.
- if (_poolSize < _upperBound &&
- Interlocked.Increment(ref _poolSize) <= _upperBound)
+ if (_poolSize < _upperBound)
{
- // If the pool hasn't reached its bounded capacity yet, then
- // we create a new item and return it.
- var logger = new RpcLogger(_msgStream);
- logger.SetContext(requestId, invocationId);
- psManager = new PowerShellManager(logger);
+ int id = Interlocked.Increment(ref _poolSize);
+ if (id <= _upperBound)
+ {
+ // If the pool hasn't reached its bounded capacity yet, then
+ // we create a new item and return it.
+ var logger = new RpcLogger(_msgStream);
+ logger.SetContext(requestId, invocationId);
+ psManager = new PowerShellManager(logger, id);
- RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.LogNewPowerShellManagerCreated, _poolSize.ToString()));
+ RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.LogNewPowerShellManagerCreated, id.ToString()));
+ }
}
- else
+
+ if (psManager == null)
{
// If the pool has reached its bounded capacity, then the thread
// should be blocked until an idle one becomes available.
@@ -78,9 +94,14 @@ internal PowerShellManager CheckoutIdleWorker(StreamingMessage request, AzFuncti
}
}
+ // Finish the initialization if not yet.
+ // This applies only to the very first PowerShellManager instance, whose initialization was deferred.
+ psManager.Initialize();
+
// Register the function with the Runspace before returning the idle PowerShellManager.
FunctionMetadata.RegisterFunctionMetadata(psManager.InstanceId, functionInfo);
psManager.Logger.SetContext(requestId, invocationId);
+
return psManager;
}
diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs
index 26959ad8..f0d37245 100644
--- a/src/RequestProcessor.cs
+++ b/src/RequestProcessor.cs
@@ -170,7 +170,15 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
// Setup the FunctionApp root path and module path.
FunctionLoader.SetupWellKnownPaths(functionLoadRequest);
- _dependencyManager.ProcessDependencyDownload(_msgStream, request);
+
+ // 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();
+ _powershellPool.Initialize(pwsh);
+
+ // Start the download asynchronously if needed.
+ _dependencyManager.ProcessDependencyDownload(_msgStream, request, pwsh);
}
catch (Exception e)
{
diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs
index 5b94bc0b..349bbe5d 100644
--- a/src/Utility/Utils.cs
+++ b/src/Utility/Utils.cs
@@ -5,14 +5,15 @@
using System;
using System.IO;
-using System.Management.Automation;
-using System.Management.Automation.Runspaces;
using System.Text;
using System.Threading;
using Microsoft.PowerShell.Commands;
namespace Microsoft.Azure.Functions.PowerShellWorker.Utility
{
+ using System.Management.Automation;
+ using System.Management.Automation.Runspaces;
+
internal class Utils
{
internal readonly static CmdletInfo ImportModuleCmdletInfo = new CmdletInfo("Import-Module", typeof(ImportModuleCommand));
@@ -20,28 +21,38 @@ internal class Utils
internal readonly static CmdletInfo GetJobCmdletInfo = new CmdletInfo("Get-Job", typeof(GetJobCommand));
internal readonly static CmdletInfo RemoveJobCmdletInfo = new CmdletInfo("Remove-Job", typeof(RemoveJobCommand));
- internal readonly static Lazy SingletonISS
- = new Lazy(NewInitialSessionState, LazyThreadSafetyMode.PublicationOnly);
+ private static InitialSessionState s_iss;
- private static InitialSessionState NewInitialSessionState()
+ ///
+ /// Create a new PowerShell instance using our singleton InitialSessionState instance.
+ ///
+ internal static PowerShell NewPwshInstance()
{
- var iss = InitialSessionState.CreateDefault();
- iss.ThreadOptions = PSThreadOptions.UseCurrentThread;
- 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)
+ if (s_iss == null)
{
- // This sets the execution policy on Windows to Unrestricted which is required to run the user's function scripts on
- // Windows client versions. This is needed if a user is testing their function locally with the func CLI.
- iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
+ if (FunctionLoader.FunctionAppRootPath == null)
+ {
+ throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved);
+ }
+
+ s_iss = InitialSessionState.CreateDefault();
+ s_iss.ThreadOptions = PSThreadOptions.UseCurrentThread;
+ 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)
+ {
+ // This sets the execution policy on Windows to Unrestricted which is required to run the user's function scripts on
+ // Windows client versions. This is needed if a user is testing their function locally with the func CLI.
+ s_iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
+ }
}
- return iss;
+ return PowerShell.Create(s_iss);
}
///
diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs
index 26741784..75e5a3fb 100644
--- a/test/Unit/PowerShell/PowerShellManagerTests.cs
+++ b/test/Unit/PowerShell/PowerShellManagerTests.cs
@@ -7,13 +7,15 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
-using System.Management.Automation;
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
+using Microsoft.Azure.Functions.PowerShellWorker.Utility;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Xunit;
namespace Microsoft.Azure.Functions.PowerShellWorker.Test
{
+ using System.Management.Automation;
+
internal class TestUtils
{
internal const string TestInputBindingName = "req";
@@ -43,9 +45,9 @@ static TestUtils()
// Have a single place to get a PowerShellManager for testing.
// This is to guarantee that the well known paths are setup before calling the constructor of PowerShellManager.
- internal static PowerShellManager NewTestPowerShellManager(ConsoleLogger logger)
+ internal static PowerShellManager NewTestPowerShellManager(ConsoleLogger logger, PowerShell pwsh = null)
{
- return new PowerShellManager(logger);
+ return pwsh != null ? new PowerShellManager(logger, pwsh) : new PowerShellManager(logger, id: 2);
}
internal static AzFunctionInfo NewAzFunctionInfo(string scriptFile, string entryPoint)
@@ -235,5 +237,26 @@ public void ProfileWithNonTerminatingError()
Assert.Equal("Error: ERROR: help me!", _testLogger.FullLog[0]);
Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", _testLogger.FullLog[1]);
}
+
+ [Fact]
+ public void PSManagerCtorRunsProfileByDefault()
+ {
+ //initialize fresh log
+ _testLogger.FullLog.Clear();
+ TestUtils.NewTestPowerShellManager(_testLogger);
+
+ Assert.Single(_testLogger.FullLog);
+ Assert.Equal($"Trace: No 'profile.ps1' is found at the FunctionApp root folder: {FunctionLoader.FunctionAppRootPath}.", _testLogger.FullLog[0]);
+ }
+
+ [Fact]
+ public void PSManagerCtorDoesNotRunProfileIfDelayInit()
+ {
+ //initialize fresh log
+ _testLogger.FullLog.Clear();
+ TestUtils.NewTestPowerShellManager(_testLogger, Utils.NewPwshInstance());
+
+ Assert.Empty(_testLogger.FullLog);
+ }
}
}