Skip to content

Create the first PowerShellManager instance when processing the first FunctionLoad request #201

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 8 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 4 additions & 19 deletions src/DependencyManagement/DependencyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ internal DependencyManager()
/// </summary>
/// <param name="msgStream">The protobuf messaging stream</param>
/// <param name="request">The StreamingMessage request for function load</param>
internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMessage request)
/// <param name="pwsh">The PowerShell instance used to download modules</param>
internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMessage request, PowerShell pwsh)
{
if (request.FunctionLoadRequest.ManagedDependencyEnabled)
{
Expand All @@ -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));
}
}

Expand All @@ -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;
}
}

/// <summary>
/// Initializes the dependency manger and performs the following:
/// - Parse functionAppRoot\requirements.psd1 file and create a list of dependencies to install.
Expand Down Expand Up @@ -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);
}
}

Expand Down
63 changes: 51 additions & 12 deletions src/PowerShell/PowerShellManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class PowerShellManager
{
private readonly ILogger _logger;
private readonly PowerShell _pwsh;
private bool _runspaceInited;

/// <summary>
/// Gets the Runspace InstanceId.
Expand All @@ -45,27 +46,64 @@ static PowerShellManager()
addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) });
}

internal PowerShellManager(ILogger logger)
/// <summary>
/// Constructor for setting the basic fields.
/// </summary>
private PowerShellManager(ILogger logger, PowerShell pwsh, int id)
{
_logger = logger;
_pwsh = pwsh;
_pwsh.Runspace.Name = $"PowerShellManager{id}";
}

/// <summary>
/// Create a PowerShellManager instance but defer the Initialization.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal PowerShellManager(ILogger logger, PowerShell pwsh)
: this(logger, pwsh, id: 1)
{
if (FunctionLoader.FunctionAppRootPath == null)
}

/// <summary>
/// Create a PowerShellManager instance and initialize it.
/// </summary>
internal PowerShellManager(ILogger logger, int id)
: this(logger, Utils.NewPwshInstance(), id)
{
// Initialize the Runspace
Initialize();
}

/// <summary>
/// Extra initialization of the Runspace.
/// </summary>
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);
/// <summary>
/// Setup Stream event listeners.
/// </summary>
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);
}

/// <summary>
Expand All @@ -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;
}

Expand Down
39 changes: 30 additions & 9 deletions src/PowerShell/PowerShellManagerPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ internal PowerShellManagerPool(MessagingStream msgStream)
RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.LogConcurrencyUpperBound, _upperBound.ToString()));
}

/// <summary>
/// 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.
/// </summary>
internal void Initialize(PowerShell pwsh)
{
var logger = new RpcLogger(_msgStream);
var psManager = new PowerShellManager(logger, pwsh);
_pool.Add(psManager);
_poolSize = 1;
}

/// <summary>
/// Checkout an idle PowerShellManager instance in a non-blocking asynchronous way.
/// </summary>
Expand All @@ -59,28 +71,37 @@ 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.
psManager = _pool.Take();
}
}

// 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;
}

Expand Down
10 changes: 9 additions & 1 deletion src/RequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
49 changes: 30 additions & 19 deletions src/Utility/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,54 @@

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));
internal readonly static CmdletInfo RemoveModuleCmdletInfo = new CmdletInfo("Remove-Module", typeof(RemoveModuleCommand));
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<InitialSessionState> SingletonISS
= new Lazy<InitialSessionState>(NewInitialSessionState, LazyThreadSafetyMode.PublicationOnly);
private static InitialSessionState s_iss;

private static InitialSessionState NewInitialSessionState()
/// <summary>
/// Create a new PowerShell instance using our singleton InitialSessionState instance.
/// </summary>
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);
}

/// <summary>
Expand Down
29 changes: 26 additions & 3 deletions test/Unit/PowerShell/PowerShellManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}
}